Skip to content

[wasm][R2R] Fix generic context / async continuation order in Wasm interpreter thunks - #134676

Open
lewing wants to merge 9 commits into
mainfrom
lewing-investigate-issue-134660
Open

lewing wants to merge 9 commits into
mainfrom
lewing-investigate-issue-134660

Conversation

@lewing

@lewing lewing commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Problem

For shared-generic runtime-async methods, crossgen2's Wasm thunks passed the hidden generic context and the async continuation in each other's slots.

WasmLowering.RaiseSignature has no way to mark a parameter as the hidden generic context, so it returns it as the first entry of the MethodSignature parameter list, with this and the return buffer implied by the signature flags and return type. The thunks built their ArgIterator from that signature without methodRequiresInstArg, so ArgIterator laid the frame out as [this][continuation][ctx][args]. The interpreter, the VM ArgIterator and the callee's GC ref map (GCRefMapBuilder.GetCallRefMap) all expect [this][ctx][continuation][args].

Andy diagnosed this in #133627 and proposed modeling the context as the hidden instantiation argument; this PR follows that suggestion.

Symptoms:

Fix

  • When a generic context precedes the async continuation, drop it from the layout signature and build the ArgIterator with methodRequiresInstArg: true. The context is then stored and loaded through GetParamTypeArgOffset(), and the continuation through GetAsyncContinuationArgOffset().
  • Share one argument layout across the three thunks that spill arguments: WasmR2RToInterpreterThunkNode, WasmInterpreterToR2RThunkNode and WasmImportThunk. Each used to hand-code the hidden-argument sequence, which is how the slots got swapped. WasmThunkArgLayout walks the Wasm signature string in Wasm parameter order, [this] [retbuf] [generic context] [continuation] [args] per clr-abi.md, takes each offset from that ArgIterator, and asserts that the two agree. Each thunk keeps its own emission and loops over the entries.
    • For WasmImportThunk, the swapped spill disagreed with the delay-load GC ref map, which GCRefMapNode builds from the callee's MethodDesc via GCRefMapBuilder.GetCallRefMap. A GC during the fixup would have reported the generic context slot as an object reference and missed the continuation. The thunk now spills both to the offsets the GC ref map describes, and WasmThunkArgLayoutMatchesCallRefMapLayout asserts the two layouts are identical.
  • Move the three copies of HasGenericContextBeforeAsync into WasmLowering, and use it from RaiseSignature so the encoding is parsed in one place.
  • RaiseSignature's output is unchanged: the direct-forwarding thunks and INodeWithTypeSignature.Signature still get the context as the first entry of the parameter list.

Testing

Notes

cc @AndyAyersMS @davidwrighton

Resolves #133953
Resolves #134660
Resolves #133627

Note

This PR description was drafted with GitHub Copilot.

lewing and others added 3 commits September 25, 2026 14:30
…thunks

WasmLowering.RaiseSignature models the hidden generic context as explicit
parameter 0, so ArgIterator placed it after the async continuation slot.
The interpreter (and the VM ArgIterator) expect the generic context before
the async continuation, so the R2R->interpreter and interpreter->R2R thunks
swapped the two arguments for shared generic async methods.

This caused the interpreter to see a MethodDesc in the GC-reported
continuation slot (SanityCheck() failures in AsyncHelpers.Await) and a
null/garbage generic context (numGenericArgs > 0 assert in dictionary
lookups).

Fixes #133953
Fixes #134660

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the per-thunk context/continuation offset swap with a shared
GCRefMapBuilder.BuildWasmThunkArgIterator helper. When a generic context
precedes the async continuation, it drops the context from the raised
layout signature and builds the ArgIterator with methodRequiresInstArg,
so the context is stored and loaded through GetParamTypeArgOffset() and
precedes the continuation, matching the interpreter and the callee's
GC ref map.

This also fixes WasmImportThunk, which spilled the two arguments in the
swapped order during delay-load fixups, so the GC ref map would report
the generic context slot as an object reference and miss the
continuation.

HasGenericContextBeforeAsync moves to WasmLowering instead of being
duplicated in each thunk. Add WasmArgumentLayoutTests coverage for the
thunk layout, and re-enable RuntimeAsync_WhenAny_TracksAllBranches,
which was disabled for the same root cause.

Fixes #133627

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
RaiseSignature re-implemented the check for a generic context that
precedes the async continuation. Call WasmLowering.HasGenericContextBeforeAsync
instead so the signature encoding is parsed in one place.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
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.

@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@lewing
lewing requested a review from AndyAyersMS September 25, 2026 20:23
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/crossgen-contrib
See info in area-owners.md if you want to be subscribed.

@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

@AndyAyersMS I hope this isn't duplicating anything, go ahead and reject this if you have a fix in progress.

@lewing lewing added the arch-wasm WebAssembly architecture label Sep 25, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@AndyAyersMS

Copy link
Copy Markdown
Member

@AndyAyersMS I hope this isn't duplicating anything, go ahead and reject this if you have a fix in progress.

Nope, I hadn't looked at fixing this yet.

@davidwrighton

Copy link
Copy Markdown
Member

@lewing, getting the wrong GC Ref map is a serious problem. Please dont' comment that it isn't understood, please fix instead.

The delay-load GC ref map for a call is computed from the callee's
MethodDesc, while WasmImportThunk spills the arguments using only the
callee's Wasm signature. Factor GetCallRefMap's ArgIterator construction
into BuildCallRefMapArgIterator and add a test asserting both produce
the same generic context, async continuation and argument offsets for
shared generic, async and shared generic async CoreLib methods.

