Repository navigation
JIT: Exclude byref-like structs from async continuation capture - #136085
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a18055aa-e854-4590-b3ee-c17c341bde34
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
Since @jakobbotsch is currently OOO, can @VSadov or @davidwrighton take a look if that is the right fix? |
It was considered, but there is not much advantage in making byref structs with no byref fields (recursively) an exception from the rules - only extra complexity in the spec. Besides, the cases where byref structs do not have byref fields is often a pattern when the struct wants to be “pinned” to the thread for correctness - like Lock.Scope, ObjectHandleOnStack,... |
It was once mentioned that we could build ArrayPool that returns a ref struct holding an object and it could survive the Well, then this PR is correct, right? Because it rejects such cases. |
|
/ba-g failure is #136096 |
#136172) <!-- --> ## Summary Move the existing no-continuation fast path ahead of local zeroing in the shared interpreter continuation handler. For a synchronously completed await, the method does not suspend and must preserve its initialized locals. The existing order clears those locals before taking the no-continuation fast path. For actual suspension, retain the existing zeroing before handling the continuation. Both the ordinary and generic continuation opcodes share this handler. ## Regression coverage Expand the existing `Runtime_136000.TestEntryPoint` into a four-case conditional theory: generic/nongeneric containing type crossed with ref-struct/byref local. Every case checks preservation across `Task.CompletedTask`, an incomplete result for a pending task, and defaulting after actual suspension. Use a no-inline ref identity helper for the ordinary byref, analogous to the original payload's opaque initialization. This keeps a byref local represented so the test exercises continuation-local clearing rather than recomputation of a local's address. Checked interpreter dumps confirm both ordinary and generic continuation handlers are exercised. Existing feature conditions are retained; no new files or skips are added. A direct-address prototype exposed a separate native-JIT address-rematerialization issue after suspension. The opaque initializer isolates the interpreter-local clearing path; this PR does not change JIT code or disable the byref cases. The test was added by [#136085](#136085), which fixes a different native-JIT continuation-capture problem. The interpreter's clearing-before-check behavior predates that test. ## Validation Built actual CoreCLR/browser-Wasm Checked runtimes and Release managed libraries from `d260b5c598ab285046e217c012f6d7126947b06a`, then rebuilt the corrected native Wasm runtime and refreshed Core_Root. Also built the same-source macOS/arm64 Checked runtime for a native-JIT control. | Validation | Result | |---|---| | Unchanged original regression, fresh unfixed Wasm runtime | 1 expected first-assertion failure | | Final expanded regression, retained unfixed Wasm runtime | 16/16 expected first-assertion failures | | Byte-identical original regression, fixed Wasm runtime | 4/4 passed | | Final expanded regression, fixed Wasm runtime | 16/16 passed | | Related async coverage through the generated browser wrapper | 36/36 passed | | Final expanded regression, same-source native ARM64 JIT | 4/4 passed | The final positive runs contain **60 passing selected test-case executions, zero selected failures, and zero selected skips**. Filtered-out tests are not counted as executed coverage. Wasm runs used Node.js v26.11.0 and the four `(DOTNET_ReadyToRun, DOTNET_TieredCompilation)` settings `(0,1)`, `(0,0)`, `(1,1)`, and `(1,0)`. The broader coverage comprises nine selected async cases per setting: shared generics, byref/out parameters, array updates, void results, generic virtual awaits and exception propagation, and emitted runtime-async methods. The native control disables R2R and interpretation. Eight FullOpts ARM64 emitted-method assembly listings verify actual native JIT compilation. The final native and Wasm regression DLLs are byte-identical: `74ae40379b4da41bfbc31c56e87ea515398a13a4053238b599ab4355f62fecf3`. Commands ran from the repository root with `DOTNET_WASM_TOOL_CACHE_DIR="$PWD/.dotnet/wasm-tools"`. ```bash # Clean browser/Wasm baseline: passed on the single identical retry. ./build.sh clr+libs+host -os browser -c Release -rc Checked # Original regression, related async runner, and initial test layout: passed. src/tests/build.sh -os browser -arch wasm Checked -skipnative -priority1 \ -test:JIT/Regression_ro_2/Regression_ro_2.csproj \ -test:async/async.csproj /p:TasksConfiguration=Release # Corrected native Wasm runtime, including interpexec.cpp and host relinking: passed. ./build.sh clr -os browser -c Release -rc Checked # Refresh the browser test layout: passed. src/tests/build.sh -os browser -arch wasm Checked -generatelayoutonly \ /p:TasksConfiguration=Release # Same-source native macOS/arm64 runtime: passed. ./build.sh clr+libs+host -os osx -arch arm64 -c Release -rc Checked # Initial native regression and Core_Root layout: passed. src/tests/build.sh -os osx -arch arm64 Checked -skipnative -priority1 \ -test:JIT/Regression_ro_2/Regression_ro_2.csproj \ /p:TasksConfiguration=Release # Final regression build, native: passed. src/tests/build.sh -os osx -arch arm64 Checked -skipnative \ -skipgeneratelayout -priority1 \ -test:JIT/Regression_ro_2/Regression_ro_2.csproj \ /p:TasksConfiguration=Release # Final regression build, browser/Wasm: passed. src/tests/build.sh -os browser -arch wasm Checked -skipnative \ -skipgeneratelayout -priority1 \ -test:JIT/Regression_ro_2/Regression_ro_2.csproj \ /p:TasksConfiguration=Release ``` The initial clean baseline hit an MSB3883 compiler-lock exception before any source edits. The one identical retry passed without a source or build-setting workaround; all listed successful build commands completed with zero warnings and errors. Representative final regression invocations, with `root` set to the repository root and each run executed in a separate output directory: ```bash # Browser/Wasm: 4 selected passes; repeat for each switch combination above. export CORE_ROOT="$root/artifacts/tests/coreclr/browser.wasm.Checked/Tests/Core_Root" DOTNET_ReadyToRun=0 DOTNET_TieredCompilation=0 \ node --experimental-wasm-exnref --stack-size=8192 \ "$root/artifacts/bin/coreclr/browser.wasm.Checked/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 # Native ARM64 JIT: 4 selected passes, with native assembly listings. export CORE_ROOT="$root/artifacts/tests/coreclr/osx.arm64.Checked/Tests/Core_Root" DOTNET_ReadyToRun=0 DOTNET_TieredCompilation=0 DOTNET_InterpMode=0 \ DOTNET_Interpreter='' DOTNET_JitDisasm='*:Test' \ "$CORE_ROOT/corerun" \ "$root/artifacts/tests/coreclr/osx.arm64.Checked/JIT/Regression_ro_2/Regression_ro_2/Regression_ro_2.dll" \ Runtime_136000 ``` The merged runner can return process exit 100 even for failed tests. Results above come from the selected xUnit XML entries and exact-count checks, not process exit status alone. Broader browser async tests were run through their generated `async.sh` wrapper for host-side subprocess orchestration. ## Scope The product correction only changes ordering in the shared interpreter handler. Real-suspension zeroing and continuation capture remain unchanged. No native JIT source, Reflection.Emit support, R2R folding, pipeline coverage, or reporting automation is changed. Validation uses the actual browser/Wasm target through Node, not a Chrome tab or the exact CI binaries. The ReadyToRun settings above are environment controls, **not verified R2R-compiled callers or composite CI payloads**. No full async suite, Mono, or other native architectures are claimed. Resolves #136171 > [!NOTE] > This pull request description was generated with GitHub Copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b691535f-9dc8-49e3-95fe-2e83af8ef957
Fixes #136000.
Exclude byref-like structs from async continuation capture even when they contain no byrefs. Capturing these structs can emit heap stores without write barriers because their layouts are considered stack-only, causing GC corruption when continuations are reused.
The runtime-async spec states:
I assume we might allow them actually (that will require a few changes in JIT to stop assuming that "ref struct" means no WB needed in the context of async-v2), but since the spec explicitly says so...