Skip to content

Implement new ReadyToRunFixup that ensures that each assemblyref for the corresponding assembly is loaded via the correct ALC#126620

Draft
jkoritzinsky wants to merge 1 commit intodotnet:mainfrom
jkoritzinsky:r2r-alc-fixup
Draft

Implement new ReadyToRunFixup that ensures that each assemblyref for the corresponding assembly is loaded via the correct ALC#126620
jkoritzinsky wants to merge 1 commit intodotnet:mainfrom
jkoritzinsky:r2r-alc-fixup

Conversation

@jkoritzinsky
Copy link
Copy Markdown
Member

This is needed to help correctly support cross-module generics with ready to run info loaded in non-default ALCs.

…the corresponding assembly is loaded via the correct ALC
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a new ReadyToRun fixup to ensure assembly references are loaded through the correct AssemblyLoadContext, improving support for cross-module generics when R2R images are loaded into non-default ALCs.

Changes:

  • Adds a new R2R fixup kind (AssemblyRefSimpleNameLoad) and bumps the R2R minor version.
  • Emits the fixup from crossgen2 behind a new --force-alc-compatibility flag and adds an ILCompiler signature node for it.
  • Handles the fixup at runtime by calling into a new CoreLib [UnmanagedCallersOnly] hook on AssemblyLoadContext.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/coreclr/vm/metasig.h Adds a new metasig for the unmanaged-callers-only hook signature.
src/coreclr/vm/jitinterface.cpp Processes the new fixup kind and calls into CoreLib to register the hook.
src/coreclr/vm/corelib.h Adds a CoreLib binder entry for the new hook method.
src/coreclr/vm/assembly.cpp Removes outdated comment about lazy friend assembly info loading.
src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs Adds the new fixup kind to tooling constants.
src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs Bumps R2R header minor version.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj Includes new signature node source file in the build.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs Introduces an optimization flag and roots the eager import for the fixup.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunAssemblyRefSimpleNameLoadSignature.cs New signature node that emits the fixup into the image.
src/coreclr/tools/aot/crossgen2/Properties/Resources.resx Adds help text for the new crossgen2 option.
src/coreclr/tools/aot/crossgen2/Program.cs Wires the new flag into node factory optimization flags.
src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs Declares the new --force-alc-compatibility option.
src/coreclr/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.CoreCLR.cs Adds the [UnmanagedCallersOnly] hook that subscribes to assembly load events and forces loads in a specific ALC.
src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h Bumps R2R header minor version for NativeAOT headers.
src/coreclr/inc/readytorun.h Bumps R2R minor version and adds native fixup enum value + comment.

DEFINE_METHOD(ASSEMBLYLOADCONTEXT, STOP_ASSEMBLY_LOAD, StopAssemblyLoad, SM_PtrGuid_PtrException_RetVoid)
DEFINE_METHOD(ASSEMBLYLOADCONTEXT, INITIALIZE_DEFAULT_CONTEXT, InitializeDefaultContext, SM_PtrException_RetVoid)
#ifdef FEATURE_READYTORUN
DEFINE_METHOD(ASSEMBLYLOADCONTEXT, REGISTER_SIMPLE_NAME_LOAD_HOOK_FOR_ASSEMBLY, RegisterSimpleNameLoadHookForAssembly, SM_PtrAssembly_PtrException_RetVoid)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REGISTER_SIMPLE_NAME_LOAD_HOOK_FOR_ASSEMBLY is bound with SM_PtrAssembly_PtrException_RetVoid, but the managed UnmanagedCallersOnly method signature is (Assembly*, sbyte** simpleNames, int count, Exception*). This metasig mismatch will cause the native callsite to pass the wrong arguments. Please use the newly added SM_PtrAssembly_PtrPtrSByte_Int_PtrException_RetVoid metasig here (or otherwise align the binder signature with the managed method).

Suggested change
DEFINE_METHOD(ASSEMBLYLOADCONTEXT, REGISTER_SIMPLE_NAME_LOAD_HOOK_FOR_ASSEMBLY, RegisterSimpleNameLoadHookForAssembly, SM_PtrAssembly_PtrException_RetVoid)
DEFINE_METHOD(ASSEMBLYLOADCONTEXT, REGISTER_SIMPLE_NAME_LOAD_HOOK_FOR_ASSEMBLY, RegisterSimpleNameLoadHookForAssembly, SM_PtrAssembly_PtrPtrSByte_Int_PtrException_RetVoid)

Copilot uses AI. Check for mistakes.

UnmanagedCallersOnlyCaller registerSimpleNameLoadHookForAssembly{METHOD__ASSEMBLYLOADCONTEXT__REGISTER_SIMPLE_NAME_LOAD_HOOK_FOR_ASSEMBLY};