Against the previous thunk layout, the shared generic async cases fail
with the generic context one slot away from where the GC ref map
reports it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

Sorry, that was badly worded. The GC ref map mismatch is fixed in this PR, not left open. WasmImportThunk now builds its spill layout with the same ArgIterator flags GCRefMapBuilder.GetCallRefMap uses for the callee. The generic context is spilled at GetParamTypeArgOffset() and the continuation at GetAsyncContinuationArgOffset(), which are the slots the delay-load GC ref map reports as the instantiation parameter and as a GC reference. "Haven't reproduced" only meant that no test forces a GC during the fixup.

bd8aa3c moves the ArgIterator construction from GetCallRefMap into a shared helper and adds WasmThunkArgLayoutMatchesCallRefMapLayout. It asserts that the thunk and GC ref map layouts are identical for shared generic, async and shared generic async methods, and it fails against the old layout. I've also updated the description.

Note

This comment was drafted with GitHub Copilot.

Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
Comment thread src/coreclr/tools/Common/JitInterface/WasmLowering.cs Outdated
lewing and others added 3 commits September 25, 2026 17:59
The hidden generic context is always passed immediately before the async
continuation (clr-abi.md, "Passing Continuation argument"). Since it is
encoded like any pointer-sized argument, detect it as the pointer char
immediately preceding the 'a' continuation marker instead of re-parsing
the return type and 'this'.

Reword the comments that described the context as "explicit parameter
0": that referred to RaiseSignature's MethodSignature parameter list,
not the ABI position.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the multi-line remarks on BuildWasmThunkArgIterator and
HasGenericContextBeforeAsync with one-line summaries and a short inline
comment where the code isn't self-explanatory; the ABI ordering is
already documented in clr-abi.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add WasmThunkArgLayout, which walks a managed Wasm signature string in Wasm
parameter order and maps each element (this, retbuf, generic context, async
continuation, explicit arguments) to its Wasm parameters and its
ArgIterator offset. The R2R-to-interpreter, interpreter-to-R2R, and import
thunks now each handle their arguments with a single loop over the layout
instead of hand-coding the hidden argument sequence.

Move BuildWasmThunkArgIterator from GCRefMapBuilder into the new type.
The refactor produces byte-identical crossgen2 output.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

- Remove the unused WasmThunkArgLayout.Signature, WasmThunkArg.Type and
  RetBufParamIndex; make BuildArgIterator private.
- Move the typed load/store helpers to Memory.Load/Store in
  WasmInstructions and use them for the thunk return values as well.
- Drop the stale spill pseudocode in WasmImportThunk and note when a
  generic context is a hidden argument.
- Fold the ArgIterator-level thunk layout tests into the layout theories.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-wasm-libtests

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

{
This,
RetBuf,
// Only when an async continuation follows; otherwise the context is encoded as the first explicit argument.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this comment also explain why this special casing of async continuation + generic context is required (or link to where it is explained)? I am not able to figure it out, and it seems to be a bug farm. What would need to change to get rid of this special casing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Wasm signature string has no distinct token for the hidden generic context. WasmLowering.GetSignature emits it as the pointer char (i on wasm32), the same as an explicit pointer argument, and the thunks only have that string, since they're shared across methods by signature. So the layout can only recognize the context by position, when the a continuation follows it. Without a continuation it doesn't matter: GetParamTypeArgOffset() is the same slot as the first argument, so the layout is identical either way.

Removing the special case needs a dedicated token for the generic context, emitted by GetSignature and the VM's GetSignatureKey (vm/wasm/helpers.cpp), and understood by RaiseSignature, the portable call-helper tokenizer and readytorun-format.md. The layout would then use the hidden instantiation argument whenever the token is present. The cost is that signatures differing only in context versus an explicit pointer argument would no longer share a thunk. Since that changes the thunk key format on both the VM and crossgen2 sides, I'd prefer to do it as a follow-up: #134716. For now f119a1d expands this comment to say why the special case exists.

Note

This comment was drafted with GitHub Copilot.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas it is definitely a source of bugs as demonstrated in this pr which reconstructs the requirement from the rest of signature and fixes the existing thunks. If this is the time to make the signature string encode the generic context distinctly I'm happy to do it here even if the agent is overly focused on our repo instructions to separate concerns.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas here's what the dedicated token looks like: prototype branch. It isn't part of this PR yet.

  • The generic context gets its own signature char, g, in both crossgen2 (WasmLowering.GetSignature) and the VM key (GetSignatureKey in vm/wasm/helpers.cpp). Wasm function types are unchanged.
  • HasGenericContextBeforeAsync, the RaiseSignature roundtrip swap, and the async-only case in WasmThunkArgLayout are gone. The layout treats the context as the hidden instantiation argument whenever g is present. RaiseSignature reports it as a flag, like the continuation, instead of as parameter 0.
  • 9 files, +82/−69. WasmArgumentLayoutTests passes 94/94. With a rebuilt browser runtime, the R2R async runtime tests pass 132/132, and a logging build confirmed the lookups hit the new g keys.
  • Every thunk body is byte-identical to before; the cost is lost sharing. A method with a generic context and one with an explicit pointer argument used to share a thunk, so CoreLib gains 232 duplicate thunks (about 20 KB). Registering one body under both keys would recover most of that, but StringDiscoverableAssemblyStubNode would need to support more than one lookup string.

I'm happy to fold this into this PR or land it separately as #134716. Which do you prefer?

Note

This comment was drafted with GitHub Copilot.

…async

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-ReadyToRun

Projects

None yet

4 participants