Part 1 assumed a type confusion fell out of the sky. This part is where it usually comes from on a modern engine: not the interpreter, but the optimizing JIT. The compiler reasons about types and ranges to delete "redundant" checks — and if you can make its reasoning wrong, it deletes a check that was load-bearing.
- component
- the optimizing JIT (V8 TurboFan / JSC DFG-FTL)
- bug
- a typer/range miscalculation -> an eliminated bounds check
- yield
- OOB read/write on a JS array's backing store
how the JIT earns its bugs#
To make hot JavaScript fast, the optimizer speculates: it watches the types you
actually pass, assumes they'll keep coming, and compiles specialized code guarded
by cheap checks. Crucially, it runs a typer that tracks the possible range/type
of every value, and a bounds-check elimination pass that removes
CheckBounds nodes it can prove are always in range.
That proof is only as sound as the typer. If a single operation's modeled range is
wrong — too wide, too narrow, missing a corner like -0 or NaN — the optimizer
can "prove" an index is in bounds when it isn't, and drop the check. The compiled
code then indexes the backing store with no guard at all.
|
Real instances of the pattern read like a museum of subtle arithmetic:
- CVE-2018-17463 — V8's
JSCreateObject/Object.createtyper returned a type that let later range analysis go wrong. - The
Math.expm1-0bug — the typer modeled the result as plainNumberand missed that it could be-0, breaking downstream range math. Array.prototypelength-tracking bugs — the optimizer's view of an array's length drifting from reality after a side-effecting callback.
Different functions, identical consequence: a CheckBounds removed on the strength
of a range the typer got wrong.
from OOB to the primitives#
The eliminated check gives you an out-of-bounds read/write on a backing store. You groom the heap so that just past your victim array sits another array's length field or a sibling array of a different elements-kind. Now:
- OOB-write the neighbour's length → a giant array → relative R/W over the heap;
- or OOB across a
PACKED_DOUBLEand aPACKED(object) array so the two alias — which is exactly the float/object confusion that buildsaddrof/fakeobj.
a removed bounds check, an out-of-bounds array
the defenders' moves#
- Verifier / typer hardening. Each of these bugs is met with a typer fix, plus fuzzers (Fuzzilli) hunting the next range error. It's a grind, not a wall.
- Hardening the consequence. V8 added bounds checks that resist typer lies in sensitive spots, and the heap sandbox (next part) limits what an OOB even reaches.
- JIT mitigations broadly — but the typer is enormous surface, and a single wrong range is still a full compromise.
The lesson that makes JIT bugs feel different from heap bugs: you're not corrupting memory directly, you're corrupting the compiler's beliefs, and letting it corrupt memory for you with the bounds check it was sure it didn't need. Get the OOB, reach the float/object confusion, and part 3 turns it into read/write over the entire address space.