Repository navigation
Runtime-async miscompiles Stream.ReadAsync(Memory<byte>) tail-call forwarder into NullReferenceException (regression, likely from #128384) #129813
Description
Activity
- addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 24, 2026 dotnet-policy-service commented
on Jun 24, 2026 ContributorMore actionsTagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.CC @jakobbotsch - from the PR that introduced this regression (according to Copilot)
I tried to reproduce it but the repro fails for me:
jakobbotsch@Jakobs-MacBook-Pro rtasync-issue % dotnet build -c Debug Restore complete (0,2s) info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy repro net11.0-maccatalyst maccatalyst-arm64 failed with 2 error(s) (5,4s) → bin/Debug/net11.0-maccatalyst/maccatalyst-arm64/repro.dll ILLink : error IL1032: Root assembly with name 'obj/Debug/net11.0-maccatalyst/maccatalyst-arm64/aot-instances.dll' could not be found. /Users/jakobbotsch/.nuget/packages/microsoft.net.illink.tasks/11.0.0-preview.6.26323.106/build/Microsoft.NET.ILLink.targets(108,5): error NETSDK1144: Optimizing assemblies for size failed. Build failed with 2 error(s) in 6,0sI added
<UseMonoRuntime>false</UseMonoRuntime>and now it reproduces.When I look at the System.Private.CoreLib.dll that is bundled with the app at
bin/Debug/net11.0-maccatalyst/maccatalyst-arm64/repro.app/Contents/MonoBundle/System.Private.CoreLib.dll, I see e.g.[CompilerGenerated] [MethodImpl(MethodImplOptions.Async)] internal static Task <CopyToAsync>g__Core|30_0(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken) { throw null; }
which is what throws the exception. @janvorli points out that this might be a trimming issue, I'll see if I can find some people to involve...
Probably not trimming but rather #125647.
It is certainly caused by #128384. That PR disabled prejitting of async variants (temporarily -- reenabling it is tracked by #129524). Hopefully we'll have that fixed soon, but we can also consider reverting #128384.@kotlarmilos would it make sense to avoid stripping the bodies of methods that crossgen2 failed to compile, since those bodies will be needed at runtime? If not we should at least give some kind of warning or error from crossgen2, or fail at runtime when we try to use interpreter to run something with stripped body.
- addedarea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIand removed
on Jun 25, 2026 dotnet-policy-service commented
on Jun 25, 2026 ContributorMore actionsTagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.- removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 26, 2026 would it make sense to avoid stripping the bodies of methods that crossgen2 failed to compil
crossgen2 does that today: https://github.com/search?q=repo%3Adotnet%2Fruntime%20compiledMethodDefs&type=code . The problem is that crossgen2 assumes that it is ok to strip the IL if we have compiled given (non-generic) method definition once. It is not the case for the async methods.
@jakobbotsch The simplest fix is to treat async methods the same way as generics and suppress IL stripping for them here:
Line 59 in c369fd6
&& factory.OptimizationFlags.CompiledMethodDefs.Contains(_method) Reacted by Jakob Botsch Nielsen1 remaining item
When I try the repro here with
/p:PublishReadyToRunStripILBodies=falsewe no longer crash, but the program does not finish either. It seems we never continue afterawait Task.Yield(). I suspect it is related to calling between interpreter and crossgen2 for async code, but I am struggling to get SOS to work on maccatalyst so I am trying to come up with a separate repro.would it make sense to avoid stripping the bodies of methods that crossgen2 failed to compil
crossgen2 does that today: github.com/search?q=repo%3Adotnet%2Fruntime%20compiledMethodDefs&type=code . The problem is that crossgen2 assumes that it is ok to strip the IL if we have compiled given (non-generic) method definition once. It is not the case for the async methods.
@jakobbotsch The simplest fix is to treat async methods the same way as generics and suppress IL stripping for them here:
runtime/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILNode.cs
Line 59 in c369fd6
&& factory.OptimizationFlags.CompiledMethodDefs.Contains(_method)Thanks! Opened #129884
Could this be backported to P6?
Not sure if there is time, but we can certainly try.
Reacted by Rolf Bjarne Kvinge- linked a pull request that will close this issueAvoid stripping IL of non-async Task-returning methods #129884
on Jun 29, 2026 When I try the repro here with
/p:PublishReadyToRunStripILBodies=falsewe no longer crash, but the program does not finish either. It seems we never continue afterawait Task.Yield(). I suspect it is related to calling between interpreter and crossgen2 for async code, but I am struggling to get SOS to work on maccatalyst so I am trying to come up with a separate repro.So I chatted a bit with @kotlarmilos and it looks like this just has to do with the default
UIKitSynchronizationContextused for maccatalyst. The C#-generatedMain()above blocks the main thread whileTask.Yield()posts back to the main thread, leading to a dead lock.So this should be fixed by #129884.
Reacted by Milos Kotlar- added a commit that references this issue
on Jun 30, 2026 - added a commit that references this issue
on Jun 30, 2026 - added 4 commits that reference this issue
on Jul 1, 2026 - locked and limited conversation to collaborators
on Jul 30, 2026
Description
A
Streamsubclass that overrides only the array-basedReadAsync(byte[], int, int, CancellationToken)throwsNullReferenceExceptionwhen consumed through the base-classStream.ReadAsync(Memory<byte>, CancellationToken)forwarder (e.g. viaStream.CopyToAsync) — but only on the runtime-async-enabled CoreCLR runtime (the Apple mobile runtime packs:maccatalyst/ios/tvos).The NRE originates inside the BCL forwarder itself:
Stream.ReadAsync(Memory<byte>)is a tail-call forwarder:I believe this is a codegen regression from #128384 ("Compile runtime async versions of synchronous task-returning methods"). When the JIT compiles a runtime-async variant from the IL of these synchronous
Task/ValueTask-returning forwarder methods, the tail-call forwarding appears to be miscompiled into an NRE.Reproduction
This reproduces with a plain
net11.0-maccatalystconsole app (no third-party / platform API code — onlyStream,MemoryStream,CopyToAsync). The Apple mobile runtime packs ship a pure-IL (JIT-compiled) CoreLib built with runtime-async enabled.Program.cs:repro.csproj:Build and run the app binary directly:
Expected behavior
CopyToAsynccompletes and printsOK, 6 bytes.Actual behavior
NullReferenceExceptionthrown from insideStream.ReadAsync(Memory<byte>).Does NOT reproduce on desktop
A plain
net11.0desktop console app pinned to the exact same runtime pack version does not reproduce, even self-contained and withDOTNET_ReadyToRun=0,DOTNET_RuntimeAsync=1,DOTNET_TieredCompilation=0. The desktoposx-arm64runtime pack ships a ReadyToRun-precompiled CoreLib and does not have runtime-async codegen enabled; only the Apple-mobile runtime packs do. So this only manifests where the runtime-async JIT path is actually used.Impact
Discovered in dotnet/macios: every
HttpClientrequest throughNSUrlSessionHandlerthat buffers its response (HttpContent.LoadIntoBufferAsync→Stream.CopyToAsyncover the native response stream) throwsNullReferenceException. ~34 networking tests fail. A genuine-asyncoverride ofReadAsync(Memory<byte>)(with a realawait, not a tail-call forwarder) works around it, since genuine async methods are compiled correctly.Configuration
11.0.0-preview.6.26323.106(dotnet/dotnet VMR commita3bc9fe168e83785ed89c54c29aea18b80f3838b)maccatalyst-arm64(CoreCLR), Apple Silicon macOSOther notes
MemoryStream.WriteAsync(ReadOnlyMemory<byte>)(another tail-call forwarder) shows the same NRE in some call paths and is similarly unpatchable from outside the runtime.