Skip to content

[wasm] Assign frame offsets to dependently promoted parameter fields#131401

Merged
lewing merged 1 commit into
dotnet:mainfrom
lewing:wasm-promoted-param-frame-offset
Jul 27, 2026
Merged

[wasm] Assign frame offsets to dependently promoted parameter fields#131401
lewing merged 1 commit into
dotnet:mainfrom
lewing:wasm-promoted-param-frame-offset

Conversation

@lewing

@lewing lewing commented Jul 26, 2026

Copy link
Copy Markdown
Member

Fixes wrong wasm codegen that is usually masked by frame-layout coincidence.

The bug

lvaAssignFrameOffsetsToPromotedStructs computes mustProcessParams from a target list that does not include wasm:

#if defined(UNIX_AMD64_ABI) || defined(TARGET_ARM) || defined(TARGET_X86)
    const bool mustProcessParams = true;
#else
    const bool mustProcessParams = opts.IsOSR() || (info.compCallConv == CorInfoCallConvExtension::Swift);
#endif

    if (varDsc->lvIsStructField && (!varDsc->lvIsParam || mustProcessParams))

So for a dependently promoted field of a struct parameter, the SetStackOffset(parent + lvFldOffset) fixup below is skipped on wasm. The comment there assumes such fields are given offsets by lvaAssignVirtualFrameOffsetToArg, which is not true for wasm: parameters arrive in wasm locals and are homed by the prolog. The only other propagation site requires lvIsRegArg and is gated to TARGET_ARMARCH/TARGET_LOONGARCH64/TARGET_RISCV64, so it does not cover wasm either.

The field therefore keeps offset zero, and once the virtual-to-actual frame delta is applied it aliases frame + frameSize — one slot past the end of the frame, in the caller's.

Why it doesn't break everything

Two things have to line up for the bad offset to be observable:

  1. Codegen must reference the field local rather than the parent. That requires the address exposure to come from a path that does not dominate the use; when it dominates, codegen simply re-reads the parent and the wrong offset is never emitted.
  2. The aliased slot must not happen to hold the right value. frame + frameSize is the caller's frame base, which very often already holds the caller's own copy of the same struct — so the miscompiled load reads the correct value by luck. It only degrades when the caller is structurally unrelated.

DateTimeFormat.Format hits both: PrepareFormatU(ref dateTime) on the 'U' branch address-exposes the parameter (so its promoted _dateData field is dependently promoted and memory-homed) while the main path still sources the argument from the field, and it is reached through a thunk, so the stale slot is genuinely garbage.

Emitted code before the fix, in a 256-byte frame whose parameter home is at 248:

i64.store 0 248   ;; prolog homes the DateTime parameter (V01)
...
i64.load  0 256   ;; V153 = field V01._dateData  <-- reads past the frame
local.set 72
local.get 72      ;; passed as the DateTime argument

Every custom and standard DateTime format therefore read uninitialized memory and produced a constant wrong date.

Fix

Add TARGET_WASM to the gate. This is wasm-only by construction — the condition simply never included wasm — and no other target's behavior changes.

Validation

System.Text.Json.Tests under browser wasm R2R:

before after
run outcome aborted partway, no results completed
failed 216 (+ abort) 39
DateTime/DateTimeOffset failures 187 0
passed 59,284 / 59,365

Also verified on WASI R2R (same repro, same fix), and Microsoft.Bcl.Memory.Tests 550/550 and System.Runtime.CompilerServices.Unsafe.Tests 128/128 pass with R2R both off and on. A checked-configuration crossgen of System.Private.CoreLib is assert-free.

The 39 remaining failures are unrelated to this change: 25 involve Int128/UInt128 (a separate comparison issue — RoundtripValues<Int128>(-1) reports Expected: -1 Actual: -1), and two also fail under the interpreter.

Note on testing

A minimal repro reliably reproduces the wrong codegen but not a wrong observable value, for reason (2) above — I was unable to make the aliased slot hold garbage in a synthetic caller, including with a deliberately poisoned stackalloc. So the meaningful end-to-end coverage is the Utf8JsonWriter DateTime tests, which fail deterministically before this change. For codegen-level coverage the signal to assert on is the emitted load offset (or SuperPMI asm diffs), not program output.

Note

This pull request was created with the assistance of GitHub Copilot.

lvaAssignFrameOffsetsToPromotedStructs computes mustProcessParams from a
target list that did not include wasm, so for a promoted field of a struct
*parameter* the SetStackOffset(parent + lvFldOffset) fixup was skipped. The
comment there assumes such fields are given offsets by
lvaAssignVirtualFrameOffsetToArg, which is not true on wasm: parameters
arrive in wasm locals and are homed by the prolog. The other propagation
site requires lvIsRegArg and is gated to ARMARCH/LOONGARCH64/RISCV64, so it
does not cover wasm either.

The field therefore kept offset zero and, once the virtual-to-actual frame
delta was applied, aliased frame+frameSize -- reading off the end of the
frame into the caller's. It only shows up when codegen reads the field local
rather than the parent, which needs the address exposure to come from a path
that does not dominate the use.

DateTimeFormat.Format hits exactly that: PrepareFormatU(ref dateTime) on the
'U' branch address-exposes the parameter, so its promoted field _dateData is
dependently promoted and memory-homed, while the main path still sources the
argument from the field. Codegen emitted "i64.load 0 256 ;; V153" against a
256-byte frame whose parameter home was at 248, so every custom/standard
DateTime format read uninitialized memory and produced a constant wrong
date. That accounted for 187 Utf8JsonWriter DateTime/DateTimeOffset failures
under wasm R2R.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
Copilot AI review requested due to automatic review settings July 26, 2026 23:06
@azure-pipelines

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a WebAssembly-specific JIT frame layout issue by ensuring dependently promoted struct parameter fields get their stack offsets updated during lvaAssignFrameOffsetsToPromotedStructs, matching how other targets already handle cases where argument homes aren’t established via caller-allocated space.

Changes:

  • Extends the mustProcessParams gating in lvaAssignFrameOffsetsToPromotedStructs to include TARGET_WASM, so dependent promoted parameter fields have their offsets derived from the parent’s home.
  • Adds a WASM-specific comment explaining why parameter fields must be processed in this routine on WASM.

Comment thread src/coreclr/jit/lclvars.cpp
@lewing lewing added the arch-wasm WebAssembly architecture label Jul 27, 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.

@lewing
lewing merged commit 2aa2ce0 into dotnet:main Jul 27, 2026
144 of 146 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-VM-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants