Repository navigation
.NET 11 regression: Unicode char arrays lose pinning during built-in LPArray marshalling #134577
Description
Description
A char[] parameter on a CharSet.Unicode unmanaged-function-pointer delegate is no longer pinned. The managed and native elements are both UTF-16 code units, but .NET 11 passes a temporary buffer. Writes that were visible to managed code on .NET 10 are consequently lost when the parameter has no explicit Out attribute.
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.
Repro.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>Program.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
internal static unsafe class Program
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private delegate int ReplaceFirst(char[] values);
private static int Main()
{
Console.WriteLine(RuntimeInformation.FrameworkDescription);
var replace = Marshal.GetDelegateForFunctionPointer<ReplaceFirst>(
(nint)(delegate* unmanaged[Cdecl]<char*, int>)&WriteFirst);
char[] values = ['A'];
int written = replace(values);
Console.WriteLine($"Native side wrote: {(char)written}; managed element: {values[0]} (expected X)");
bool passed = written == 'X' && values[0] == 'X';
Console.WriteLine(passed ? "PASS" : "BUG: Unicode char array lost implicit copyback.");
return passed ? 0 : 1;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static int WriteFirst(char* values)
{
values[0] = 'X';
return values[0];
}
}Build once with dotnet build Repro.csproj. Run the generated apphost against the two runtimes:
$env:DOTNET_ROLL_FORWARD_TO_PRERELEASE = '1'
$env:DOTNET_ROLL_FORWARD = 'LatestPatch'
& .\bin\Debug\net10.0\Repro.exe
$env:DOTNET_ROLL_FORWARD = 'LatestMajor'
& .\bin\Debug\net10.0\Repro.exeThese 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 pinning for direct managed-to-unmanaged Unicode char-array marshalling. The native callee's write of X should update the original managed array.
.NET 10 output:
.NET 10.0.5
Native side wrote: X; managed element: X (expected X)
PASS
Actual behavior
.NET 11 output from the same compiled program:
.NET 11.0.0-rc.1.26420.103
Native side wrote: X; managed element: A (expected X)
BUG: Unicode char array lost implicit copyback.
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, targetingnet10.0. - The repository runtime was not rebuilt for this comparison. Linux, ARM64, x86, Mono, NativeAOT, and source-generated marshalling were not tested.
Implementation observations
A separate address-comparison probe pinned the managed char array with fixed and compared its address with the pointer observed by an UnmanagedCallersOnly callee. They were equal on .NET 10.0.5 and different on .NET 11.0.0-rc.1.26420.103.
The new CanMarshalViaPinning predicate uses the managed type's IsBlittable result rather than accounting for the selected UTF-16 representation. Unicode char arrays need no element conversion; ANSI char arrays must remain excluded from this pinning path. The adjacent implementation explicitly documents preserving implicit in/out behavior for compatibility.
This report is about direct built-in interop pinning, not a claim that arrays default to In/Out across all interop mechanisms. Explicit [In, Out] expresses writable-array intent, but should not be required to recover previously supported pinning. See interop best practices.
Related but distinct: #134174 and #134284 address pointer/function-pointer arrays. Their pointer-specific changes do not restore Unicode char-array pinning. The enum case is also being tracked separately because its eligibility does not depend on CharSet.
Note
This report was prepared with GitHub Copilot assistance; the included runtime outputs were reproduced locally.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Sep 24, 2026
Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.
Fix pointer array copying and various array pinning regressions in Co…
[release/11.0] Fix many array marshalling regressions in CoreCLR (#13…