diff --git a/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs index f76aa7daade17e..0fd0ff7b7564b7 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/AppContext.CoreCLR.cs @@ -9,6 +9,17 @@ namespace System { public static partial class AppContext { + // Keep in sync with IsKnownHostProperty in src/coreclr/dlls/mscoree/exports.cpp. + private static bool IsKnownHostProperty(string name) + => name is "TRUSTED_PLATFORM_ASSEMBLIES" + or "NATIVE_DLL_SEARCH_DIRECTORIES" + or "PLATFORM_RESOURCE_ROOTS" + or "APP_PATHS"; + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AppContext_TryGetHostPropertyValue", StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool TryGetHostPropertyValue(string name, StringHandleOnStack retValue); + [UnmanagedCallersOnly] private static unsafe void OnProcessExit(Exception* pException) { diff --git a/src/coreclr/dlls/mscoree/exports.cpp b/src/coreclr/dlls/mscoree/exports.cpp index 07b4d9761e19c5..30f1846c848c79 100644 --- a/src/coreclr/dlls/mscoree/exports.cpp +++ b/src/coreclr/dlls/mscoree/exports.cpp @@ -74,6 +74,48 @@ class ConstWStringArrayHolder : public NewArrayHolder } }; +// Returns true for known host properties that do not need to be stored as CLR configuration. +// The runtime parses these into binder/AppDomain structures during AppDomain creation, so it +// does not need to also keep the original raw strings around in the CLR config knobs. +static bool IsKnownHostProperty(LPCSTR key) +{ + return strcmp(key, HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES) == 0 + || strcmp(key, HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES) == 0 + || strcmp(key, HOST_PROPERTY_PLATFORM_RESOURCE_ROOTS) == 0 + || strcmp(key, HOST_PROPERTY_APP_PATHS) == 0; +} + +struct HostPropertyArrays final +{ + int count; + const char** keys; + LPCWSTR* keysW; + LPCWSTR* valuesW; +}; + +// Frees the key and value arrays and the wide-string copies of the known host probing-path +// properties. The remaining strings are handed to the CLR configuration knobs, which retain +// them for the process lifetime, so they are not freed here. +struct HostPropertyArraysTraits final +{ + using Type = HostPropertyArrays; + static Type Default() { return {}; } + static void Free(Type arrays) + { + for (int i = 0; i < arrays.count; ++i) + { + if (IsKnownHostProperty(arrays.keys[i])) + { + delete[] arrays.keysW[i]; + delete[] arrays.valuesW[i]; + } + } + + delete[] arrays.keysW; + delete[] arrays.valuesW; + } +}; + // Convert 8 bit string to unicode static LPCWSTR StringToUnicode(LPCSTR str) { @@ -294,8 +336,35 @@ int coreclr_initialize( Bundle::AppBundle = &bundle; } - // This will take ownership of propertyKeysWTemp and propertyValuesWTemp - Configuration::InitializeConfigurationKnobs(propertyCount, propertyKeysW, propertyValuesW); + // Take ownership of the converted (unfiltered) property arrays. The unfiltered arrays + // are passed to CreateAppDomainWithManager so the binder/AppDomain can parse them. + // Afterward, the arrays and the excluded strings are freed. The other property strings + // are handed to the CLR config knobs, which own them for the process lifetime. + LifetimeHolder hostPropertiesHolder{ HostPropertyArrays{ propertyCount, propertyKeys, propertyKeysW, propertyValuesW } }; + + // Build a filtered set of properties for the CLR config knobs that excludes probing path + // properties (TPA, NATIVE_DLL_SEARCH_DIRECTORIES, PLATFORM_RESOURCE_ROOTS, APP_PATHS). + // Those are parsed into binder/AppDomain structures during CreateAppDomainWithManager, + // so the runtime does not need a duplicate copy held forever in the CLR config knobs. + // The TPA list in particular can be tens of KB. + LPCWSTR* configKeysW = new (nothrow) LPCWSTR[propertyCount]; + ASSERTE_ALL_BUILDS(configKeysW != nullptr); + LPCWSTR* configValuesW = new (nothrow) LPCWSTR[propertyCount]; + ASSERTE_ALL_BUILDS(configValuesW != nullptr); + + int configPropertyCount = 0; + for (int i = 0; i < propertyCount; ++i) + { + if (IsKnownHostProperty(propertyKeys[i])) + continue; + + configKeysW[configPropertyCount] = propertyKeysW[i]; + configValuesW[configPropertyCount] = propertyValuesW[i]; + ++configPropertyCount; + } + + // Configuration takes ownership of the filtered arrays and the strings they reference + Configuration::InitializeConfigurationKnobs(configPropertyCount, configKeysW, configValuesW); #ifdef TARGET_UNIX if (Configuration::GetKnobBooleanValue(W("System.Runtime.CrashReportBeforeSignalChaining"), CLRConfig::INTERNAL_CrashReportBeforeSignalChaining)) diff --git a/src/coreclr/vm/appdomainnative.cpp b/src/coreclr/vm/appdomainnative.cpp index ada5e6c7b689a0..ffcf604e4e5280 100644 --- a/src/coreclr/vm/appdomainnative.cpp +++ b/src/coreclr/vm/appdomainnative.cpp @@ -11,6 +11,9 @@ #include "appdomain.inl" #include "eventtrace.h" #include "../binder/inc/defaultassemblybinder.h" +#include "../binder/inc/applicationcontext.hpp" +#include +#include "stringarraylist.h" // static extern "C" void QCALLTYPE AppDomain_CreateDynamicAssembly(QCall::ObjectHandleOnStack assemblyLoadContext, NativeAssemblyNameParts* pAssemblyNameParts, INT32 hashAlgorithm, INT32 access, QCall::ObjectHandleOnStack retAssembly) @@ -112,6 +115,125 @@ extern "C" void QCALLTYPE AssemblyNative_GetLoadedAssemblies(QCall::ObjectHandle END_QCALL; } +namespace +{ + // Append all paths from a StringArrayList into 'output', separated by PATH_SEPARATOR_CHAR_W. + void AppendStringArrayList(StringArrayList* pList, SString& output) + { + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + _ASSERTE(pList != NULL); + + for (DWORD i = 0; i < pList->GetCount(); ++i) + { + if (i != 0) + output.Append(PATH_SEPARATOR_CHAR_W); + + output.Append(pList->Get(i)); + } + } +} + +// Get the value of a known host property from the binder/AppDomain state. +extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall::StringHandleOnStack retValue) +{ + QCALL_CONTRACT; + + BOOL found = FALSE; + + BEGIN_QCALL; + + AppDomain* pDomain = AppDomain::GetCurrentDomain(); + DefaultAssemblyBinder* pBinder = pDomain->GetDefaultBinder(); + BINDER_SPACE::ApplicationContext* pAppContext = pBinder->GetAppContext(); + + if (u16_strcmp(name, _T(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES)) == 0) + { + if (pAppContext->IsTpaListProvided()) + { + BINDER_SPACE::SimpleNameToFileNameMap* pMap = pAppContext->GetTpaList(); + _ASSERTE(pMap != NULL); + + SString result; + BINDER_SPACE::SimpleNameToFileNameMap::Iterator i = pMap->Begin(); + BINDER_SPACE::SimpleNameToFileNameMap::Iterator end = pMap->End(); + while (i != end) + { + if (i->m_wszILFileName != NULL) + { + if (!result.IsEmpty()) + result.Append(PATH_SEPARATOR_CHAR_W); + + result.Append(i->m_wszILFileName); + } + + ++i; + } + + if (!result.IsEmpty()) + { + retValue.Set(result); + found = TRUE; + } + } + } + else if (u16_strcmp(name, _T(HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES)) == 0) + { + SString result; + AppDomain::PathIterator iter = pDomain->IterateNativeDllSearchDirectories(); + while (iter.Next()) + { + if (!result.IsEmpty()) + result.Append(PATH_SEPARATOR_CHAR_W); + + result.Append(*iter.GetPath()); + } + + if (!result.IsEmpty()) + { + retValue.Set(result); + found = TRUE; + } + } + else if (u16_strcmp(name, _T(HOST_PROPERTY_PLATFORM_RESOURCE_ROOTS)) == 0) + { + StringArrayList* pList = pAppContext->GetPlatformResourceRoots(); + if (pList != NULL && pList->GetCount() > 0) + { + SString result; + AppendStringArrayList(pList, result); + retValue.Set(result); + found = TRUE; + } + } + else if (u16_strcmp(name, _T(HOST_PROPERTY_APP_PATHS)) == 0) + { + StringArrayList* pList = pAppContext->GetAppPaths(); + if (pList != NULL && pList->GetCount() > 0) + { + SString result; + AppendStringArrayList(pList, result); + retValue.Set(result); + found = TRUE; + } + } + else + { + // Caller is expected to only request known properties. + _ASSERTE(!"AppContext_TryGetHostPropertyValue called with unknown name"); + } + + END_QCALL; + + return found; +} + extern "C" void QCALLTYPE String_IsInterned(QCall::StringHandleOnStack str) { QCALL_CONTRACT; diff --git a/src/coreclr/vm/appdomainnative.hpp b/src/coreclr/vm/appdomainnative.hpp index d1dc7cfad1b998..1ddcc1da98df49 100644 --- a/src/coreclr/vm/appdomainnative.hpp +++ b/src/coreclr/vm/appdomainnative.hpp @@ -21,6 +21,7 @@ extern "C" void QCALLTYPE String_IsInterned(QCall::StringHandleOnStack str); extern "C" void QCALLTYPE AppDomain_CreateDynamicAssembly(QCall::ObjectHandleOnStack assemblyLoadContext, NativeAssemblyNameParts* pAssemblyName, INT32 hashAlgorithm, INT32 access, QCall::ObjectHandleOnStack retAssembly); extern "C" void QCALLTYPE AppContext_SetFirstChanceExceptionHandler(); +extern "C" BOOL QCALLTYPE AppContext_TryGetHostPropertyValue(LPCWSTR name, QCall::StringHandleOnStack retValue); extern "C" void QCALLTYPE AssemblyNative_GetLoadedAssemblies(QCall::ObjectHandleOnStack retAssemblies); diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index 85b240951b7ca1..680059381416be 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -291,6 +291,7 @@ static const Entry s_QCall[] = DllImportEntry(String_IsInterned) DllImportEntry(AppDomain_CreateDynamicAssembly) DllImportEntry(AppContext_SetFirstChanceExceptionHandler) + DllImportEntry(AppContext_TryGetHostPropertyValue) DllImportEntry(ThreadNative_Start) DllImportEntry(ThreadNative_SetPriority) DllImportEntry(ThreadNative_GetCurrentThread) diff --git a/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs b/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs index a7cd246b848e36..a85effb7b26085 100644 --- a/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs +++ b/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs @@ -3,7 +3,10 @@ using System; using System.IO; +using System.Text.RegularExpressions; +using FluentAssertions; using Microsoft.DotNet.Cli.Build; +using Microsoft.DotNet.Cli.Build.Framework; using Microsoft.DotNet.CoreSetup.Test; using Microsoft.DotNet.TestUtils; using Xunit; @@ -29,7 +32,7 @@ public void AppConfigProperty_AppCanGetData() .Execute() .Should().Pass() .And.HaveStdErrContaining($"Property {SharedTestState.AppTestPropertyName} = {SharedTestState.AppTestPropertyValue}") - .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.AppTestPropertyName}) = {SharedTestState.AppTestPropertyValue}"); + .And.HavePropertyValue(SharedTestState.AppTestPropertyName, SharedTestState.AppTestPropertyValue); } [Fact] @@ -40,7 +43,7 @@ public void FrameworkConfigProperty_AppCanGetData() .Execute() .Should().Pass() .And.HaveStdErrContaining($"Property {SharedTestState.FrameworkTestPropertyName} = {SharedTestState.FrameworkTestPropertyValue}") - .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.FrameworkTestPropertyName}) = {SharedTestState.FrameworkTestPropertyValue}"); + .And.HavePropertyValue(SharedTestState.FrameworkTestPropertyName, SharedTestState.FrameworkTestPropertyValue); } [Fact] @@ -56,7 +59,7 @@ public void DuplicateConfigProperty_AppConfigValueUsed() .Execute() .Should().Pass() .And.HaveStdErrContaining($"Property {SharedTestState.FrameworkTestPropertyName} = {SharedTestState.AppTestPropertyValue}") - .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.FrameworkTestPropertyName}) = {SharedTestState.AppTestPropertyValue}"); + .And.HavePropertyValue(SharedTestState.FrameworkTestPropertyName, SharedTestState.AppTestPropertyValue); } [Fact] @@ -77,7 +80,7 @@ public void HostFxrPathProperty_NotVisibleFromApp() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdOutContaining($"Property '{SharedTestState.HostFxrPathPropertyName}' was not found."); + .And.NotFindProperty(SharedTestState.HostFxrPathPropertyName); } [Fact] @@ -112,7 +115,24 @@ public void SpecifiedInConfigAndDevConfig_DevConfigWins() .Execute() .Should().Pass() .And.HaveStdErrContaining($"Property {SharedTestState.AppTestPropertyName} = {devConfigValue}") - .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.AppTestPropertyName}) = {devConfigValue}"); + .And.HavePropertyValue(SharedTestState.AppTestPropertyName, devConfigValue); + } + + [Fact] + public void KnownHostProperty_AppCanGetData() + { + sharedState.DotNet.Exec(sharedState.App.AppDll, PrintProperties, + "TRUSTED_PLATFORM_ASSEMBLIES", "NATIVE_DLL_SEARCH_DIRECTORIES", + "PLATFORM_RESOURCE_ROOTS", "APP_PATHS") + .EnableTracingAndCaptureOutputs() + .Execute() + .Should().Pass() + .And.HavePropertyContaining("TRUSTED_PLATFORM_ASSEMBLIES", + Path.Combine(sharedState.DotNet.GreatestVersionSharedFxPath, "System.Private.CoreLib.dll")) + .And.HavePropertyContaining("NATIVE_DLL_SEARCH_DIRECTORIES", + sharedState.DotNet.GreatestVersionSharedFxPath) + .And.NotFindProperty("PLATFORM_RESOURCE_ROOTS") + .And.NotFindProperty("APP_PATHS"); } public class SharedTestState : IDisposable @@ -159,4 +179,23 @@ public void Dispose() } } } + + internal static class RuntimePropertyCommandResultExtensions + { + public static AndConstraint NotFindProperty(this CommandResultAssertions assertion, string propertyName) + { + return assertion.HaveStdOutContaining($"Property '{propertyName}' was not found."); + } + + public static AndConstraint HavePropertyValue(this CommandResultAssertions assertion, string propertyName, string expectedValue) + { + return assertion.HaveStdOutContaining($"AppContext.GetData({propertyName}) = {expectedValue}"); + } + + public static AndConstraint HavePropertyContaining(this CommandResultAssertions assertion, string propertyName, string expectedSubstring) + { + return assertion.HaveStdOutMatching( + $@"AppContext\.GetData\({Regex.Escape(propertyName)}\) = .*{Regex.Escape(expectedSubstring)}"); + } + } } diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs index b857dca79198e5..289d3432db4c93 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs @@ -43,12 +43,34 @@ public static partial class AppContext if (s_dataStore == null) return null; - object? data; lock (s_dataStore) { - s_dataStore.TryGetValue(name, out data); + if (s_dataStore.TryGetValue(name, out object? data)) + return data; } - return data; + +#if !MONO && !NATIVEAOT + if (IsKnownHostProperty(name)) + { + string? value = null; + if (TryGetHostPropertyValue(name, new StringHandleOnStack(ref value))) + { + lock (s_dataStore) + { + if (s_dataStore.TryGetValue(name, out object? existing)) + { + Debug.Assert(existing is string existingValue && existingValue == value); + return existing; + } + + s_dataStore[name] = value; + return value; + } + } + } +#endif + + return null; } /// @@ -205,7 +227,14 @@ internal static unsafe void Setup(char** pNames, char** pValues, int count, Exce s_dataStore = new Dictionary(count); for (int i = 0; i < count; i++) { - s_dataStore.Add(new string(pNames[i]), new string(pValues[i])); + string name = new string(pNames[i]); + + // Avoid retaining a managed string copy of known host properties. + // They will be retrieved if explicitly requested. + if (IsKnownHostProperty(name)) + continue; + + s_dataStore.Add(name, new string(pValues[i])); } } catch (Exception ex)