← All reports

[Wasm] OMG tail call patchpoint needs to clobber late pinned registers

JSC WebAssemblyOther

Component: JSC WebAssembly | c18d1e3

Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

// (OMGIRGenerator::emitIndirectCall, root-tail path)
// restored the late-clobber declaration on the pinned registers used for
// cross-instance memory reload in return_call_ref/return_call_indirect,
// so B3 keeps live late inputs (boxed callee, shuffled args) out of
// wasmBaseMemoryPointer/wasmBoundsCheckingSizeRegister.

JSTests/wasm/stress/omg-indirect-tail-call-late-input-clobber.js

// builds a relay module that return_call_ref's into a 24-arg target
// through a 12-arg caller, forcing register pressure so B3 would pick
// pinned registers for late inputs if not marked as late-clobbered.

WebKit's OMG tier JIT-compiles WebAssembly through B3, whose pinned registers hold the current instance's linear memory base pointer and bounds-check size so memory accesses can be fast and branch-light. Tail calls (return_call_ref / return_call_indirect) must switch to a different Wasm instance and reload those pinned registers before jumping, since the callee has its own memory. B3's register allocator relies on late clobber annotations to know that a patchpoint destroys a register's value only after normally-scheduled uses complete — without the annotation, the allocator assumes it is safe to also use that register for other live values needed at the same point, such as tail-call arguments. This commit restores the missing late-clobber declaration on the OMG tail-call patchpoint and adds a register-pressure-heavy regression test.

Tail-call patchpoint execution order:
  1. reload wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister from callee instance
  2. run parallel-move shuffle (places boxed callee + args into registers)
  3. jump to callee

Bug: without late-clobber flag, B3 may allocate a live late input
(callee ref / arg) into the pinned register BEFORE step 1 overwrites it
  → shuffle in step 2 reads a corrupted value
  → callee runs with wrong bounds-check register

The bounds-checking register could be corrupted right before it is used to validate memory accesses in the tail-called function, turning a register-allocation bug into a potential out-of-bounds access primitive in JIT-compiled Wasm. This is the textbook shape of a register-allocator correctness bug with direct memory-safety consequences: the value the callee trusts to bound its linear-memory accesses is the one the allocator was free to reuse.

Narrow: review the other B3 patchpoints that reload or clobber pinned registers — regular calls, exception handling, stack switching — for the same missing late-clobber annotation; the code-review tell is a patchpoint that writes wasmBaseMemoryPointer or wasmBoundsCheckingSizeRegister in its generator lambda without a corresponding late-clobber declaration on the patchpoint object. Wider: the same class covers any patchpoint whose generated code mutates a register the surrounding IR still considers live, not just the pinned pair — audit patchpoints that spill, scratch, or reload callee-saved state, and pair the review with fuzzing of register-pressure-heavy call shapes (many arguments, nested tail calls) around OMG tier-up, since the bug only manifests when the allocator actually runs out of free registers. Widest: the invariant is that any hand-written machine-code region embedded in an optimizing compiler must declare its full clobber set, because the allocator's correctness argument is entirely derived from those declarations — it transfers to LLVM inline-asm clobber lists, V8's Turbofan CallDescriptor register constraints, and any JIT with a patchpoint or stub-call abstraction.