CVE-2026-28903
Overview
Background
- Air / B3 backend
- JavaScriptCore’s low-level JIT IR (Air) and optimizer that allocates registers and rewrites instructions.
- fixObviousSpills
- An Air pass that replaces spilled stack-slot operands with equivalent registers/constants it knows are aliased.
- Early vs late def
- An early def writes its result before the instruction reads its inputs (clobbering at the early point), while a late def writes after; alias state must clobber each at the right time.
- Aliasing/spill state
- The per-point set of reg/slot/constant equalities fixObviousSpills uses to justify operand substitutions.
Root Cause Analysis
fixObviousSpills is an Air (B3 backend) optimization that tracks which registers, stack slots and constants alias each other so it can replace spilled-slot operands with registers/immediates. Its per-instruction dataflow originally used a single executeInst() that, for each instruction, clobbered all of the instruction’s defs and then added the instruction’s aliases — treating every def uniformly. That ignores the distinction between an instruction’s early defs (registers/slots written before the instruction consumes its inputs, i.e. clobbered at the ’early’ point) and its late defs. Because early defs were not clobbered before fixInst() rewrote the instruction’s arguments, the alias state could still believe a register or slot held a value that the instruction’s early def had already overwritten, so fixInst could replace an operand with a register/constant that no longer holds the expected value. That is a miscompilation: the optimized code reads a stale aliased value.
The fix splits def clobbering into clobberEarlyDefs() (clobber before fixInst) and clobberLateDefs() plus addInstAliases() (after), implemented via clobberDefs(prevInst, nextInst) calling forEachDefWithExtraClobberedRegs/forEachDef with the early (nullptr,&inst) and late (&inst,nullptr) roles; the fixCode loop now does clobberEarlyDefs(); fixInst(); clobberLateDefs(); addInstAliases().
The restored invariant is that the aliasing/spill state accounts for early defs before the instruction is fixed, so no operand is rewritten to a value invalidated by an early def.
Attack Path
- Shape hot code with early-def instructions Run JS (e.g. DataView/ArrayBuffer arithmetic) that the JIT compiles through B3/Air producing instructions with early-def operands and obvious spills.
- Trigger the miscompilation fixObviousSpills replaces an operand with a register/slot/constant it wrongly believes is still live across an early def, baking a stale value into the compiled code.
- Use the wrong value The optimized function computes on the stale aliased value (e.g. an index/length), producing results inconsistent with the intended semantics.
- Corrupt memory A miscomputed index/bound turns into an out-of-bounds or type-confused access in JIT code in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
FixObviousSpills::fixCode / runSource/JavaScriptCore/b3/air/AirFixObviousSpills.cpp |
modified | Reworks the per-instruction sequence to clobberEarlyDefs() before fixInst(), then clobberLateDefs() and addInstAliases(), replacing the single executeInst(). |
FixObviousSpills::clobberDefs / clobberEarlyDefs / clobberLateDefs / addInstAliasesSource/JavaScriptCore/b3/air/AirFixObviousSpills.cpp |
added | Split def-clobbering into early vs late via forEachDefWithExtraClobberedRegs(prevInst,nextInst,...), so early defs are clobbered from the alias state before instruction fixing. |
Files Changed
JSTests/stress/fixobviousspills-earlydefs.jsSource/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
Audit Directions
- Other passes ignoring early defsgrep Air passes for forEachDef usage that doesn’t distinguish early vs late roles (Arg::Role early/late) where operand rewriting or liveness depends on it.
- Alias/spill state consumersAudit code around m_state.clobber/addAlias in fixObviousSpills and sibling passes (e.g. lowerAfterRegAlloc) for ordering of clobber vs operand substitution.
- forEachDefWithExtraClobberedRegs callersReview callers passing (&inst,&inst) that should instead split into (nullptr,&inst) early and (&inst,nullptr) late to model clobbering correctly.