Skip to content

NativeAOT: MethodInfo.Invoke on a ref-returning method can return a wrong object (ref return escapes invoke helper's argument storage) #135170

Closed

Description

@EgorBo

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, then ReturnTransform. The array, Span and MethodInvoker paths 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 ReturnTransform inside each helper, before the temporary storage goes out of scope and the GC registrations are removed, and return the resulting object?.

Activity

  1. dotnet-policy-service commented on Oct 3, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
    See info in area-owners.md if you want to be subscribed.

  2. added a commit that references this issue on Oct 4, 2026
    30f8136
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions