← All reports

JSC B3-to-Air lowering used a locked value as a scaled index

The wasm bounds check was fine — it just described a different address.

Component: JavaScriptCore B3/Air JIT | 10d4d20

B3 is JSC's optimizing IR, sitting between DFG IR and Air — JSC's assembly-level IR — and it backs both the FTL top-tier JIT for JavaScript and WebAssembly compilation. The LowerToAir pass processes blocks in pre-order and aggressively folds sequences of operations into single complex instructions wherever the target ISA allows, for example folding a left-shift that represents a scale factor directly into a base + index*scale + offset addressing mode rather than emitting a separate shift. During that same pass, on ARM64 a value can be locked — tracked in m_locked — once it has been committed into a fused instruction, such as a BitAnd or ZExt32(Trunc) folded into a UBFIZ; a locked value no longer has any instruction independently defining a Tmp for it.

This commit fixes the WasmAddress case, which folded a pointer-width Shl into a scaled-index addressing mode without first checking whether the Shl's child value was already locked. When it was, the fold referenced a Tmp with no defining instruction, and the memory access was emitted with an undefined index register.

  Before:                                  After:
  Shl(x, k)  ── already locked into UBFIZ   Shl(x, k)  ── already locked
    │                                          │
    └─► WasmAddress folds Shl into             └─► WasmAddress checks m_locked
        [base + Tmp(x)*scale]                        └─► falls back: emit the
              ▲ no defining instruction                   shift, use a real Tmp
        bounds check validated a               bounds check and access now
        different expression                  agree on the same address

The wasm bounds check validated the correct pointer expression while the load or store used an addressing mode built from an undefined register. That divergence between the checked address and the accessed address is a well-known shape for an out-of-bounds primitive in JIT-compiled sandboxed code — the guard is not bypassed so much as rendered irrelevant, because it no longer describes the access that follows it.

Narrow: every other LowerToAir case that folds a child value into an addressing mode or a complex instruction needs the same m_locked consultation, and the match tell is a fold site that reads a child's Tmp without a preceding lock query. Wider: the general shape to hunt is any single-pass IR lowering where one rule's decision to consume a value invalidates an assumption a later rule makes about that value being independently materialized — locking is JSC's name for it, but every instruction-selection pass with fusion has an equivalent, and the sites at risk are exactly those where two rules can both claim the same child. Widest: in bounds-checked JIT code specifically, any transformation that rewrites the address expression after the check has been emitted deserves an explicit argument for why the two still denote the same value; the review tell is a check emitted against an IR node and an access emitted against a lowered addressing mode, with a fold in between.