registerSimpleNameLoadHookForAssembly.InvokeThrowing(&assemblyObject, &simpleNames[0], simpleNames.GetCount());
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&simpleNames[0] is passed to InvokeThrowing even when the module has zero AssemblyRefs, which would dereference an empty SArray and pass an invalid pointer into managed code. Handle the empty case explicitly (e.g., pass nullptr when GetCount()==0).

Suggested change
registerSimpleNameLoadHookForAssembly.InvokeThrowing(&assemblyObject, &simpleNames[0], simpleNames.GetCount());
LPCSTR* pSimpleNames = simpleNames.GetCount() == 0 ? nullptr : &simpleNames[0];
registerSimpleNameLoadHookForAssembly.InvokeThrowing(&assemblyObject, pSimpleNames, simpleNames.GetCount());

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +152
public Option<bool> ForceAssemblyLoadContextCompatibility { get; } =
new("--force-alc-compatibility") { Description = SR.ForceAssemblyLoadContextCompatibility };
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option is declared but not added to the command's Options collection in the constructor (it's missing from the Options.Add(...) list), so the switch can't be set from the command line and parsing may reject it. Add ForceAssemblyLoadContextCompatibility to the Options.Add(...) list alongside the other flags.

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +177
// For any names that remain, register a callback to the AppDomain.AssemblyLoad event to load into the provided Assembly's ALC as well.
if (simpleNames.Count > 0)
{
AppDomain.CurrentDomain.AssemblyLoad += SimpleNameLoadHook;
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscribing to AppDomain.CurrentDomain.AssemblyLoad with a closure that captures context and simpleNames can keep the AssemblyLoadContext/assembly rooted indefinitely if the remaining simple names are never loaded, which can prevent collectible ALC unload. Consider using a weak reference to the ALC (and/or a bounded lifetime / explicit unregistration path) so this hook can’t leak indefinitely.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +191
void SimpleNameLoadHook(object? sender, AssemblyLoadEventArgs args)
{
string loadedSimpleName = args.LoadedAssembly.GetName().Name!;
if (simpleNames.Contains(loadedSimpleName))
{
context.LoadFromAssemblyName(args.LoadedAssembly.GetName());
simpleNames.Remove(loadedSimpleName);

if (simpleNames.Count == 0)
{
AppDomain.CurrentDomain.AssemblyLoad -= SimpleNameLoadHook;
}
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleNameLoadHook mutates the captured HashSet (simpleNames.Remove(...)) without any synchronization. AppDomain.AssemblyLoad can be raised concurrently, which can lead to races or HashSet corruption/throws. Protect access with a lock, or use a thread-safe set / immutable pattern (e.g., ConcurrentDictionary or Interlocked-based removal) and ensure unsubscription is thread-safe.

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +185
if (simpleNames.Contains(loadedSimpleName))
{
context.LoadFromAssemblyName(args.LoadedAssembly.GetName());
simpleNames.Remove(loadedSimpleName);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SimpleNameLoadHook, the name is removed from simpleNames only after calling context.LoadFromAssemblyName(...). Loading an assembly typically raises AppDomain.AssemblyLoad synchronously, so this handler can re-enter before the removal and attempt to load the same name again (potential recursion/re-entrancy). Remove/mark the name before calling LoadFromAssemblyName (or otherwise guard against re-entrancy) to ensure the handler is idempotent.

Suggested change
if (simpleNames.Contains(loadedSimpleName))
{
context.LoadFromAssemblyName(args.LoadedAssembly.GetName());
simpleNames.Remove(loadedSimpleName);
if (simpleNames.Remove(loadedSimpleName))
{
try
{
context.LoadFromAssemblyName(args.LoadedAssembly.GetName());
}
catch
{
simpleNames.Add(loadedSimpleName);
throw;
}

Copilot uses AI. Check for mistakes.
HashSet<string> simpleNames = [];
for (int i = 0; i < numAsmRefs; i++)
{
simpleNames.Add(new string(pAsmRefSimpleNames[i]));
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assembly reference names in metadata are UTF-8, but new string(sbyte*) interprets the bytes using the system ANSI code page. This can mis-handle non-ASCII assembly simple names. Use an explicit UTF-8 decode (e.g., Marshal.PtrToStringUTF8 or Encoding.UTF8.GetString on a span) for correctness.

Suggested change
simpleNames.Add(new string(pAsmRefSimpleNames[i]));
simpleNames.Add(Marshal.PtrToStringUTF8((IntPtr)pAsmRefSimpleNames[i])!);

Copilot uses AI. Check for mistakes.
@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 8, 2026

What is the problem that this is fixing? It looks like a gross hack.

@jkoritzinsky
Copy link
Copy Markdown
Member Author

@davidwrighton mentioned that this was needed for some correctness case with cross-module generics.

One of the reasons this is in draft is for me to figure out a better way to articulate the specific reason we need this and name the fixup and command line option better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants