← All issues

[4] [JSC] Add write barrier to op_del_by_id/op_del_by_val in baseline JIT

Severity: High | Component: JSC Baseline JIT | 1cdc540

Rated High because the diff closes a missing generational write barrier in baseline op_del_by_* whose inline cache transitions the base cell's Structure without barriering; an eden GC after the bytecode runs can sweep the freshly stored Structure, producing a dangling Structure* reachable from a live JSCell.

The barrier emission is moved before the result write to dst, and the ShouldFilterBase mode is replaced with WriteBarrierForCellState so the barrier reads the cell from the IC slot rather than re-reading base from the (now-overwritten) virtual register.

Source/JavaScriptCore/jit/JITPropertyAccess.cpp

- emitPutVirtualRegister(dst, resultJSR);
- emitWriteBarrier(base, ShouldFilterBase);
+ emitWriteBarrier(/*owner=*/baseGPR, /*value=*/InvalidGPRReg, WriteBarrierForCellState);
+ emitPutVirtualRegister(dst, resultJSR);

Missing generational write barrier on a JSCell whose Structure pointer was just transitioned by the property-deletion inline cache.

In both emit_op_del_by_id and emit_op_del_by_val, the barrier is emitted before the result is boxed into dst. The owner is the base cell register from the IC, not the virtual register base, which on o = delete o.x aliases dst.

JSC's generational GC requires every old-gen → new-gen pointer write to be remembered. The inline cache emitted by op_del_by_* may transition the base object's Structure (the comment in source confirms "IC can write new Structure without write-barrier if a base is cell"); the JIT is then responsible for the barrier. emitWriteBarrier(base, ShouldFilterBase) reads the base virtual register, checks it is a cell, then performs the barrier.

The pre-fix sequence for o = delete o.x was: (1) IC fast path that may transition the Structure of o; (2) boxBoolean(resultJSR); (3) emitPutVirtualRegister(dst, resultJSR) overwriting o with a boolean; (4) emitWriteBarrier(base, ShouldFilterBase) re-reading the virtual register. Because base and dst refer to the same local register holding o, step (4) reads the boolean. ShouldFilterBase's branchIfNotCell early-exit then skips the barrier entirely.

The result is a JSCell whose Structure was just transitioned to a new (young) Structure object, with no entry in the remembered set. After an eden GC the young Structure can be considered unreachable from any old-gen root and reclaimed. The live JSCell now holds a dangling Structure*; any subsequent property access through that pointer dereferences freed memory.

This weakens the GC generational invariant for property deletion on hot bytecode. The exploit primitive is UAF on the Structure pointer of a script-reachable object, an extensively documented seed for JSC exploitation.