From 6b9333980e9c6575144a9c8bd31a0640bf83eb74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:38:34 +0000 Subject: [PATCH 1/3] Initial plan From 65c10698cac05e2daadbfdb4fc62782dfa799089 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:00:41 +0000 Subject: [PATCH 2/3] Fix NativeAOT reflection ref-return argument storage lifetime Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../System/Reflection/DynamicInvokeInfo.cs | 49 ++++++++++--------- .../SmokeTests/Reflection/Reflection.cs | 41 ++++++++++++++++ 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs index e6f2f76ae0faef..adb6faba1d3d40 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs @@ -235,12 +235,12 @@ public unsafe DynamicInvokeInfo(MethodBase method, IntPtr invokeThunk) } else if (argCount > MaxStackAllocArgCount) { - ret = ref InvokeWithManyArguments(methodToCall, ref thisArg, ref ret, + return InvokeWithManyArguments(methodToCall, ref thisArg, ref ret, ref returnObject, parameters, binderBundle, wrapInTargetInvocationException); } else { - ret = ref InvokeWithFewArguments(methodToCall, ref thisArg, ref ret, + return InvokeWithFewArguments(methodToCall, ref thisArg, ref ret, ref returnObject, parameters, binderBundle, wrapInTargetInvocationException); } @@ -296,11 +296,11 @@ public unsafe DynamicInvokeInfo(MethodBase method, IntPtr invokeThunk) } else if (argCount > MaxStackAllocArgCount) { - ret = ref InvokeWithManyArguments(methodToCall, ref thisArg, ref ret, parameters); + return InvokeWithManyArguments(methodToCall, ref thisArg, ref ret, ref returnObject, parameters); } else { - ret = ref InvokeWithFewArguments(methodToCall, ref thisArg, ref ret, parameters); + return InvokeWithFewArguments(methodToCall, ref thisArg, ref ret, ref returnObject, parameters); } return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? @@ -375,7 +375,7 @@ public unsafe DynamicInvokeInfo(MethodBase method, IntPtr invokeThunk) } else { - ret = ref InvokeDirectWithFewArguments(methodToCall, ref thisArg, ref ret, parameters); + return InvokeDirectWithFewArguments(methodToCall, ref thisArg, ref ret, ref returnObject, parameters); } return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? @@ -395,8 +395,8 @@ private void ThrowForArgCountMismatch() throw new TargetParameterCountException(SR.Arg_ParmCnt); } - private unsafe ref byte InvokeWithManyArguments( - IntPtr methodToCall, ref byte thisArg, ref byte ret, + private unsafe object? InvokeWithManyArguments( + IntPtr methodToCall, ref byte thisArg, ref byte ret, ref object? returnObject, object?[] parameters, BinderBundle binderBundle, bool wrapInTargetInvocationException) { int argCount = _argumentCount; @@ -433,20 +433,21 @@ private unsafe ref byte InvokeWithManyArguments( if (needsCopyBack) CopyBackToArray(ref Unsafe.As(ref *pStorage), parameters, shouldCopyBack); + + return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? + ReturnTransform(ref ret, wrapInTargetInvocationException) : returnObject; } finally { RuntimeImports.RhUnregisterForGCReporting(®ByRefStorage); RuntimeImports.RhUnregisterForGCReporting(®ArgStorage); } - - return ref ret; } // This method is equivalent to the one above except that it takes 'Span' instead of 'object[]' // for the parameters and does not require re-throw capability. - private unsafe ref byte InvokeWithManyArguments( - IntPtr methodToCall, ref byte thisArg, ref byte ret, Span parameters) + private unsafe object? InvokeWithManyArguments( + IntPtr methodToCall, ref byte thisArg, ref byte ret, ref object? returnObject, Span parameters) { int argCount = _argumentCount; @@ -475,19 +476,20 @@ private unsafe ref byte InvokeWithManyArguments( if (needsCopyBack) CopyBackToSpan(copyOfParameters, parameters, shouldCopyBack); + + return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? + ReturnTransform(ref ret, wrapInTargetInvocationException: false) : returnObject; } finally { RuntimeImports.RhUnregisterForGCReporting(®ByRefStorage); RuntimeImports.RhUnregisterForGCReporting(®ArgStorage); } - - return ref ret; } // This is a separate method to localize the overhead of stack allocs for 'StackAllocatedByRefs' and 'StackAllocatedByRefs'. - private unsafe ref byte InvokeWithFewArguments( - IntPtr methodToCall, ref byte thisArg, ref byte ret, + private unsafe object? InvokeWithFewArguments( + IntPtr methodToCall, ref byte thisArg, ref byte ret, ref object? returnObject, object?[] parameters, BinderBundle? binderBundle, bool wrapInTargetInvocationException) { Debug.Assert(_argumentCount <= MaxStackAllocArgCount); @@ -514,13 +516,14 @@ private unsafe ref byte InvokeWithFewArguments( if (needsCopyBack) CopyBackToArray(ref copyOfParameters[0], parameters, shouldCopyBack); - return ref ret; + return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? + ReturnTransform(ref ret, wrapInTargetInvocationException) : returnObject; } // This method is equivalent to the one above except that it takes 'Span' instead of 'object[]' // for the parameters and does not require 'BinderBundle' or re-throw capability. - private unsafe ref byte InvokeWithFewArguments( - IntPtr methodToCall, ref byte thisArg, ref byte ret, Span parameters) + private unsafe object? InvokeWithFewArguments( + IntPtr methodToCall, ref byte thisArg, ref byte ret, ref object? returnObject, Span parameters) { Debug.Assert(_argumentCount <= MaxStackAllocArgCount); @@ -539,12 +542,13 @@ private unsafe ref byte InvokeWithFewArguments( if (needsCopyBack) CopyBackToSpan(copyOfParameters, parameters, shouldCopyBack); - return ref ret; + return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? + ReturnTransform(ref ret, wrapInTargetInvocationException: false) : returnObject; } // This method is equivalent to the one above except that it does not require a copy of the args or CopyBack. - private unsafe ref byte InvokeDirectWithFewArguments( - IntPtr methodToCall, ref byte thisArg, ref byte ret, Span parameters) + private unsafe object? InvokeDirectWithFewArguments( + IntPtr methodToCall, ref byte thisArg, ref byte ret, ref object? returnObject, Span parameters) { Debug.Assert(_argumentCount <= MaxStackAllocArgCount); @@ -559,7 +563,8 @@ private unsafe ref byte InvokeDirectWithFewArguments( // No need to call CopyBack here since no copy of the arguments was made. - return ref ret; + return ((_returnTransform & (Transform.Nullable | Transform.Pointer | Transform.FunctionPointer | Transform.ByRef)) != 0) ? + ReturnTransform(ref ret, wrapInTargetInvocationException: false) : returnObject; } private unsafe object? GetCoercedDefaultValue(int index, in ArgumentInfo argumentInfo) diff --git a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs index 7e1423bd1b87b4..2ef6c65b7aabfb 100644 --- a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs +++ b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs @@ -1679,6 +1679,46 @@ public static void TestRefReturnNullable() TestRefReturnInvokeNullable(new BigStruct { X = 987, D = 543 }); } + public static ref object RefReturningArgument(ref object value) => ref value; + + public static ref object RefReturningArgumentFew(ref object value, object a, object b, object c) => ref value; + + public static ref object RefReturningArgumentMany(ref object value, object a, object b, object c, object d) => ref value; + + public static void TestRefReturnArgument() + { + foreach (MethodInfo method in new[] + { + typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgument)), + typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgumentFew)), + typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgumentMany)), + }) + { + MethodInvoker invoker = MethodInvoker.Create(method); + object[] arguments = new object[method.GetParameters().Length]; + Array.Fill(arguments, new object()); + + for (int i = 0; i < 100; i++) + { + object expected = i % 2 == 0 ? new object() : null; + arguments[0] = expected; + Assert.Same(expected, method.Invoke(null, arguments)); + Assert.Same(expected, arguments[0]); + Assert.Same(expected, invoker.Invoke(null, arguments.AsSpan())); + Assert.Same(expected, arguments[0]); + + if (arguments.Length == 1) + { + Assert.Same(expected, invoker.Invoke(null, arguments[0])); + } + else if (arguments.Length == 4) + { + Assert.Same(expected, invoker.Invoke(null, arguments[0], arguments[1], arguments[2], arguments[3])); + } + } + } + } + public static void TestRefReturnNullableNoValue() { TestRefReturnInvokeNullable(default(int?)); @@ -1784,6 +1824,7 @@ public static void Run() Console.WriteLine(nameof(TestByRefReturnInvoke)); TestRefReturnPropertyGetValue(); TestRefReturnMethodInvoke(); + TestRefReturnArgument(); TestRefReturnNullable(); TestRefReturnNullableNoValue(); TestRefReturnOfPointer(); From 5137aeb63e76c3e042d8693028dd3212ccb534d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:24:44 +0000 Subject: [PATCH 3/3] Move ref-return invocation regression into System.Reflection tests Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../MethodInvokerTests.cs | 37 +++++++++++++++++ .../SmokeTests/Reflection/Reflection.cs | 41 ------------------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs index bfe9adedbe695c..d936dd1d7fd58a 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs @@ -19,6 +19,43 @@ public class MethodInvokerTests : MethodCommonTests protected override bool SupportsMissing => false; + [Theory] + [InlineData(nameof(RefReturningArgument))] + [InlineData(nameof(RefReturningArgumentFew))] + [InlineData(nameof(RefReturningArgumentMany))] + public void Invoke_RefReturnAliasesArgument(string methodName) + { + MethodInfo method = typeof(MethodInvokerTests).GetMethod(methodName)!; + MethodInvoker invoker = MethodInvoker.Create(method); + object?[] arguments = new object?[method.GetParameters().Length]; + Array.Fill(arguments, new object()); + + for (int i = 0; i < 100; i++) + { + object? expected = i % 2 == 0 ? new object() : null; + arguments[0] = expected; + Assert.Same(expected, method.Invoke(null, arguments)); + Assert.Same(expected, arguments[0]); + Assert.Same(expected, invoker.Invoke(null, arguments.AsSpan())); + Assert.Same(expected, arguments[0]); + + if (arguments.Length == 1) + { + Assert.Same(expected, invoker.Invoke(null, arguments[0])); + } + else if (arguments.Length == 4) + { + Assert.Same(expected, invoker.Invoke(null, arguments[0], arguments[1], arguments[2], arguments[3])); + } + } + } + + public static ref object? RefReturningArgument(ref object? value) => ref value; + + public static ref object? RefReturningArgumentFew(ref object? value, object? a, object? b, object? c) => ref value; + + public static ref object? RefReturningArgumentMany(ref object? value, object? a, object? b, object? c, object? d) => ref value; + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs index 2ef6c65b7aabfb..7e1423bd1b87b4 100644 --- a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs +++ b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs @@ -1679,46 +1679,6 @@ public static void TestRefReturnNullable() TestRefReturnInvokeNullable(new BigStruct { X = 987, D = 543 }); } - public static ref object RefReturningArgument(ref object value) => ref value; - - public static ref object RefReturningArgumentFew(ref object value, object a, object b, object c) => ref value; - - public static ref object RefReturningArgumentMany(ref object value, object a, object b, object c, object d) => ref value; - - public static void TestRefReturnArgument() - { - foreach (MethodInfo method in new[] - { - typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgument)), - typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgumentFew)), - typeof(TestByRefReturnInvoke).GetMethod(nameof(RefReturningArgumentMany)), - }) - { - MethodInvoker invoker = MethodInvoker.Create(method); - object[] arguments = new object[method.GetParameters().Length]; - Array.Fill(arguments, new object()); - - for (int i = 0; i < 100; i++) - { - object expected = i % 2 == 0 ? new object() : null; - arguments[0] = expected; - Assert.Same(expected, method.Invoke(null, arguments)); - Assert.Same(expected, arguments[0]); - Assert.Same(expected, invoker.Invoke(null, arguments.AsSpan())); - Assert.Same(expected, arguments[0]); - - if (arguments.Length == 1) - { - Assert.Same(expected, invoker.Invoke(null, arguments[0])); - } - else if (arguments.Length == 4) - { - Assert.Same(expected, invoker.Invoke(null, arguments[0], arguments[1], arguments[2], arguments[3])); - } - } - } - } - public static void TestRefReturnNullableNoValue() { TestRefReturnInvokeNullable(default(int?)); @@ -1824,7 +1784,6 @@ public static void Run() Console.WriteLine(nameof(TestByRefReturnInvoke)); TestRefReturnPropertyGetValue(); TestRefReturnMethodInvoke(); - TestRefReturnArgument(); TestRefReturnNullable(); TestRefReturnNullableNoValue(); TestRefReturnOfPointer();