You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
.NET 11 regression: SAFEARRAY VARIANT copy-back coerces replacement values through existing byref elements #134579
During reverse [In, Out] SAFEARRAY(VARIANT) marshalling, replacing an object[] element with a different managed type now coerces the replacement to the existing native byref element's type. A VT_BYREF|VT_I4 element referring to integer 4 becomes integer 7 through that reference after managed code assigns string "7". On .NET 10, the array slot is replaced with VT_BSTR and the referenced integer remains 4.
Reproduction steps
Create an empty directory outside the runtime repository and save the following as Repro.csproj and Program.cs. No external native library or NuGet package is required; the native-facing entry point is provided through UnmanagedCallersOnly or Windows built-in COM interop.
// Licensed to the .NET Foundation under one or more agreements.// The .NET Foundation licenses this file to you under the MIT license.// When a reverse [In, Out] SAFEARRAY copy-back runs with valid native contents, the old array// converter used MarshalCommonOleRefVariantForObject and, when the managed value did not fit the// existing element type, replaced the array slot outright. The replacement// VariantArrayElementMarshaler calls ObjectMarshaler.ConvertToNative, which reaches// MarshalOleRefVariantForObject (src/coreclr/vm/olevariant.cpp) and instead coerces the value to the// existing VARTYPE and writes it through the caller's VT_BYREF pointer.usingSystem;usingSystem.Runtime.InteropServices;internalstaticunsafeclassProgram{privateconstushortVT_I4=3;privateconstushortVT_BSTR=8;privateconstushortVT_VARIANT=12;privateconstushortVT_BYREF=0x4000;[UnmanagedFunctionPointer(CallingConvention.Cdecl)]privatedelegatevoidModify([In,Out,MarshalAs(UnmanagedType.SafeArray,SafeArraySubType=VarEnum.VT_VARIANT)]object[]values);privatestaticintMain(){Console.WriteLine(RuntimeInformation.FrameworkDescription);if(!OperatingSystem.IsWindows()){Console.WriteLine("Requires Windows built-in COM interop.");return2;}intnative=4;SafeArray*array=SafeArrayCreateVector(VT_VARIANT,0,1);if(arrayisnull){Console.WriteLine("SafeArrayCreateVector failed.");return2;}// The caller's element already holds a pointer to the native integer.Variant*elements=(Variant*)array->Data;elements[0].Vt=VT_BYREF|VT_I4;elements[0].Data1=(nint)(&native);varcallback=newModify(ReplaceWithString);nintentryPoint=Marshal.GetFunctionPointerForDelegate(callback);try{((delegate* unmanaged[Cdecl]<SafeArray*,void>)entryPoint)(array);Console.WriteLine($"Element VARIANT type: 0x{elements[0].Vt:X4} (expected 0x{VT_BSTR:X4} VT_BSTR)");Console.WriteLine($"Referenced native integer: {native} (expected 4, unchanged)");boolpassed=native==4&&elements[0].Vt==VT_BSTR;Console.WriteLine(passed?"PASS":"BUG: the string was coerced into the existing VT_BYREF|VT_I4 element instead of replacing the slot.");returnpassed?0:1;}catch(Exceptionex){Console.WriteLine($"BUG: replacing the element threw instead of replacing the slot: {ex}");return1;}finally{GC.KeepAlive(callback);if(elements[0].Vt==VT_BSTR){Marshal.FreeBSTR(elements[0].Data1);elements[0]=default;}SafeArrayDestroy(array);}}privatestaticvoidReplaceWithString(object[]values){Console.WriteLine($"Managed method saw: [{string.Join(", ",values)}]");values[0]="7";}[DllImport("oleaut32.dll")]privatestaticexternSafeArray*SafeArrayCreateVector(ushortvt,intlowerBound,uintcount);[DllImport("oleaut32.dll")]privatestaticexternintSafeArrayDestroy(SafeArray*array);[StructLayout(LayoutKind.Sequential)]privatestructVariant{publicushortVt;publicushortReserved1;publicushortReserved2;publicushortReserved3;publicnintData1;publicnintData2;}[StructLayout(LayoutKind.Sequential)]privatestructSafeArray{publicushortDimensions;publicushortFeatures;publicuintElementSize;publicuintLocks;publicnintData;publicuintElements;publicintLowerBound;}}
Build once with dotnet build Repro.csproj. Run the generated apphost against the two runtimes:
These commands assume the apphost resolves to a .NET installation containing the tested .NET 10 and .NET 11 runtimes, with no newer major version. If needed, set DOTNET_ROOT to that installation. Check the framework version printed by the program rather than assuming which runtime was selected. Remove the temporary roll-forward environment variables after the comparison.
Expected behavior
Preserve the existing array-element replacement semantics: assign a VT_BSTR containing "7" to the array slot and leave the old referenced integer unchanged. Same-type byref updates and different-type replacements are distinct cases.
.NET 11.0.0-rc.1.26420.103
Managed method saw: [4]
Element VARIANT type: 0x4003 (expected 0x0008 VT_BSTR)
Referenced native integer: 7 (expected 4, unchanged)
BUG: the string was coerced into the existing VT_BYREF|VT_I4 element instead of replacing the slot.
Regression
Confirmed by running the same net10.0-targeted program on .NET 10.0.5 and .NET 11.0.0-rc.1.26420.103.
The exact introducing commit has not been established by a runtime build-and-bisect. The implementation observations below are based on source inspection.
Configuration
Windows x64, CoreCLR, built-in runtime marshalling.
Passing runtime: .NET 10.0.5.
Failing runtime: .NET 11.0.0-rc.1.26420.103.
Compiled with SDK 11.0.100-rc.1.26420.103, targeting net10.0.
The repository runtime was not rebuilt for this comparison. Linux, ARM64, x86, Mono, NativeAOT, and source-generated marshalling were not tested.
Implementation observations
The old MarshalVariantArrayComToOle implementation deliberately used MarshalCommonOleRefVariantForObject first and replaced the slot if it could not preserve the managed value's type. Its compatibility comment explicitly says not to use MarshalOleRefVariantForObject because that would coerce based on the existing native VARTYPE.
The new VariantArrayElementMarshaler calls ObjectMarshaler.ConvertToNative for valid native contents, reaching the coercing byref path instead. See the pre-migration converter for the explicit compatibility logic.
The array's base type is VT_VARIANT; VT_BYREF is set on an individual VARIANT element, not on the SAFEARRAY's base type. A separate control using SafeArrayPutElement successfully inserted and preserved VT_BYREF|VT_I4 with the same referenced pointer. This is not an attempt to create a SAFEARRAY whose base VARTYPE includes VT_BYREF.
This is separate from the static SAFEARRAY issue: here native contents are recognized as valid, but the selected element conversion changes the replacement/coercion semantics.
Note
This report was prepared with GitHub Copilot assistance; the included runtime outputs were reproduced locally.
Description
During reverse [In, Out] SAFEARRAY(VARIANT) marshalling, replacing an object[] element with a different managed type now coerces the replacement to the existing native byref element's type. A VT_BYREF|VT_I4 element referring to integer 4 becomes integer 7 through that reference after managed code assigns string "7". On .NET 10, the array slot is replaced with VT_BSTR and the referenced integer remains 4.
Reproduction steps
Create an empty directory outside the runtime repository and save the following as
Repro.csprojandProgram.cs. No external native library or NuGet package is required; the native-facing entry point is provided throughUnmanagedCallersOnlyor Windows built-in COM interop.Repro.csproj
Program.cs
Build once with
dotnet build Repro.csproj. Run the generated apphost against the two runtimes:These commands assume the apphost resolves to a .NET installation containing the tested .NET 10 and .NET 11 runtimes, with no newer major version. If needed, set
DOTNET_ROOTto that installation. Check the framework version printed by the program rather than assuming which runtime was selected. Remove the temporary roll-forward environment variables after the comparison.Expected behavior
Preserve the existing array-element replacement semantics: assign a VT_BSTR containing "7" to the array slot and leave the old referenced integer unchanged. Same-type byref updates and different-type replacements are distinct cases.
.NET 10 output:
Actual behavior
.NET 11 output from the same compiled program:
Regression
Confirmed by running the same
net10.0-targeted program on .NET 10.0.5 and .NET 11.0.0-rc.1.26420.103.The exact introducing commit has not been established by a runtime build-and-bisect. The implementation observations below are based on source inspection.
Configuration
.NET 10.0.5..NET 11.0.0-rc.1.26420.103.11.0.100-rc.1.26420.103, targetingnet10.0.Implementation observations
The old MarshalVariantArrayComToOle implementation deliberately used MarshalCommonOleRefVariantForObject first and replaced the slot if it could not preserve the managed value's type. Its compatibility comment explicitly says not to use MarshalOleRefVariantForObject because that would coerce based on the existing native VARTYPE.
The new VariantArrayElementMarshaler calls ObjectMarshaler.ConvertToNative for valid native contents, reaching the coercing byref path instead. See the pre-migration converter for the explicit compatibility logic.
The array's base type is VT_VARIANT; VT_BYREF is set on an individual VARIANT element, not on the SAFEARRAY's base type. A separate control using SafeArrayPutElement successfully inserted and preserved VT_BYREF|VT_I4 with the same referenced pointer. This is not an attempt to create a SAFEARRAY whose base VARTYPE includes VT_BYREF.
This is separate from the static SAFEARRAY issue: here native contents are recognized as valid, but the selected element conversion changes the replacement/coercion semantics.
Note
This report was prepared with GitHub Copilot assistance; the included runtime outputs were reproduced locally.