[wasm] Report on-frame GC slots as pinned#131374
Conversation
fgWasmSpillRefs spills ref/byref values that are live across a call into pinned shadow-stack slots, because values on the wasm operand stack are not reported to the GC. It skipped values sourced from a non-address-exposed GT_LCL_VAR, on the grounds that such a local "won't be mutated between its def and its use". That only accounts for explicit IR mutation, not for the GC. GC locals are forced to the shadow stack on wasm, so the local's home slot is a reported root the GC updates in place -- but once the local has been loaded onto the operand stack, the pushed value is a copy the GC cannot see. A nested call is a safepoint: the GC moves the object and fixes up the local's slot, and the consumer then dereferences the stale pre-move address. The common shape is `obj.Field = Allocate()`, which lowers to STOREIND(LCL_VAR obj, CALL alloc). Record GT_LCL_VAR refs in the live-defs list so they are spilled like any other ref. To avoid pinning far more than necessary, drop from the spill set any non-address-exposed local that is a direct operand of the call being processed: the object stays reachable through the local's own reported slot for the duration of the call, and there is no safepoint between the load and the call for the pushed copy to go stale. Without that narrowing, every `Foo(localRef)` would gain a pinned temp, a store, a reload and a block-end zeroing store. Fixes dotnet#131373 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR updates the WebAssembly-specific fgWasmSpillRefs pass to ensure GC refs/byrefs that are live across calls are spilled to pinned shadow-stack slots even when the value originates from a local, and adds a wasm R2R regression test covering the scenario.
Changes:
- Extend the spill-candidate tracking to include
GT_LCL_VARref/byref nodes (so they can be pinned/spilled across nested calls). - Add a narrowing step at each call site intended to avoid spilling certain non-address-exposed locals that are directly consumed by the call.
- Add a new JitBlue regression test (
Runtime_131373) withAlwaysUseCrossGen2on the browser leg to ensure wasm R2R coverage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/coreclr/jit/fgwasm.cpp | Adjusts wasm spill pass to treat GT_LCL_VAR refs as spill candidates and attempts to narrow spills for call-consumed locals. |
| src/tests/JIT/Regression/JitBlue/Runtime_131373/Runtime_131373.cs | Adds a regression reproducer exercising a local-held ref across an allocating call in wasm R2R. |
| src/tests/JIT/Regression/JitBlue/Runtime_131373/Runtime_131373.csproj | Configures the test to force CrossGen2 for TargetOS=browser so it runs as wasm R2R on the browser leg. |
Validation on the browser R2R rig showed the test is a false pass: crossgen'd with the fixed and unfixed JIT it produces byte-identical R2R images, so it would pass on main and guards nothing. The shape doesn't hold a bare GT_LCL_VAR across the call. `node.Payload = Call()` lowers to STOREIND(ADD(LCL_VAR node, offset), CALL) because the field is at a non-zero offset, and that TYP_BYREF ADD was already recorded and spilled by the old code. Call-argument shapes are immune for a different reason: fgMorphArgs spills an earlier argument into a temp when a later one contains a call, so the load happens after the safepoint. Sourcing a real triggering method from CoreLib instead; a test will follow once a shape is confirmed to differ between the fixed and unfixed JIT. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
Replaces the earlier attempt, which was a false pass. Verified on the browser R2R rig: faults deterministically on the unfixed JIT (box.Slot null at i=0, 5/5 runs) and passes on the fixed JIT (5/5). The fixed and unfixed R2R images differ, and the spill instrumentation confirms StoreThroughByref is where the fix spills a GT_LCL_VAR. Three things were required to make it a real guard, each of which the first attempt got wrong: - Store through a byref parameter, not a class field. A field sits at a non-zero offset, so its address is ADD(LCL_VAR obj, offset) -- a byref ADD the unfixed code already spilled. A `ref` parameter store has no ADD, so the address is the bare GT_LCL_VAR. - Allocation pressure around the forced compacting collect, so the collection has something to relocate. Without it nothing moves and the stale copy is still valid. - A synchronous [Fact]. The merged runner emits a bare `Type.Method();` statement and never awaits, so an async Task test would have its Task discarded and its assertion failures dropped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
A moving GC relocates when an allocation forces a collection, so the churn -- not the forced GC.Collect on its own -- is what guarantees the object actually moves. Only finalizers need the event loop; relocation itself is synchronous. The previous wording implied the churn was just about improving the odds. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/tests/JIT/Regression/JitBlue/Runtime_131373/Runtime_131373.cs:78
Allocatecurrently always performs allocation churn and a forced compacting GC.Collect, even on non-browser legs where the test isn't intended to validate wasm R2R behavior. This can make the test suite significantly slower on other platforms. Consider guarding the expensive churn/collect behind anOperatingSystem.IsBrowser()check so only the browser leg pays the cost.
private static object Allocate(int i)
{
for (int j = 0; j < 48; j++)
{
byte[] garbage = new byte[8192];
GC.KeepAlive(garbage);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/tests/JIT/Regression/JitBlue/Runtime_131373/Runtime_131373.cs:44
- The test only exercises the intended wasm R2R path on the browser leg (via AlwaysUseCrossGen2), but currently runs unconditionally. Given the heavy allocation/GC behavior in this test, consider making it browser-only using ConditionalFact + PlatformDetection to avoid slowing down non-browser test legs that don’t provide coverage here.
using System;
using System.Runtime.CompilerServices;
using Xunit;
public class Runtime_131373
{
public sealed class Box
{
public object Slot;
public int Tag;
}
[Fact]
public static void TestEntryPoint()
src/tests/JIT/Regression/JitBlue/Runtime_131373/Runtime_131373.cs:47
- This test currently does 400 iterations, each forcing a compacting Gen2 GC (via Allocate), plus substantial allocation churn. That’s likely to be very slow (especially on wasm/node) and risks test timeouts; the PR description indicates the failure happens on the first iteration, so the outer loop can likely be reduced to a small count while still guarding the regression.
{
for (int i = 0; i < 400; i++)
{
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
GC-typed locals live on the shadow stack (they are not eligible to be "register allocated" aka kept in Wasm locals) and should already be getting reported as pinned from their shadow stack homes. If this is not happening, we should fix the GC reporting, rather than adding code to re-spill things that are already essentially spilled. |
|
You're right, and the ABI says so explicitly — I was fixing this a layer too high.
( It isn't happening today. On-frame GC locals are encoded So I'll pivot this to the reporting fix. One question on the right layer before I do: Should on-frame Wasm GC roots get The encoder looks like the less invasive layer given that Wasm already forces One thing I'd keep from this PR either way: the regression test. It fails deterministically without a fix and passes with one, and it should be equally valid against the reporting fix. Note This reply was drafted with GitHub Copilot. |
Replaces the fgWasmSpillRefs change with a fix at the reporting layer, per review feedback. The wasm ABI already specifies the intended model. From "GC References at Call Sites" in clr-abi.md: all GC references live after the call (and all untracked GC references, which are effectively always live) must be saved to the linear stack. These GC references will be reported as pinned to the GC so that if they normally live in Wasm locals those locals do not need to be updated after the call. GC-typed locals are already homed on the linear stack (they are denied wasm locals via DoNotEnregisterReason::WasmGCVisibility), so they satisfy the "saved to the linear stack" half. But they were encoded GC_SLOT_UNTRACKED without GC_SLOT_PINNED, which is only added for lvPinned. The GC therefore relocated the referent and updated the home, while the copy already pushed onto the wasm operand stack kept the stale pre-move address -- a GC hole whose symptom was an intermittent NullReferenceException in reflection paths under R2R. Report on-frame GC slots as pinned on wasm, which is what the ABI calls for. These slots were already reported and scanned as GC roots, so this adds no new zero-initialization requirement; it only tells the GC not to relocate. fgWasmSpillRefs goes back to skipping non-address-exposed GT_LCL_VAR sources. The skip is now justified by the pinning invariant rather than by the previous claim that the local "won't be mutated between its def and its use", which did not account for relocation; the comment is updated to say so. Values with no linear-stack home -- call results, indirections, computed byrefs -- still go through the spill path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
Wrap the existing `if (varDsc->lvPinned)` in #ifndef TARGET_WASM so wasm drops the condition and shares the block, instead of having its own copy of the flag assignment. Matches existing usage elsewhere in the JIT (e.g. forwardsub.cpp around the TARGET_ARM check) and leaves the non-wasm code untouched in the diff. Also record what this pinning depends on. It covers every on-frame GC local only because wasm forces noTrackedGCSlots, which routes them all through the untracked section. If tracked GC slots are enabled on wasm later they would be reported by gcMakeVarPtrTable instead, which pins only on pinned_OFFSET_FLAG, and the pinning would silently stop happening -- with nothing on other targets to catch it. Noted at both the pinning site and the TODO-WASM that would prompt the change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
Ten lines of comment for a two-line change was out of proportion, and editing the TODO-WASM line put a comment change on code this fix doesn't touch. Condensed to the two facts worth stating -- why wasm pins, and that it depends on noTrackedGCSlots routing on-frame GC locals through this section. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
The code already says wasm always pins; what it can't say is why. Keep that and the doc reference, drop the rest. The forward-looking note about tracked GC slots was speculation about a change nobody has made, and the kind of thing that goes stale and starts misleading. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/coreclr/jit/gcencode.cpp:4192
- The new wasm pinning comment reads as if it applies generally to “refs homed on the linear stack”, but this logic currently only runs for stack slots reported via the untracked section. Since wasm forces
noTrackedGCSlots = true, that’s effectively all on-frame GC locals today; if tracked GC slots are ever enabled, the same pinning rule would need to be applied ingcMakeVarPtrTable(tracked-lifetime path) to avoid reintroducing this behavior gap.
// Per the wasm ABI refs homed on the linear stack are reported pinned, since copies of them
// on the wasm operand stack are invisible to the GC. See "GC References at Call Sites".
#ifndef TARGET_WASM
|
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. |
AndyAyersMS
left a comment
There was a problem hiding this comment.
Code change LGTM.
I don't think we need the detailed comments in the test case.
Per review. The header block also described the original spill-pass diagnosis, which no longer matches the fix. Left two lines saying what goes wrong and that it only reproduces under wasm R2R, and one line on the csproj explaining why crossgen is forced on the browser leg. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
|
Thanks — comments cut in 3f6f612. Worth noting the header block had also gone stale: it still described the original One correction to an earlier reply of mine on the Note This reply was drafted with GitHub Copilot. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/coreclr/jit/gcencode.cpp:4194
- The wasm-specific pinning behavior here depends on wasm currently forcing
noTrackedGCSlots=true(so on-frame GC refs/byrefs are reported via this untracked section). The new comment doesn’t mention that dependency or that enabling tracked GC slots would require applying the same pinning rule ingcMakeVarPtrTable, which risks the original issue silently coming back when the TODO is addressed.
// Per the wasm ABI refs homed on the linear stack are reported pinned, since copies of them
// on the wasm operand stack are invisible to the GC. See "GC References at Call Sites".
#ifndef TARGET_WASM
if (varDsc->lvPinned)
#endif // !TARGET_WASM
Keep only what the code doesn't say itself, per the house style in dotnet#131374. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 86fe3398-c7ec-4555-b4ba-c732819b4dc5
| // Per the wasm ABI refs homed on the linear stack are reported pinned, since copies of them | ||
| // on the wasm operand stack are invisible to the GC. See "GC References at Call Sites". | ||
| #ifndef TARGET_WASM | ||
| if (varDsc->lvPinned) | ||
| #endif // !TARGET_WASM |
There was a problem hiding this comment.
I thought lvPinned was expected to be added here:
runtime/src/coreclr/jit/fgwasm.cpp
Line 1919 in bd240d9
How do we end up with a local live across a safe point without the logic here kicking in?
There was a problem hiding this comment.
Because that path is never reached for a plain local — fgWasmSpillRefs explicitly skips non-address-exposed GT_LCL_VAR:
If a value is just a
GT_LCL_VARthat isn't address-exposed, by construction we ensure that it won't be mutated between its def (here) and its use (the call that would produce a spill) and we won't need to spill it.
That reasoning holds for IR mutation but not for relocation, so the skip is where the hole comes from.
Removing the skip is exactly what this PR did originally — locals then flowed to the lvPinned slot you linked. @AndyAyersMS's point was that it re-spills a value that already has a linear-stack home, and pinning that home gets the same effect without the extra slot, store, reload and block-end zeroing. Measured on the framework composite: +22,480 bytes for pinning vs +291,124 for the spill version.
Note
This reply was drafted with GitHub Copilot.
There was a problem hiding this comment.
Hmm, that makes sense. But:
Removing the skip is exactly what this PR did originally — locals then flowed to the lvPinned slot you linked. @AndyAyersMS's point was that it re-spills a value that already has a linear-stack home, and pinning that home gets the same effect without the extra slot, store, reload and block-end zeroing. Measured on the framework composite: +22,480 bytes for pinning vs +291,124 for the spill version.
Yes, it makes sense that pinning stuff will make the code smaller, but that is a trade off with GC performance and fragmentation. Doesn't this PR essentially report everything as pinned always? If we are going to do that then reloads from the shadow stack seem unnecessary in the first place.
There was a problem hiding this comment.
Can we just mark the locals we identify above as lvPinned directly?
There was a problem hiding this comment.
We aren't reloading (at least we shouldn't be), just spilling.
There was a problem hiding this comment.
We aren't reloading (at least we shouldn't be), just spilling.
I mean that if we are going to pin all references on the shadow stack, then we should be able to register allocate them too (keeping them both in the shadow stack and in the wasm local).
There was a problem hiding this comment.
We could set Wasm only stack pinning flag in a variant of the initial fix that we respect in slot pinning instead of doing it unconditionally?
There was a problem hiding this comment.
Explicitly marking the locals as pinned may have other side effects so may not be as clean as it might sound.
I suppose we could have a new bit on LclVarDsc if we are worried about it.
There was a problem hiding this comment.
I'll prototype that but my primary goal at the moment is to get tests stood up without crashing so if there isn't consensus I can open and issue and a different pr to track design resolution?
There was a problem hiding this comment.
If Andy thinks pinning everything is the best, then feel free to go with that, but it felt like a big hammer to me and seemed like something we had been avoiding previously.
Hmm, maybe I am mistaken, but I thought we were reloading these locals from the shadow stack at uses. Why do they need to be pinned? |
|
They are reloaded at every use, but the load lands where the wasm value stack requires, which can be before an intervening safepoint — see
For Measured: 13,018 such values across 5,167 framework methods; the test here fails on the first iteration without pinning. Your question does point at the alternative, though — "we may want to un-nest calls, relying on a Wasm local instead of the Wasm stack" would make reload-at-use literally true and drop the need to pin these. Much larger change, and it wouldn't remove Note This reply was drafted with GitHub Copilot. |
The code I linked above is supposed to handle that. When a LIR edge spans a GC safe point, the code is meant to store it into a pinned local and replace the use by that pinned local. EDIT: Ah ok, I see you responded in the comment. |
|
Opened #131402 as a draft alternative to this, since the objection above is a fair one and easier to judge side by side than in the abstract. It pins only the locals whose operand-stack copy actually spans a safepoint, rather than every on-frame GC slot: 21,161 vs 141,029 pinned (15.0%, a 6.7× reduction), and slightly smaller (+14,256 B vs +22,512 B) on the same composite against a common baseline. Correctness is equal — same probe 3/3, same 5-seed NRE check clean — so "held across a call" turns out not to be too narrow a criterion. It also doesn't reuse One thing I'd keep honest: #131402 reduces the number of pins, not their duration — that's still until the local is overwritten or the frame returns. GC-pressure measurement couldn't separate the two variants on the available workloads, so the static pin count is the defensible number, not a runtime claim. Only one of the two should be taken. Happy either way — this one is approved and smaller, #131402 is more targeted and directly answers the objection. Note This comment was drafted with GitHub Copilot. |
Alternative to dotnet#131374, which reports every on-frame GC slot pinned. This pins only the locals whose operand-stack copy actually spans a safepoint. On wasm a GC reference loaded from its frame home onto the operand stack is a copy the GC can neither see nor update, so it goes stale if the referent is relocated at an intervening call. fgWasmSpillRefs handles that for values with no home by spilling them to pinned slots, but it skips non-address-exposed GT_LCL_VAR on the grounds that such a local "won't be mutated between its def and its use" -- true of IR mutation, but not of relocation. Stop skipping those locals, and instead of spilling them to a second slot, report the home they already have as pinned. Reuse of lvPinned is deliberately avoided: it is a source-level concept with roughly fifteen consumers, all of which run before this phase, and setting it here breaks the lvPinned => !lvTracked invariant asserted in gcencode.cpp and gcinfo.cpp. A wasm-only lvWasmReportPinned bit, read only when encoding GC info, says just the thing that needs saying. Locals the call consumes directly are excluded: there is no safepoint between loading one and the call, and the home keeps the referent reachable meanwhile. Measured against the same unfixed baseline on a browser R2R composite: pinned slots size dotnet#131374 (pin all) 141,029 (100%) +22,512 this change 21,161 (15.0%) +14,256 spill to new slots n/a (bounded) +2,028,932 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
Alternative to dotnet#131374, which reports every on-frame GC slot pinned. This pins only the locals whose operand-stack copy actually spans a safepoint. On wasm a GC reference loaded from its frame home onto the operand stack is a copy the GC can neither see nor update, so it goes stale if the referent is relocated at an intervening call. fgWasmSpillRefs handles that for values with no home by spilling them to pinned slots, but it skips non-address-exposed GT_LCL_VAR on the grounds that such a local "won't be mutated between its def and its use" -- true of IR mutation, but not of relocation. Stop skipping those locals, and instead of spilling them to a second slot, report the home they already have as pinned. Reuse of lvPinned is deliberately avoided: it is a source-level concept with roughly fifteen consumers, all of which run before this phase, and setting it here breaks the lvPinned => !lvTracked invariant asserted in gcencode.cpp and gcinfo.cpp. A wasm-only lvStackPinned bit, read only when encoding GC info, says just the thing that needs saying. Locals the call consumes directly are excluded: there is no safepoint between loading one and the call, and the home keeps the referent reachable meanwhile. Measured against the same unfixed baseline on a browser R2R composite: pinned slots size dotnet#131374 (pin all) 141,029 (100%) +22,512 this change 21,161 (15.0%) +14,256 spill to new slots n/a (bounded) +2,028,932 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d24becc-f8f3-40bf-b13b-5dd1df662196
I guess this comment was confusing. We have to reload but we do it just after spilling (there is no Keeping GC refs in locals (basically doing a general sort of "GC write through") seems like an appealing thing to try, but for now I simply want to get something in place that works, so we can enable CI testing. Once we're good there it will be easier to experiment with alternatives. |
Fixes #131373
Problem
On wasm, a GC reference loaded from its linear-stack home onto the wasm operand stack is a copy the GC can neither see nor update. If the object is relocated at a safepoint, the GC fixes up the home slot but the pushed copy keeps the stale pre-move address, and the consumer dereferences it.
The wasm ABI already specifies the model that prevents this —
docs/design/coreclr/botr/clr-abi.md, "GC References at Call Sites":GC-typed locals already satisfy the "saved to the linear stack" half: they are denied wasm locals via
DoNotEnregisterReason::WasmGCVisibilityand homed on the frame. But they were not being reported pinned — ingcMakeRegPtrTablethey are encodedGC_SLOT_UNTRACKED, andGC_SLOT_PINNEDwas only added whenvarDsc->lvPinnedis set, which ordinary GC locals never set. So the referent moved and copies on the operand stack went stale.The symptom was an intermittent
NullReferenceExceptioninRuntimeCustomAttributeData..ctorand similar reflection paths during xUnit discovery under wasm R2R.Fix
Report on-frame GC slots as pinned on wasm. Non-wasm targets are untouched, and this adds no code to the generated image — only GC-info flags. These slots were already reported and scanned as GC roots, so there is no new zero-initialization requirement; it only tells the GC not to relocate.
fgWasmSpillRefsis unchanged and remains load-bearing: refs with no frame home (call results, indirections, computed byrefs) still need spilling, and pinning cannot cover them.Validation
Measured on a browser R2R rig with the pinning change as the only variable (identical
fgwasm.cppon both sides; fixed = pinning, unfixed =gcencode.cppreverted).Runtime_131373Cost, versus the same composite without pinning: +22,480 bytes, entirely GC-info flags. Peak RSS under an allocation-churn workload with forced compacting collects was ~344.8 MB pinned vs ~348.7 MB unpinned — within noise. Pervasive pinning does not show up as footprint growth here, because the pinned set at any GC point is a small transient per-frame population rather than a persistent un-compactable mass. That is one synthetic workload and RSS is coarse, so it bounds the risk rather than eliminating it.
Instrumenting the spill pass confirms the two mechanisms address the same population and are complementary:
GT_LCL_VARspills drop from 13,018 across 5,167 methods to 431 across 170, withRuntimeCustomAttributeDatafalling out of the set entirely. The residual 431 are address-exposed locals, whichfgWasmSpillRefsstill spills by design. A further 15,684 spills are call results and indirections, which have no home slot and so are unaffected.Regression test
Runtime_131373stores through aref objectparameter whose value comes from an allocating call, so the store address is a bareGT_LCL_VARpushed across a safepoint.Three variations look correct but silently false-pass, which is worth recording:
obj.Field = Call()lowers toSTOREIND(ADD(LCL_VAR obj, offset), CALL)because the field is at a non-zero offset (the method table pointer occupies 0). That byrefADDhas no linear-stack home and was already spilled. Storing through arefparameter has noADD.fgMorphArgsspills an earlier argument to a temp when a later one contains a call, so the load happens after the safepoint.The test is also deliberately a synchronous
[Fact]: the merged CoreCLR runner emits a bareType.Method();statement (XUnitWrapperGenerator/ITestInfo.cs) and never awaits, so anasync Tasktest would have its Task discarded along with its assertion failures.Note that this fix leaves R2R instruction bytes unchanged — the difference is
GC_SLOT_PINNEDin the embedded GC info — so the behavioural fail/pass is the load-bearing evidence rather than an image comparison.History
This PR originally fixed the hole in
fgWasmSpillRefs, by spillingGT_LCL_VAR-sourced refs into pinned slots and narrowing the spill set to exclude direct call operands. That worked, but as @AndyAyersMS pointed out it re-spills values that already have linear-stack homes — the defect is the missing pinned reporting, one layer down. The measurements above bear that out: the reporting fix is 268,644 bytes smaller than the spill approach on the same composite.Note
This change was authored with GitHub Copilot.