From b33d4e34e1cbf993583d78fc1b64ea8400935978 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 1 May 2025 19:02:06 +0000 Subject: [PATCH] Merged PR 49480: [internal/release/8.0] Fix issue where libhost scenarios (COM, C++/CLI, custom component host) could try to load coreclr from CWD There is a fallback for apps with no .deps.json where the host will consider the app directory for loading coreclr. In component hosting scenarios, we do not have an app path / directory. We were incorrectly going down the path of looking for coreclr next to the empty app directory, which resulted in looking in the current directory. This change skips that path for libhost scenarios. It also adds checks that the paths we determine for loading coreclr, hostpolicy, and hostfxr are absolute. --- .../NativeHosting/Comhost.cs | 29 +++++++++++++++++ .../NativeHosting/Ijwhost.cs | 28 ++++++++++++++++ .../LoadAssemblyAndGetFunctionPointer.cs | 32 +++++++++++++++++++ ...ns.cs => NativeHostingResultExtensions.cs} | 19 ++++++++++- src/installer/tests/TestUtils/TestArtifact.cs | 14 ++++++++ .../apphost/standalone/hostfxr_resolver.cpp | 5 +++ .../fxr/standalone/hostpolicy_resolver.cpp | 4 +++ src/native/corehost/fxr_resolver.h | 4 +++ .../corehost/hostpolicy/deps_resolver.cpp | 14 ++++++-- .../standalone/coreclr_resolver.cpp | 4 +++ 10 files changed, 149 insertions(+), 4 deletions(-) rename src/installer/tests/HostActivation.Tests/NativeHosting/{FunctionPointerResultExtensions.cs => NativeHostingResultExtensions.cs} (69%) diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs index 4d023023d9791f..5f10c3672fd102 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs @@ -116,6 +116,35 @@ public void ActivateClass_IgnoreAppLocalHostFxr() } } + [Fact] + public void ActivateClass_IgnoreWorkingDirectory() + { + using (TestArtifact cwd = TestArtifact.Create("cwd")) + { + // Validate that hosting components in the working directory will not be used + File.Copy(Binaries.CoreClr.MockPath, Path.Combine(cwd.Location, Binaries.CoreClr.FileName)); + File.Copy(Binaries.HostFxr.MockPath_5_0, Path.Combine(cwd.Location, Binaries.HostFxr.FileName)); + File.Copy(Binaries.HostPolicy.MockPath, Path.Combine(cwd.Location, Binaries.HostPolicy.FileName)); + + string[] args = { + "comhost", + "synchronous", + "1", + sharedState.ComHostPath, + sharedState.ClsidString + }; + sharedState.CreateNativeHostCommand(args, sharedState.ComLibraryFixture.BuiltDotnet.BinPath) + .WorkingDirectory(cwd.Location) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("New instance of Server created") + .And.HaveStdOutContaining($"Activation of {sharedState.ClsidString} succeeded.") + .And.ResolveHostFxr(sharedState.ComLibraryFixture.BuiltDotnet) + .And.ResolveHostPolicy(sharedState.ComLibraryFixture.BuiltDotnet) + .And.ResolveCoreClr(sharedState.ComLibraryFixture.BuiltDotnet); + } + } + [Fact] public void ActivateClass_ValidateIErrorInfoResult() { diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/Ijwhost.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/Ijwhost.cs index 062e6bcc58130f..9fd2dfa0ee57b5 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/Ijwhost.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/Ijwhost.cs @@ -73,6 +73,34 @@ public void LoadLibrary_ContextConfig(bool load_isolated) } } + [Fact] + public void LoadLibrary_IgnoreWorkingDirectory() + { + using (TestArtifact cwd = TestArtifact.Create("cwd")) + { + // Validate that hosting components in the working directory will not be used + File.Copy(Binaries.CoreClr.MockPath, Path.Combine(cwd.Location, Binaries.CoreClr.FileName)); + File.Copy(Binaries.HostFxr.MockPath_5_0, Path.Combine(cwd.Location, Binaries.HostFxr.FileName)); + File.Copy(Binaries.HostPolicy.MockPath, Path.Combine(cwd.Location, Binaries.HostPolicy.FileName)); + + string [] args = { + "ijwhost", + sharedState.IjwApp.AppDll, + "NativeEntryPoint" + }; + var dotnet = new Microsoft.DotNet.Cli.Build.DotNetCli(sharedState.RepoDirectories.BuiltDotnet); + sharedState.CreateNativeHostCommand(args, sharedState.RepoDirectories.BuiltDotnet) + .WorkingDirectory(cwd.Location) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("[C++/CLI] NativeEntryPoint: calling managed class") + .And.HaveStdOutContaining("[C++/CLI] ManagedClass: AssemblyLoadContext = \"Default\" System.Runtime.Loader.DefaultAssemblyLoadContext") + .And.ResolveHostFxr(dotnet) + .And.ResolveHostPolicy(dotnet) + .And.ResolveCoreClr(dotnet); + } + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/LoadAssemblyAndGetFunctionPointer.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/LoadAssemblyAndGetFunctionPointer.cs index 653a44eb289677..6fd5be36a99d75 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/LoadAssemblyAndGetFunctionPointer.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/LoadAssemblyAndGetFunctionPointer.cs @@ -234,6 +234,38 @@ public void CallDelegateOnComponentContext_UnhandledException() .And.ExecuteFunctionPointerWithException(entryPoint, 1); } + [Fact] + public void CallDelegateOnComponentContext_IgnoreWorkingDirectory() + { + using (TestArtifact cwd = TestArtifact.Create("cwd")) + { + // Validate that hosting components in the working directory will not be used + File.Copy(Binaries.CoreClr.MockPath, Path.Combine(cwd.Location, Binaries.CoreClr.FileName)); + File.Copy(Binaries.HostPolicy.MockPath, Path.Combine(cwd.Location, Binaries.HostPolicy.FileName)); + + var component = sharedState.ComponentWithNoDependenciesFixture.TestProject; + string[] args = + { + ComponentLoadAssemblyAndGetFunctionPointerArg, + sharedState.HostFxrPath, + component.RuntimeConfigJson, + component.AppDll, + sharedState.ComponentTypeName, + sharedState.ComponentEntryPoint1, + }; + + var dotnet = new Microsoft.DotNet.Cli.Build.DotNetCli(sharedState.DotNetRoot); + sharedState.CreateNativeHostCommand(args, sharedState.DotNetRoot) + .WorkingDirectory(cwd.Location) + .Execute() + .Should().Pass() + .And.InitializeContextForConfig(component.RuntimeConfigJson) + .And.ExecuteFunctionPointer(sharedState.ComponentEntryPoint1, 1, 1) + .And.ResolveHostPolicy(dotnet) + .And.ResolveCoreClr(dotnet); + } + } + public class SharedTestState : SharedTestStateBase { public string HostFxrPath { get; } diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/FunctionPointerResultExtensions.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/NativeHostingResultExtensions.cs similarity index 69% rename from src/installer/tests/HostActivation.Tests/NativeHosting/FunctionPointerResultExtensions.cs rename to src/installer/tests/HostActivation.Tests/NativeHosting/NativeHostingResultExtensions.cs index 09fdc52bc186bf..9a0447a219dcb9 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/FunctionPointerResultExtensions.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/NativeHostingResultExtensions.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting { - internal static class FunctionPointerResultExtensions + internal static class NativeHostingResultExtensions { public static FluentAssertions.AndConstraint ExecuteFunctionPointer(this CommandResultAssertions assertion, string methodName, int callCount, int returnValue) { @@ -47,5 +48,21 @@ public static FluentAssertions.AndConstraint ExecuteWit { return assertion.HaveStdOutContaining($"{assemblyName}: Location = '{location}'"); } + + public static FluentAssertions.AndConstraint ResolveHostFxr(this CommandResultAssertions assertion, Microsoft.DotNet.Cli.Build.DotNetCli dotnet) + { + return assertion.HaveStdErrContaining($"Resolved fxr [{dotnet.GreatestVersionHostFxrFilePath}]"); + } + + public static FluentAssertions.AndConstraint ResolveHostPolicy(this CommandResultAssertions assertion, Microsoft.DotNet.Cli.Build.DotNetCli dotnet) + { + return assertion.HaveStdErrContaining($"{Binaries.HostPolicy.FileName} directory is [{dotnet.GreatestVersionSharedFxPath}]"); + } + + public static FluentAssertions.AndConstraint ResolveCoreClr(this CommandResultAssertions assertion, Microsoft.DotNet.Cli.Build.DotNetCli dotnet) + { + return assertion.HaveStdErrContaining($"CoreCLR path = '{Path.Combine(dotnet.GreatestVersionSharedFxPath, Binaries.CoreClr.FileName)}'") + .And.HaveStdErrContaining($"CoreCLR dir = '{dotnet.GreatestVersionSharedFxPath}{Path.DirectorySeparatorChar}'"); + } } } diff --git a/src/installer/tests/TestUtils/TestArtifact.cs b/src/installer/tests/TestUtils/TestArtifact.cs index c900bba17005c5..23fac7eceec575 100644 --- a/src/installer/tests/TestUtils/TestArtifact.cs +++ b/src/installer/tests/TestUtils/TestArtifact.cs @@ -52,6 +52,20 @@ protected TestArtifact(TestArtifact source) source._copies.Add(this); } + /// + /// Create a new test artifact. + /// + /// Name of the test artifact + /// Test artifact containing no files + public static TestArtifact Create(string name) + { + var (location, parentPath) = GetNewTestArtifactPath(name); + return new TestArtifact(location) + { + DirectoryToDelete = parentPath + }; + } + protected void RegisterCopy(TestArtifact artifact) { _copies.Add(artifact); diff --git a/src/native/corehost/apphost/standalone/hostfxr_resolver.cpp b/src/native/corehost/apphost/standalone/hostfxr_resolver.cpp index 2c244807ac9a94..d06be0719551f5 100644 --- a/src/native/corehost/apphost/standalone/hostfxr_resolver.cpp +++ b/src/native/corehost/apphost/standalone/hostfxr_resolver.cpp @@ -38,6 +38,11 @@ hostfxr_resolver_t::hostfxr_resolver_t(const pal::string_t& app_root) { m_status_code = StatusCode::CoreHostLibMissingFailure; } + else if (!pal::is_path_rooted(m_fxr_path)) + { + // We should always be loading hostfxr from an absolute path + m_status_code = StatusCode::CoreHostLibMissingFailure; + } else if (pal::load_library(&m_fxr_path, &m_hostfxr_dll)) { m_status_code = StatusCode::Success; diff --git a/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp b/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp index 67ddd18de3a488..bb0da3238f42b6 100644 --- a/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp +++ b/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp @@ -180,6 +180,10 @@ int hostpolicy_resolver::load( return StatusCode::CoreHostLibMissingFailure; } + // We should always be loading hostpolicy from an absolute path + if (!pal::is_path_rooted(host_path)) + return StatusCode::CoreHostLibMissingFailure; + // Load library // We expect to leak hostpolicy - just as we do not unload coreclr, we do not unload hostpolicy if (!pal::load_library(&host_path, &g_hostpolicy)) diff --git a/src/native/corehost/fxr_resolver.h b/src/native/corehost/fxr_resolver.h index 3df6c2de3053d3..ecbf37c12b1ffc 100644 --- a/src/native/corehost/fxr_resolver.h +++ b/src/native/corehost/fxr_resolver.h @@ -44,6 +44,10 @@ int load_fxr_and_get_delegate(hostfxr_delegate_type type, THostPathToConfigCallb return StatusCode::CoreHostLibMissingFailure; } + // We should always be loading hostfxr from an absolute path + if (!pal::is_path_rooted(fxr_path)) + return StatusCode::CoreHostLibMissingFailure; + // Load library if (!pal::load_library(&fxr_path, &fxr)) { diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index 66d44f0c17dae0..a00f4dabc71545 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -830,13 +830,21 @@ bool deps_resolver_t::resolve_probe_dirs( } } - // If the deps file is missing add known locations. - if (!get_app_deps().exists()) + // If the deps file is missing for the app, add known locations. + // For libhost scenarios, there is no app or app path + if (m_host_mode != host_mode_t::libhost && !get_app_deps().exists()) { + assert(!m_app_dir.empty()); + // App local path add_unique_path(asset_type, m_app_dir, &items, output, &non_serviced, core_servicing); - (void) library_exists_in_dir(m_app_dir, LIBCORECLR_NAME, &m_coreclr_path); + if (m_coreclr_path.empty()) + { + // deps_resolver treats being able to get the coreclr path as optional, so we ignore the return value here. + // The caller is responsible for checking whether coreclr path is set and handling as appropriate. + (void) library_exists_in_dir(m_app_dir, LIBCORECLR_NAME, &m_coreclr_path); + } } // Handle any additional deps.json that were specified. diff --git a/src/native/corehost/hostpolicy/standalone/coreclr_resolver.cpp b/src/native/corehost/hostpolicy/standalone/coreclr_resolver.cpp index b040c3e8546278..8df8e395e3f259 100644 --- a/src/native/corehost/hostpolicy/standalone/coreclr_resolver.cpp +++ b/src/native/corehost/hostpolicy/standalone/coreclr_resolver.cpp @@ -13,6 +13,10 @@ bool coreclr_resolver_t::resolve_coreclr(const pal::string_t& libcoreclr_path, c pal::string_t coreclr_dll_path(libcoreclr_path); append_path(&coreclr_dll_path, LIBCORECLR_NAME); + // We should always be loading coreclr from an absolute path + if (!pal::is_path_rooted(coreclr_dll_path)) + return false; + if (!pal::load_library(&coreclr_dll_path, &coreclr_resolver_contract.coreclr)) { return false;