Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
IJW Host (#5185)
Browse files Browse the repository at this point in the history
* First pass porting mscoree/mscoreei's IJW hosting hooks into a new .NET Core host. Functions that use runtime data structures are stubbed out.

* Use Windows heap functions for allocating executable memory.

* Add IJW activation design document.

* Add talking point about loading dependencies from *.deps.json* files and what work needs to be done for those.

* Clean up design doc.

* Update design doc.

* Feedback.

* Clean up the PEDecoder since we don't need to port over all of the validation, just enough to ensure that the assembly is a .NET assembly.

* The OS will verify that the IJW image and the IJW host are the same architecture, so we don't need to branch on architecture.

* Clean up ijwhost and PEDecoder code based on review feedback.

* Refactor corehost.cpp and add implementations of functions to fetch IJW delegates from hostfxr.

* Move hostfxr resolution into separate file and make corehost only relevant for the exe hosts.

* Make get_latest_fxr local to fxr_resolver.cpp

* Implement fxr wire-up for ijwhost.

* Rvas are already mapped for loaded images.

* Don't use stubs if being loaded into currently running runtime.

* Update IJW activation doc based on updated info about callbacks.

* Implement token resolution from thunks. Fix calling a users native entry-point from _CorDllMain.

* Correctly resolve side-by-side hostfxr from ijwhost and comhost. Correctly handle an empty TPA when appending S.P.CL. Pass app-path to delegate.

* Update design doc.

* Remove unneeded validation.

* Fix assembler selection for ARM/ARM64.

* Fix indentation.

* Remove dead code in PEDecoder.

* Fix missing CommandLineToArgvW symbol in arm/arm64 builds.

* Fix ARM/ARM64 build by bringing over custom arm assembler supporting cmake from coreclr.

* Remove IJWBootstrapThunk opaque class. Rename all non-exported apis to match the snake_case convention in this repo.

* Remove exports.cpp files per pr feedback.

* Use an enum to specify which delegate to load from the runtime in the hostfxr<->hostpolicy API.

* Fix x86 build

* Make x86 implementation of get_thunk_from_cookie clearer.

* Symbol export changes needed for x86 as found by testing.

* Remove ijw-exe-specific path.

* clean up ijwhost.cpp since we only have one entrypoint into hostfxr from ijwhost now.

* Use enum for delegate getter in hostfxr-exposed api as well.

* PR Feedback.

* Add ijwhost to Microsoft.NetCore.DotNetAppHost package.

* Setup tracing on comhost and ijwhost entry points.

* PR Feedback.

* Remove IsILOnly checks.

* Clean up design doc.

* More cleanup on IJW activation design doc

* swallow tracing on IJW. Remove as much of corhdr.h as possible.

* Fix bad copy-paste in the install command in ijwhost cmake script.

* Fix cmake

* Add error message to trace for failure to find the "corehost_get_coreclr_delegate" entrypoint.

* Sign ijwhost. Fixes #5485.
  • Loading branch information
jkoritzinsky authored Mar 20, 2019
1 parent eeb8042 commit bc7bcc6
Show file tree
Hide file tree
Showing 54 changed files with 5,621 additions and 317 deletions.
99 changes: 99 additions & 0 deletions Documentation/design-docs/IJW-activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# IJW Activation for .NET Core on Windows

To support any C++/CLI users that wish to use .NET Core, the runtime and hosting APIs must be updated to provide support activation of the managed portion of mixed-mode assemblies. Without this support, any users of C++/CLI cannot move to .NET Core without using the deprecated Visual C++ compiler `/clr:pure` switch.

## Requirements

* Discover all installed versions of .NET Core.
* Load the appropriate version of .NET Core for the assembly if a .NET Core instance is not running, or validate that the currently running .NET Core instance can satisfy the assemblies requirements.
* Load the (already-in-memory) assembly into the runtime.
* Patch the vtfixup table tokens to point to JIT stubs.

## Design

IJW activation has a variety of hard problems associated with it, mainly with loading in mixed mode assemblies that are not the application.

Specifically, since IJW assemblies can be loaded from native code while under the Windows loader lock, the library that does the activation must be passed to the linker. Additionally, the library that does the activation cannot call `LoadLibrary` or family under the loader lock. So, the host library cannot start the runtime when it is initially loaded. As a result, the host library will patch the vtfixup table with its own stubs that, when called, will load the runtime and patch the vtfixup table again with pointers to JIT stubs.

IJW applications are much easier in that they are not loaded under the loader-lock, so they only need to load the runtime, load the image in, and patch the vtfixup table once to place in JIT stubs.

### .NET Framework IJW Activation

When targetting .NET Framework, mixed mode assemblies are linked to the shim library `mscoree.dll` or `mscoreei.dll`. See the document on [COM Activation](COM-activation.md#.NET-Framework-Class-COM-Activation) for more information on the history of `mscoree.dll` and `mscoreei.dll`. C++/CLI executables are wired up to call `mscoreei.dll`'s `_CorExeMain` method on start which starts the runtime, patching the vtfixup table and calling the managed entry point. If the C++/CLI executable has a native entry point, a managed P/Invoke signature pointing to that native function in the image is emitted into the assembly as the assembly entry point.

If the assembly is a library, the library's native initialization function (what calls `DllMain` in a fully native DLL load scenario), calls into `mscoreei.dll`'s `_CorDllMain`. This `_CorDllMain` function patches the vtfixup table to have pointers to stubs that will start the runtime when called. Additionally, the runtime will call back into `mscoree.dll` when patching the vtfixup table with JIT stubs to check if the value in the table is a token or a stub to ensure that it patches the correct JIT stub in place.

Additionally, .NET Framework has support for a legacy code-path to forward calls to `_CorDllMain` to a user-provided `DllMain`. These code-paths are prone to locking under the loader lock if they call into any managed code or if the user-provided `DllMain` is a managed method, so support for a managed `DllMain` implementation is deprecated. Additionally, the Visual C++ compiler no longer generates a `DllMain` implementation for library initialization. Instead, the compiler generates a static module constructor to initialize any global state.

### .NET Core IJW Activation

Like with COM activation, our intent is to avoid a system-wide shim for IJW activation, especially since the host DLL needs to be linked to all C++/CLI assemblies. This new library (henceforth called the 'shim') will export functions to fulfill the requirements that the Visual C++ compiler needs to compile C++/CLI assemblies. Since we do not need to support backward compatibility with previously compiled mixed-mode assemblies, we are free to rename the exported functions while finalizing the design.

Below are the entry-points that the Visual C++ team needs

* `std::int32_t _CorExeMain()`
* Called from a `.exe` mixed-mode assembly on startup. Starts the runtime with the entry `.exe`.
* `BOOL _CorDllMain(HMODULE hModule, DWORD dwFlags, LPVOID lpReserved)`
* Called from a `.dll` mixed-mode assembly on load and unload.
* On load, inserts the delayed-activation thunks.
* On unload, frees the memory allocated for the delayed-activation thunks.
* Calls the user-provided native `DllMain`.

#### IJW Executables

When `_CorExeMain()` is called, the following will occur:

1) If a [`.runtimeconfig.json`](https://github.com/dotnet/cli/blob/master/Documentation/specs/runtime-configuration-file.md) file exists adjacent to the shim assembly (`<shim_name>.runtimeconfig.json`), that file will be used to describe CLR configuration details. The documentation for the `.runtimeconfig.json` format defines under what circumstances this file may be optional.
2) Using the existing `hostfxr` library, attempt to discover the desired CLR and target [framework](https://docs.microsoft.com/en-us/dotnet/core/packages#frameworks).
* If a CLR is active with the process, the requested CLR version will be validated against that CLR. If version satisfiability fails, activation will fail.
* If a CLR is **not** active with the process, an attempt will be made to create a satisfying CLR instance.
* Failure to create an instance will result in activation failure.
3) A request to the CLR will be made to load the assembly from memory and get the entry-point.
* The ability to load an assembly from memory will require exposing a new function that can be called from `hostfxr`, as well as a new API in `System.Private.CoreLib` on a new class in `Internal.Runtime.InteropServices`:

```csharp
public static class InMemoryAssemblyLoader
{
public static int LoadAndExecuteInMemoryAssembly(IntPtr handle, int argc, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 1)] string[] argv); /* argc is required for marshalling to know how large to make the argv array */
}
```

Note this API would not be exposed outside of `System.Private.CoreLib` unless we decide to do so.
* The loading of the assembly will take place in the default `AssemblyLoadContext`.

#### IJW DLLs and Delayed-Activation Thunks

When `_CorDllMain()` is called, the following will occur:

1) If `_CorDllMain` was called because the DLL is being attached to a process:
1) Calculate how many thunks we need to create from the number of entries in each record in the vtfixup table of the calling DLL.
2) Allocate executable memory for all of the thunks needed for this module.
3) Mark this chunk of thunks as associated to the calling DLL.
4) For each method in each record of the vtfixup table, initialize the thunk to call into the thunk stub (which then calls a helper to start up the runtime) and replace the stub with the original token for later patching by the runtime.
2) Call the native `DllMain` if the user provided one.
3) If `_CorDllMain` was called because the DLL is being unloaded from the process:
1) Deallocate the thunks allocated for the calling DLL.

