From caa69510af476bd55278e67638143ea4c2710bab Mon Sep 17 00:00:00 2001 From: Cristian Mazo Date: Tue, 23 Jun 2026 13:32:23 -0700 Subject: [PATCH 1/6] Add AssemblyLoadContext.SetAssemblyLocationOverride Adds a static, set-once callback that overrides the value returned by Assembly.Location. The callback is stored in the shared AssemblyLoadContext and applied by RuntimeAssembly.Location on CoreCLR, Mono, and NativeAOT. Updates the System.Runtime.Loader reference assembly and adds tests covering the override, the set-once guard, and null handling. Fix #127097 --- .../src/System/Reflection/RuntimeAssembly.cs | 2 +- .../Reflection/Runtime/General/ThunkedApis.cs | 2 +- .../src/Resources/Strings.resx | 3 + .../Runtime/Loader/AssemblyLoadContext.cs | 37 ++++++++++++ .../ref/System.Runtime.Loader.cs | 1 + .../tests/AssemblyLoadContextTest.cs | 57 +++++++++++++++++++ .../src/System/Reflection/RuntimeAssembly.cs | 2 +- 7 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs index 48ebce7eb9e4cc..ea39af1c950455 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs @@ -519,7 +519,7 @@ public override string Location RuntimeAssembly runtimeAssembly = this; GetLocation(new QCallAssembly(ref runtimeAssembly), new StringHandleOnStack(ref location)); - return location!; + return AssemblyLoadContext.ResolveAssemblyLocation(this, location!); } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs index 2389a9bc0d6a84..da530bc1d1f6d8 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ThunkedApis.cs @@ -63,7 +63,7 @@ public override string Location { get { - return string.Empty; + return System.Runtime.Loader.AssemblyLoadContext.ResolveAssemblyLocation(this, string.Empty); } } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index ac51112ce4dbd1..ea9393351f1a6d 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4052,6 +4052,9 @@ Use of ResourceManager for custom types is disabled. Set the MSBuild Property CustomResourceTypesSupport to true in order to enable it. + + The assembly location override has already been set and can only be set once. + The assembly can not be edited or changed. diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs index 569e1c039d3b18..20221ac3c2d53d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @@ -309,6 +309,43 @@ public static AssemblyName GetAssemblyName(string assemblyPath) return AssemblyName.GetAssemblyName(assemblyPath); } + // Callback that, when set, can override the value returned by Assembly.Location. + private static Func? s_assemblyLocationOverride; + + /// + /// Sets a process-wide callback that overrides the value returned by . + /// + /// The callback can only be set once for the lifetime of the process. + /// + /// A callback that receives an and the location the runtime computed for it, and + /// returns the value that should report. + /// + /// is . + /// The location override has already been set. + public static void SetAssemblyLocationOverride(Func locationOverride) + { + ArgumentNullException.ThrowIfNull(locationOverride); + + if (Interlocked.CompareExchange(ref s_assemblyLocationOverride, locationOverride, null) is not null) + { + throw new InvalidOperationException(SR.InvalidOperation_AssemblyLocationOverrideAlreadySet); + } + } + + // Applies the location override callback (if any) to the location the runtime computed for the assembly. + // Called from each runtime's RuntimeAssembly.Location implementation. + internal static string ResolveAssemblyLocation(Assembly assembly, string originalLocation) + { + Func? locationOverride = s_assemblyLocationOverride; + if (locationOverride is null) + { + return originalLocation; + } + + // A callback that returns null falls back to the original location, since Assembly.Location is non-nullable. + return locationOverride(assembly, originalLocation) ?? originalLocation; + } + // Custom AssemblyLoadContext implementations can override this // method to perform custom processing and use one of the protected // helpers above to load the assembly. diff --git a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs index 8f7555ac5b599d..3ad39352c54e5f 100644 --- a/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs +++ b/src/libraries/System.Runtime.Loader/ref/System.Runtime.Loader.cs @@ -87,6 +87,7 @@ public event System.Action? Unloading public System.Reflection.Assembly LoadFromStream(System.IO.Stream assembly, System.IO.Stream? assemblySymbols) { throw null; } protected virtual System.IntPtr LoadUnmanagedDll(string unmanagedDllName) { throw null; } protected System.IntPtr LoadUnmanagedDllFromPath(string unmanagedDllPath) { throw null; } + public static void SetAssemblyLocationOverride(System.Func locationOverride) { } public void SetProfileOptimizationRoot(string directoryPath) { } public void StartProfileOptimization(string? profile) { } public override string ToString() { throw null; } diff --git a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs index 19604e0455aa74..43f71503442561 100644 --- a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs @@ -418,6 +418,63 @@ public static void LoadFromAssemblyPath_DefaultAlc_NonTpaAssembly_MvidMismatch() }).Dispose(); } + [Fact] + public static void SetAssemblyLocationOverride_NullArgument_Throws() + { + // The null check happens before any global state is mutated, so this is safe to run in-process. + AssertExtensions.Throws("locationOverride", () => AssemblyLoadContext.SetAssemblyLocationOverride(null)); + } + + [ConditionalFact(typeof(AssemblyLoadContextTest), nameof(IsRemoteExecutorSupportedAndAssemblyLoadingSupported))] + public static void SetAssemblyLocationOverride_OverridesLocationForStreamLoadedAssembly() + { + // The override is process-wide and set-once, so it must run in its own process. + RemoteExecutor.Invoke(static () => + { + // Override the location only for assemblies the runtime reports no location for (e.g. loaded + // from a stream / memory). Embed the assembly name so we also verify the Assembly argument. + AssemblyLoadContext.SetAssemblyLocationOverride( + static (assembly, location) => string.IsNullOrEmpty(location) ? $"/overridden/{assembly.GetName().Name}.dll" : location); + + string asmPath = ExtractEmbeddedAssembly("System.Runtime.Loader.Tests.AssemblyVersion1"); + try + { + // Loading from a stream means the runtime has no location, so the override kicks in. + var streamAlc = new AssemblyLoadContext("LocationOverride_Stream", isCollectible: true); + Assembly streamLoaded; + using (FileStream fs = File.OpenRead(asmPath)) + { + streamLoaded = streamAlc.LoadFromStream(fs); + } + Assert.Equal($"/overridden/{streamLoaded.GetName().Name}.dll", streamLoaded.Location); + + // The same assembly loaded from a path (into a separate context) has a real location, + // so the callback leaves it untouched. + var pathAlc = new AssemblyLoadContext("LocationOverride_Path", isCollectible: true); + Assembly fileLoaded = pathAlc.LoadFromAssemblyPath(asmPath); + Assert.False(string.IsNullOrEmpty(fileLoaded.Location)); + Assert.DoesNotContain("/overridden/", fileLoaded.Location); + } + finally + { + try { File.Delete(asmPath); } catch { } + } + }).Dispose(); + } + + [ConditionalFact(typeof(AssemblyLoadContextTest), nameof(IsRemoteExecutorSupportedAndAssemblyLoadingSupported))] + public static void SetAssemblyLocationOverride_CalledTwice_Throws() + { + RemoteExecutor.Invoke(static () => + { + AssemblyLoadContext.SetAssemblyLocationOverride(static (assembly, location) => location); + Assert.Throws( + () => AssemblyLoadContext.SetAssemblyLocationOverride(static (assembly, location) => location)); + }).Dispose(); + } + + private static bool IsRemoteExecutorSupportedAndAssemblyLoadingSupported => RemoteExecutor.IsSupported && PlatformDetection.IsAssemblyLoadingSupported; + private static bool IsRemoteExecutorSupportedAndCoreCLR => RemoteExecutor.IsSupported && PlatformDetection.IsAssemblyLoadingSupported && PlatformDetection.IsCoreCLR; private static string ExtractEmbeddedAssembly(string name) diff --git a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs index 0e2b66efc1a85c..cc784fa524fcf7 100644 --- a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs +++ b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs @@ -131,7 +131,7 @@ public override Module ManifestModule public override string ImageRuntimeVersion => GetInfo(AssemblyInfoKind.ImageRuntimeVersion)!; - public override string Location => GetInfo(AssemblyInfoKind.Location)!; + public override string Location => AssemblyLoadContext.ResolveAssemblyLocation(this, GetInfo(AssemblyInfoKind.Location)!); // TODO: consider a dedicated icall instead public override bool IsCollectible => AssemblyLoadContext.GetLoadContext((Assembly)this)!.IsCollectible; From 8fd076f1c490f6b78de71da26720de4d11d5d829 Mon Sep 17 00:00:00 2001 From: Cristian Mazo Date: Wed, 24 Jun 2026 12:45:25 -0700 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Theodore Tsirpanis --- .../src/System/Runtime/Loader/AssemblyLoadContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs index 20221ac3c2d53d..74f49e458b9e68 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @@ -342,8 +342,7 @@ internal static string ResolveAssemblyLocation(Assembly assembly, string origina return originalLocation; } - // A callback that returns null falls back to the original location, since Assembly.Location is non-nullable. - return locationOverride(assembly, originalLocation) ?? originalLocation; + return locationOverride(assembly, originalLocation); } // Custom AssemblyLoadContext implementations can override this From 641781fde4ed1db31fc96261f24f481edd3bdf09 Mon Sep 17 00:00:00 2001 From: Cristian Mazo Date: Wed, 24 Jun 2026 15:28:56 -0700 Subject: [PATCH 3/6] Added remark to SEtAssemblyLocationOverride --- .../src/System/Runtime/Loader/AssemblyLoadContext.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs index 74f49e458b9e68..5402ec00cf81ba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @@ -315,7 +315,10 @@ public static AssemblyName GetAssemblyName(string assemblyPath) /// /// Sets a process-wide callback that overrides the value returned by . /// - /// The callback can only be set once for the lifetime of the process. + /// + /// The callback can only be set once for the lifetime of the process. The callback should not + /// call on the provided assembly to avoid recursion. + /// /// /// A callback that receives an and the location the runtime computed for it, and /// returns the value that should report. @@ -342,7 +345,8 @@ internal static string ResolveAssemblyLocation(Assembly assembly, string origina return originalLocation; } - return locationOverride(assembly, originalLocation); + // A callback that returns null falls back to the original location, since Assembly.Location is non-nullable. + return locationOverride(assembly, originalLocation) ?? originalLocation; } // Custom AssemblyLoadContext implementations can override this From 7394fbd8e82f456835ae9e97bef67bdf2aa1ff71 Mon Sep 17 00:00:00 2001 From: Cristian Mazo Date: Wed, 24 Jun 2026 18:54:58 -0700 Subject: [PATCH 4/6] Applied suggestions --- .../src/System/Runtime/Loader/AssemblyLoadContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs index 5402ec00cf81ba..8064d2a46a7bd1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @@ -345,8 +345,7 @@ internal static string ResolveAssemblyLocation(Assembly assembly, string origina return originalLocation; } - // A callback that returns null falls back to the original location, since Assembly.Location is non-nullable. - return locationOverride(assembly, originalLocation) ?? originalLocation; + return locationOverride(assembly, originalLocation); } // Custom AssemblyLoadContext implementations can override this From 5d8e150833635a02d7300f3c212485b754cbe96e Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Thu, 25 Jun 2026 16:03:42 -0700 Subject: [PATCH 5/6] Update src/libraries/System.Private.CoreLib/src/Resources/Strings.resx --- src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index ea9393351f1a6d..794a9797656512 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4053,7 +4053,7 @@ Use of ResourceManager for custom types is disabled. Set the MSBuild Property CustomResourceTypesSupport to true in order to enable it. - The assembly location override has already been set and can only be set once. + Attempt to update previously set assembly location override. The assembly can not be edited or changed. From d758591f75d88844176ce0b3d15167e422fa07d7 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Thu, 25 Jun 2026 16:15:45 -0700 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 794a9797656512..d8d8ba8e142ffd 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4053,7 +4053,7 @@ Use of ResourceManager for custom types is disabled. Set the MSBuild Property CustomResourceTypesSupport to true in order to enable it. - Attempt to update previously set assembly location override. + The assembly location override has already been set. The assembly can not be edited or changed.