[Wasm] OMG tail call patchpoint needs to clobber late pinned registers
Component: JSC WebAssembly | c18d1e3
Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
JSTests/wasm/stress/omg-indirect-tail-call-late-input-clobber.js
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
Significance
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.
Audit directions
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.