#### Loading the Assembly Into the Runtime

When a delayed-activation thunk is called, it will be outside of the loader lock. So, we can load the runtime. We can now follow steps 1 and 2 from the section on [IJW Executables](#IJW-Executables). Finally, we will need another new function on `hostfxr` and a new API in `System.Private.CoreLib` in `Internal.Runtime.InteropServices`:

```csharp
public static class InMemoryAssemblyLoader
{
public static unsafe void LoadInMemoryAssembly(IntPtr handle, char* modulePath);
}
```

Note this API would not be exposed outside of `System.Private.CoreLib` unless we decide to do so.
* The loading of this assembly will take place in an isolated `AssemblyLoadContext`.

The naming of these APIs is designed to be useful for non-IJW scenarios as well, such as possibly Single-Exe.

When the runtime loads the assembly, it needs to know if each element in the vtfixup table is a token or a stub. In .NET Framework, this check is implemented by the runtime querying `mscoree.dll` by looking up callbacks. When the runtime is traversing the vtfixup table and updating the entries to point to JIT stubs, it queries `mscoree.dll` if the module has stubs. If the module has stubs, it calls back into `mscoree.dll` to query the stub data structures for the metadata token. Otherwise, it grabs the token from the slot.

We will implement it similarly, by having CoreCLR call back into the IJW assembly's shim. We will discover this shim by traversing the IJW assembly's import table to find the `_CorDllMain` import, and from there resolve the shim's `HMODULE`. Since it is technically possible to craft a non-IJW assembly that exports functions via the vtfixup table, we will enable CoreCLR to resolve the tokens from the table in the simple case where no delayed-activation thunks are used.

#### Caveats

Since native images can only be loaded into memory once on Windows, there is only one instance of the vtfixup table. As a result, the native code in an IJW assembly will always call into managed code from the first managed load of the assembly. As a result, if an IJW assembly is loaded into two different ALCs, then a call to managed code in an IJW assembly that calls into native code and back into managed within the IJW assembly may change ALCs within the stack if the call into the IJW assembly is in a different ALC than the IJW assembly was initially loaded into. We have a test that reproduces this behavior.
3 changes: 3 additions & 0 deletions signing/sign.proj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<FilesToSign Include="$(OutDir)corehost/**/dotnet.exe">
<Authenticode>$(CertificateId)</Authenticode>
</FilesToSign>
<FilesToSign Include="$(OutDir)corehost/**/ijwhost.dll">
<Authenticode>$(CertificateId)</Authenticode>
</FilesToSign>
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/corehost/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cmake_minimum_required (VERSION 2.6)

include(../settings.cmake)
include(../functions.cmake)
add_subdirectory(cli)
4 changes: 2 additions & 2 deletions src/corehost/Windows/gen-buildsys-win.bat
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ for /f "delims=" %%a in ('powershell -NoProfile -ExecutionPolicy ByPass "& .\Win
popd

:DoGen
echo "%CMakePath%" %__sourceDir% %__SDKVersion% "-DCLI_CMAKE_RUNTIME_ID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_HOST_VER:STRING=%__HostVersion%" "-DCLI_CMAKE_APPHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_COMHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_HOST_FXR_VER:STRING=%__HostFxrVersion%" "-DCLI_CMAKE_HOST_POLICY_VER:STRING=%__HostPolicyVersion%" "-DCLI_CMAKE_PKG_RID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_COMMIT_HASH:STRING=%__LatestCommit%" "-DCLI_CMAKE_PLATFORM_ARCH_%cm_Arch%=1" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DCLI_CMAKE_RESOURCE_DIR:STRING=%__ResourcesDir%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams%
"%CMakePath%" %__sourceDir% %__SDKVersion% "-DCLI_CMAKE_RUNTIME_ID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_HOST_VER:STRING=%__HostVersion%" "-DCLI_CMAKE_APPHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_COMHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_HOST_FXR_VER:STRING=%__HostFxrVersion%" "-DCLI_CMAKE_HOST_POLICY_VER:STRING=%__HostPolicyVersion%" "-DCLI_CMAKE_PKG_RID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_COMMIT_HASH:STRING=%__LatestCommit%" "-DCLI_CMAKE_PLATFORM_ARCH_%cm_Arch%=1" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DCLI_CMAKE_RESOURCE_DIR:STRING=%__ResourcesDir%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams%
echo "%CMakePath%" %__sourceDir% %__SDKVersion% "-DCLI_CMAKE_RUNTIME_ID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_HOST_VER:STRING=%__HostVersion%" "-DCLI_CMAKE_APPHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_COMHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_IJWHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_HOST_FXR_VER:STRING=%__HostFxrVersion%" "-DCLI_CMAKE_HOST_POLICY_VER:STRING=%__HostPolicyVersion%" "-DCLI_CMAKE_PKG_RID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_COMMIT_HASH:STRING=%__LatestCommit%" "-DCLI_CMAKE_PLATFORM_ARCH_%cm_Arch%=1" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DCLI_CMAKE_RESOURCE_DIR:STRING=%__ResourcesDir%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams%
"%CMakePath%" %__sourceDir% %__SDKVersion% "-DCLI_CMAKE_RUNTIME_ID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_HOST_VER:STRING=%__HostVersion%" "-DCLI_CMAKE_APPHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_COMHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_IJWHOST_VER:STRING=%__AppHostVersion%" "-DCLI_CMAKE_HOST_FXR_VER:STRING=%__HostFxrVersion%" "-DCLI_CMAKE_HOST_POLICY_VER:STRING=%__HostPolicyVersion%" "-DCLI_CMAKE_PKG_RID:STRING=%cm_BaseRid%" "-DCLI_CMAKE_COMMIT_HASH:STRING=%__LatestCommit%" "-DCLI_CMAKE_PLATFORM_ARCH_%cm_Arch%=1" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DCLI_CMAKE_RESOURCE_DIR:STRING=%__ResourcesDir%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams%
endlocal
GOTO :DONE

Expand Down
3 changes: 3 additions & 0 deletions src/corehost/build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<HostFiles Include="comhost">
<FileDescription>.NET Core COM Host</FileDescription>
</HostFiles>
<HostFiles Include="ijwhost">
<FileDescription>.NET Core IJW Host</FileDescription>
</HostFiles>
</ItemGroup>

<MSBuild Projects="$(MSBuildProjectFullPath)"
Expand Down
3 changes: 2 additions & 1 deletion src/corehost/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ add_subdirectory(test)

if(WIN32)
add_subdirectory(comhost)
endif()
add_subdirectory(ijwhost)
endif()
4 changes: 2 additions & 2 deletions src/corehost/cli/comhost/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ include_directories(../json/casablanca/include)

# CMake does not recommend using globbing since it messes with the freshness checks
set(SOURCES
../../corehost.cpp
exports.cpp
comhost.cpp
../fxr_resolver.cpp
clsidmap.cpp
../fxr/fx_ver.cpp
../json/casablanca/src/json/json.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include "comhost.h"
#include <corehost.h>
#include <error_codes.h>
#include <trace.h>
#include "hostfxr.h"
#include "fxr_resolver.h"
#include "pal.h"
#include "trace.h"
#include "error_codes.h"
#include "utils.h"
#include <type_traits>

using comhost::clsid_map_entry;
Expand All @@ -23,6 +26,22 @@ using comhost::clsid_map;

#endif // _WIN32


//
// See ComActivator class in System.Private.CoreLib
//
struct com_activation_context
{
GUID class_id;
GUID interface_id;
const pal::char_t *assembly_path;
const pal::char_t *assembly_name;
const pal::char_t *type_name;
void **class_factory_dest;
};

using com_activation_fn = int(*)(com_activation_context*);

namespace
{
pal::stringstream_t & get_comhost_error_stream()
Expand All @@ -42,6 +61,53 @@ namespace
{
get_comhost_error_stream() << msg;
}

int get_com_activation_delegate(pal::string_t *app_path, com_activation_fn *delegate)
{
pal::string_t host_path;
if (!pal::get_own_module_path(&host_path) || !pal::realpath(&host_path))
{
trace::error(_X("Failed to resolve full path of the current host module [%s]"), host_path.c_str());
return StatusCode::CoreHostCurHostFindFailure;
}

pal::string_t dotnet_root;
pal::string_t fxr_path;
if (!resolve_fxr_path(get_directory(host_path), &dotnet_root, &fxr_path))
{
return StatusCode::CoreHostLibMissingFailure;
}

// Load library
pal::dll_t fxr;
if (!pal::load_library(&fxr_path, &fxr))
{
trace::error(_X("The library %s was found, but loading it from %s failed"), LIBFXR_NAME, fxr_path.c_str());
trace::error(_X(" - Installing .NET Core prerequisites might help resolve this problem."));
trace::error(_X(" %s"), DOTNET_CORE_INSTALL_PREREQUISITES_URL);
return StatusCode::CoreHostLibLoadFailure;
}

// Leak fxr

auto get_runtime_delegate = (hostfxr_get_delegate_fn)pal::get_symbol(fxr, "hostfxr_get_runtime_delegate");
if (get_runtime_delegate == nullptr)
return StatusCode::CoreHostEntryPointFailure;

pal::string_t app_path_local{ host_path };

// Strip the comhost suffix to get the 'app'
size_t idx = app_path_local.rfind(_X(".comhost.dll"));
assert(idx != pal::string_t::npos);
app_path_local.replace(app_path_local.begin() + idx, app_path_local.end(), _X(".dll"));

*app_path = std::move(app_path_local);

auto set_error_writer_fn = (hostfxr_set_error_writer_fn)pal::get_symbol(fxr, "hostfxr_set_error_writer");
propagate_error_writer_t propagate_error_writer_to_hostfxr(set_error_writer_fn);

return get_runtime_delegate(host_path.c_str(), dotnet_root.c_str(), app_path->c_str(), hostfxr_delegate_type::com_activation, (void**)delegate);
}
}

COM_API HRESULT STDMETHODCALLTYPE DllGetClassObject(
Expand All @@ -61,8 +127,10 @@ COM_API HRESULT STDMETHODCALLTYPE DllGetClassObject(
pal::string_t app_path;
com_activation_fn act;
{
trace::setup();
reset_comhost_error_stream();
trace::set_error_writer(comhost_error_writer);

error_writer_scope_t writer_scope(comhost_error_writer);

int ec = get_com_activation_delegate(&app_path, &act);
if (ec != StatusCode::Success)
Expand Down
2 changes: 2 additions & 0 deletions src/corehost/cli/deps_resolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,8 @@ bool deps_resolver_t::resolve_tpa_list(
}
};

// We do not support self-contained in a libhost scenario since in the self-contained scenario,
// we cannot determine what assemblies are framework assemblies, and what assemblies are app-local assemblies.
if (m_host_mode != host_mode_t::libhost)
{
// First add managed assembly to the TPA.
Expand Down
5 changes: 4 additions & 1 deletion src/corehost/cli/exe.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ include_directories(${CMAKE_CURRENT_LIST_DIR}/fxr)

# CMake does not recommend using globbing since it messes with the freshness checks
list(APPEND SOURCES
${CMAKE_CURRENT_LIST_DIR}/../corehost.cpp)
${CMAKE_CURRENT_LIST_DIR}/fxr_resolver.cpp
${CMAKE_CURRENT_LIST_DIR}/../corehost.cpp
${CMAKE_CURRENT_LIST_DIR}/../common/trace.cpp
${CMAKE_CURRENT_LIST_DIR}/../common/utils.cpp)

add_executable(${DOTNET_PROJECT_NAME} ${SOURCES} ${RESOURCES})

Expand Down
Loading

0 comments on commit bc7bcc6

Please sign in to comment.