Repository navigation
CoreCLR interpreter clears byref and byref-like locals when an await does not suspend #136171
Description
We welcome bug reports! Please see our contribution guidelines for more information on writing a good bug report. This template will help us gather the information we need to start the triage process.
Description
The CoreCLR interpreter clears byref and byref-like locals after an await even when the awaited task is already complete and the method never suspends.
The existing Runtime_136000.TestEntryPoint regression demonstrates this: its first assertion expects an initialized ref-struct payload to survive AsyncHelpers.Await(Task.CompletedTask), but the payload becomes default. Actual suspension is supposed to default the payload; synchronous completion is not.
This fails in all four browser-Wasm Priority 1 configurations in runtime-coreclr outerloop build 1641441: interpreter, interpreter/no-tiering, R2R, and R2R/no-tiering. The dynamically emitted method executes through the interpreter in each configuration; selecting R2R does not precompile Reflection.Emit bodies.
The unchanged original regression was also reproduced on a freshly built CoreCLR/browser-Wasm Checked runtime from source d260b5c598ab285046e217c012f6d7126947b06a.
Reproduction Steps
Use the unchanged regression at source d260b5c598ab285046e217c012f6d7126947b06a, which includes #136085. Build the browser-Wasm runtime, regression, and test layout:
./build.sh clr+libs+host -os browser -c Release -rc Checked
src/tests/build.sh -os browser -arch wasm Checked -skipnative -priority1 \
-test:JIT/Regression_ro_2/Regression_ro_2.csproj \
/p:TasksConfiguration=ReleaseRun the existing regression under the browser-Wasm CoreCLR host:
root="$PWD"
export CORE_ROOT="$root/artifacts/tests/coreclr/browser.wasm.Checked/Tests/Core_Root"
mkdir -p artifacts/tmp/runtime136000-repro
cd artifacts/tmp/runtime136000-repro
DOTNET_ReadyToRun=0 node --experimental-wasm-exnref --stack-size=8192 \
"$CORE_ROOT/corerun.js" -c "$CORE_ROOT" \
"$root/artifacts/tests/coreclr/browser.wasm.Checked/JIT/Regression_ro_2/Regression_ro_2/Regression_ro_2.dll" \
Runtime_136000Observe the first assertion in Runtime_136000.cs fail:
Assert.False(test(Task.CompletedTask).GetAwaiter().GetResult());The merged runner can return exit 100 even for a selected test failure; inspect Regression_ro_2.testResults.xml and the assertion output, not exit status alone.
The test uses Reflection.Emit to create a runtime-async method that initializes a ref struct containing an object reference and an integer, awaits the supplied task, then checks whether both fields are default. Its emitted body is effectively:
Initialize(ref payload); // Value = new object(), Number = 42
AsyncHelpers.Await(task);
return IsDefault(ref payload); // Value is null && Number == 0
This is an outline of emitted runtime-async IL, not an ordinary C# async method: Reflection.Emit expresses the byref-like local across the await.
Expected behavior
If the task is already complete, no suspension occurs and the initialized payload is preserved. IsDefault returns false.
If the task actually suspends the method, the byref-like local becomes default on resumption, as specified by runtime async. The regression also checks this separate case. Ordinary byrefs likewise remain valid on synchronous completion and become null after actual suspension.
Actual behavior
For Task.CompletedTask, IsDefault returns true:
Assert.False() Failure
Expected: False
Actual: True
The unchanged original test fails before reaching its pending-task portion. Expanded cases covering generic/nongeneric containing types and ref-struct/ordinary-byref locals reproduce the same first-assertion failure.
Regression?
The test was added by #136085, merged as 9a88a84fac7732c52669c85c28ca8e5e2173947e. That native-JIT fix is already included in the failing build.
The interpreter's clearing-before-check order predates the test: blame attributes it to #121862, 192bdc380b47a01081748c296eb67e5b1ae73bad. The first affected released version has not been established.
Known Workarounds
None verified. Selecting the R2R test configuration does not avoid interpretation of the dynamically emitted method.
Configuration
- CI: .NET main, CoreCLR/browser-Wasm, Linux-hosted Priority 1 runs; source
845ed4c1d00bdc6d00aba9abb707c78385e37ebf. - CI configurations: interpreter and R2R, each with normal tiering settings and no-tiering.
- Fresh local reproduction: browser-Wasm Checked runtime and Release managed libraries, source
d260b5c598ab285046e217c012f6d7126947b06a, under Node.js v26.11.0 on macOS 26.7.1/arm64. This is the actual browser/Wasm target, not a Chrome-tab run or the exact CI binary. - Local ReadyToRun/tiering switches are environment controls, not verified R2R-compiled callers or composite payloads.
- Other interpreter hosts have not been reproduced locally; the affected handler is shared interpreter code.
Other information
In the shared INTOP_HANDLE_CONTINUATION / INTOP_HANDLE_CONTINUATION_GENERIC handler, the zeroedLocalsIntervals loop runs before the GetContinuation() == NULL early exit. The compiler correctly puts byref/byref-like locals in the zeroing map instead of capturing them into a heap continuation, but execution applies that zeroing even when no continuation was returned.
The correction is to take the no-continuation fast path first and only zero these locals on actual suspension, preserving the existing defaulting behavior for real suspensions.
This is distinct from the native-JIT continuation-capture issue in #136000 and its fix #136085.
Note
This report was generated with GitHub Copilot.
Activity
dotnet-policy-service commentedon Oct 20, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.
[clr-interp] Preserve byref-like locals when an await does not suspend (