← 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.

🔒

Detailed look at how an aliasing quirk in baseline JIT register allocation silently disarms a GC invariant — and what that means for renderer-process exploitability.

Subscribe to read more

🔒

Four reusable audit patterns identified, with concrete baseline-JIT emitters and IC paths to investigate for the same class of barrier bug.

Subscribe to read more