Repository navigation
Description
Description
DynamicInvokeInfo.InvokeWithManyArguments and InvokeWithFewArguments copy reference-type arguments into stack storage (GC-registered in the many-args case) and pass byrefs to those slots. If the target returns ref to one of its ref arguments, the returned byref points into that storage.
The helpers return that byref, and only the outer Invoke dereferences it in ReturnTransform. By then the helper's frame is gone and its GC registrations are removed. The result can be stale or wrong, especially under a compacting GC.
Reproduction Steps
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
public static class Program {
private static bool s_stop;
[MethodImpl(MethodImplOptions.NoInlining)]
public static ref object Echo(
ref object value, object a, object b, object c, object d) {
return ref value;
}
public static int Main() {
Console.WriteLine(RuntimeInformation.FrameworkDescription);
MethodInfo method = typeof(Program).GetMethod(nameof(Echo))!;
object[] arguments = {
new object(), new object(), new object(), new object(), new object()
};
Thread collector = new Thread(static () => {
while (!Volatile.Read(ref s_stop)) {
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
Thread.Yield();
}
});
collector.Start();
try {
for (int i = 0; i < 100_000; i++) {
object expected = new object();
arguments[0] = expected;
object? actual = method.Invoke(null, arguments);
if (!ReferenceEquals(expected, actual)) {
Console.WriteLine($"Incorrect reference at iteration {i}");
return 1;
}
}
Console.WriteLine("All references matched.");
return 0;
}
finally {
Volatile.Write(ref s_stop, true);
collector.Join();
}
}
}Publish with PublishAot=true (dotnet publish -c Release -r win-x64) and run. It is timing-dependent, so rerun if needed.
Expected behavior
Every call returns the exact object passed as arguments[0]; exit code 0.
Actual behavior
Invoke returns a different object (first observed at iteration 5523 in my run); exit code 1.
Regression?
Unknown.
Known Workarounds
Avoid reflection invocation of methods that return ref to a reference-type ref parameter.
Configuration
- NativeAOT, Windows x64, .NET 11.0.0-rc.1.26425.128
- Likely affects all NativeAOT targets, since the code is shared.
Other information
- Affected code is in
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:InvokeWithManyArguments,InvokeWithFewArguments,InvokeDirectWithFewArguments, thenReturnTransform. The array,SpanandMethodInvokerpaths are all affected. - The generated invoke thunk deliberately doesn't copy byref returns (
DynamicInvokeMethodThunk.cs, "Store the return value unless it is a byref"). - Suggested fix: call
ReturnTransforminside each helper, before the temporary storage goes out of scope and the GC registrations are removed, and return the resultingobject?.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Oct 3, 2026
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.
Fix NativeAOT reflection ref-return storage lifetime (#135172)