Skip to content

CoreCLR interpreter clears byref and byref-like locals when an await does not suspend #136171

Closed

Description

@radekdoulik

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=Release

Run 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_136000

Observe 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

  1. dotnet-policy-service commented on Oct 20, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
    See info in area-owners.md if you want to be subscribed.

  2. added a commit that references this issue on Oct 20, 2026
    6f6de27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions