[wasm] Report untracked GC slots for aborted Wasm R2R frames - #134769
Merged
Merged
Conversation
Wasm R2R code has no interruptible ranges and reports all of its frame GC refs as untracked (pinned) slots. When an exception is thrown from a funclet, that frame is ExecutionAborted, so EnumerateLiveSlots reported nothing for it. Its parent frames share the same frame slots and are skipped as already reported by the funclet, so no frame reported the method's untracked slots during exception dispatch. A GC in that window could free objects still held in those slots, and a later report of the slot then hits a dangling reference. Keep reporting untracked slots for aborted Wasm frames that have no interruptible ranges. Interpreter code always has an interruptible range and native targets are unchanged. This fixes the SanityCheck assert in nested EH tests on browser-wasm R2R (Test_throwinfinallynestedintry_30, throwincascadedexcept_d, throwincascadedexceptnofin_d), which reproduces deterministically with DOTNET_GCStress=0x1. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @agocke |
Contributor
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
jkotas
reviewed
Sep 28, 2026
jkotas
reviewed
Sep 28, 2026
Address review feedback: - Add a HAS_INTERRUPTIBLE_RANGES trait to each GcInfoEncoding, false only for Wasm32GcInfoEncoding. The interpreter encoding is also used on Wasm and does define interruptible ranges, so TARGET_WASM is not the right gate. - In EnumerateLiveSlots, skip the interruptible range handling for encodings without interruptible ranges and report only untracked slots, including for aborted frames. Assert the invariant in the decoder and the encoder. - Mirror the change in the cDAC GCInfoDecoder and document it in GCInfo.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Sep 28, 2026
Closed
jkotas
reviewed
Sep 28, 2026
Address review feedback: encodings without interruptible ranges no longer serialize NumInterruptibleRanges in the fat header. The encoder, the runtime decoder, the cDAC decoder and R2RDump all key off HAS_INTERRUPTIBLE_RANGES, which is false only for Wasm32. The asserts on the decoded count are dropped since it is no longer read. This changes the Wasm GC info format, so bump the R2R major version to 31. On browser-wasm CoreLib this saves 1,088 bytes: 2 bits per fat header, which shrinks a blob only when it crosses a byte boundary, and identical GC info blobs are shared. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
jkotas
reviewed
Sep 28, 2026
Wasm has not shipped, so Wasm-specific format changes don't need an R2R version bump. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Member
Author
|
cc @dotnet/wasm-contrib |
lewing
added a commit
that referenced
this pull request
Sep 30, 2026
…tub (#134779) ## Summary On Wasm, `CallDescrWorkerInternal` ran `DoPrestub` with no frame reporting the call's arguments, and `DoPrestub` can trigger a GC. A GC in that window can free a reference argument before the callee receives it. It also ran `DoPrestub` on every call to an R2R-compiled target, not just the first, because R2R methods never have interpreter code. One visible symptom: `CustomAttribute_CreateCustomAttributeInstance` parses a string constructor argument from the attribute blob, ends its GC protection, and calls the constructor through `MethodDescCallSite`. A GC during the prestub collects the string, and the attribute stores a dangling reference. ## Fix - Call `DoPrestub` only when `MethodDesc::ShouldCallPrestub()`, matching the interpreter call path. - `MethodDescCallSite` and reflection invoke already place the arguments right after a `TransitionBlock`. Pass it through `CallDescrData` and push a `PrestubMethodFrame` over it around `DoPrestub`, the same mechanism the native prestub and the Wasm R2R-to-interpreter thunk path use. - `DispatchCallSimple` has no `TransitionBlock`, so its arguments are still not reported during `DoPrestub` (now only on the first call). Left as a follow-up. ## Validation Browser-wasm checked runtime with the `Methodical_d1` Helix payload and R2R-compiled test code, `DOTNET_TieredCompilation=0`: | | Before | After | |---|---|---| | Standalone repro calling `Assembly.GetCustomAttributes(typeof(NeutralResourcesLanguageAttribute), false)` in a loop, `DOTNET_GCStress=0x1` | `SanityCheck()` assert, 4/4 runs | pass, 3/3 runs | | `Methodical_d1` merged runner, `DOTNET_GCStress=0x1`, 4 runs in parallel (with #134769 applied) | intermittent `RawGetMethodTable()` assert in `throw_SEH` | 4/4 clean, 126 passed | | `Methodical_d1` merged runner, no stress | baseline | identical to baseline | In every full run, 18 out-of-process tests fail the same way because the local runner doesn't handle out-of-process tests. There is no new regression test: the hole only shows under GC stress, and no Wasm R2R CI leg runs GC stress today. Resolves #134777 > [!NOTE] > This PR description was drafted with GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2 tasks
jkotas
approved these changes
Sep 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a GC hole during exception dispatch in Wasm R2R code. It shows up in CI as a rare
SanityCheck()assert in nested EH tests on the browser-wasm R2R leg, for exampleTest_throwinfinallynestedintry_30.Root cause
Wasm R2R code has no interruptible ranges, and it reports every GC ref on the frame as an untracked (pinned) slot. A method and its funclets share those frame slots.
When an exception is thrown from a funclet, that funclet's frame is
ExecutionAborted.GcInfoDecoder::EnumerateLiveSlotsfinds no interruptible range covering the offset and returns without reporting anything, including the untracked slots. The parent frames (the caller of an out-of-line finally, and the main method body) are skipped as already reported by the funclet.So during dispatch nothing reports the method's untracked slots. A GC in that window can free an object still held in one of them. In the repro it's a boxed
Int32. The next time the slot is reported, it points at freed memory and hitsSanityCheck().Other targets avoid this in the JIT.
CodeGenInterface::setFramePointerRequiredEHforces every method with EH to be fully interruptible, becauseEnumGcRefsonly reports slots in aborted frames that are fully interruptible. Wasm is explicitly excluded there, and Wasm R2R code can't be fully interruptible.Fix
Add a
HAS_INTERRUPTIBLE_RANGEStrait to eachGcInfoEncoding,falseonly forWasm32GcInfoEncoding.EnumerateLiveSlotsskips the interruptible-range handling for that encoding and reports only untracked slots outside safe points, including for aborted frames.DefineInterruptibleRangeasserts it's never used for that encoding.The gate is on the encoding rather than
TARGET_WASM: on Wasm the interpreter's GC info goes through the same decoder, and interpreter code does define an interruptible range. Native targets are unchanged.The cDAC
GCInfoDecodermirrors the change through the same trait (defaulttrue), anddocs/design/datacontracts/GCInfo.mddocuments it.mainhas no Wasm32 GC info traits in the cDAC yet; #133890 adds them.I fixed this in the decoder rather than having the JIT declare Wasm methods interruptible over their whole range. That claim isn't true for Wasm, and other checks rely on it (GC stress, the safe-point asserts).
Size
On browser-wasm
System.Private.CoreLib.wasm(CI crossgen options), omitting the count saves 1,088 bytes (0.003%). A GC info blob only shrinks when the 2 saved bits cross a byte boundary (11,606 of 58,253 methods), and identical blobs are shared (5,331 distinct unwind entries across 60,323 methods and funclets).Validation
I ran the CI Helix payload for
Methodical_d1from build 1613981 locally, with crossgen2 built from this branch and a browser-wasmcorerunbuilt with and without the fix. For the final format I recompiled CoreLib and the test assemblies with this branch's crossgen2.Test_throwinfinallynestedintry_30,DOTNET_GCStress=0x1SanityCheck()assert as CI, every runthrowincascadedexcept_d,throwincascadedexceptnofin_d,DOTNET_GCStress=0x1Methodical_d1merged runner, no stressMethodical_d1assemblies, R2R-compiled with the checked Wasm JIT, never hit the new encoder assert.readytorun.hand the twoModuleHeadersfiles tomain; it wasn't rebuilt.With this fix, the full
Methodical_d1suite underDOTNET_GCStress=0x1was clean in 3 of 4 runs. The 4th hit aRawGetMethodTable()assert inthrow_SEH. That's a separate, pre-existing hole, #134777:CallDescrWorkerInternalleaves the call's arguments unreported whileDoPrestubruns. It reproduces identically with and without this change and is fixed in #134779. With both fixes, the full suite under stress passed 4 of 4 runs (126 passed), in a run before the count was removed from the format.I also checked native (osx-arm64, release 11.0 rc1 runtime, FullOpts JIT) with a small app. An object is held only in an address-exposed local, a
finallythrows, and the same method catches it with a filter that forces a GC. The object stayed alive 20/20 times, and JitDisasm shows the method as; fully interruptible, assetFramePointerRequiredEHrequires.CI doesn't run GC stress on the Wasm R2R leg, which is why this showed up as a ~0.5% flake rather than a hard failure.
Resolves #134768
Note
This PR description was drafted with GitHub Copilot.