055680a255 [JSC] Track customSlotBase for CustomAccessorGetter/CustomAccessorSetter
Triage note: Adds the custom-accessor cases to GC dependent-cell visiting with a gc()-based regress test, fixing a use-after-free on a collected custom slot base.
Contents
The bug at a glance
This is a use-after-free in JavaScriptCore’s polymorphic inline cache machinery: the GC’s dependent-cell walk omitted the custom slot base for CustomAccessorGetter/CustomAccessorSetter cases, so a JSObject serving as the accessor’s slot base could be collected while a still-live AccessCase referenced it. A UAF on a JSCell in the JIT/IC path is a classic renderer memory-corruption primitive that historically leads to type confusion and RCE, which justifies a high real-world severity even though the metadata labels it medium. The included gc()-driven regression test and the $vm custom getter/setter harness show the condition is deterministically reachable.
AccessCase::forEachDependentCell is the single point where the GC learns which cells an inline-cache stub keeps alive. The CustomAccessorGetter/CustomAccessorSetter cases were listed in the no-op fall-through group rather than the group that visits customSlotBase(), so the object holding the accessor slot was invisible to the collector. The fix simply moves those two case labels into the CustomValueGetter/CustomValueSetter block that reports customSlotBase().
Root cause
When JSC compiles a property access into a polymorphic inline cache, it records an AccessCase describing how to service that access. For custom accessors (native getter/setter functions installed via the CustomGetterSetter mechanism, e.g. from $vm.createCustomTestGetterSetter or from host bindings), the AccessCase is a GetterSetterAccessCase that carries a customSlotBase() – the JSObject the native accessor should be invoked against. That slot base is a heap JSCell whose lifetime must be at least as long as the stub that references it.
Garbage collection in JSC is precise: every structure that retains a JSCell must report it during marking. For inline caches, AccessCase::forEachDependentCell is the callback the collector uses to discover the cells an individual case depends on (the structure, the prototype chain, the getter/setter object, etc.). Before this patch, the switch in forEachDependentCell handled CustomValueGetter and CustomValueSetter by calling functor(accessor.customSlotBase()), but CustomAccessorGetter and CustomAccessorSetter were grouped with Load, LoadMegamorphic, StoreMegamorphic and the other cases in the trailing block that does nothing. As a result, a custom-accessor stub’s slot base was never marked on its behalf.
The UAF window opens when the slot base is reachable only through the inline cache. In the regress test, an object is given a prototype produced by $vm.createCustomTestGetterSetter(), an opt() function repeatedly reads x.customAccessor to polymorphically warm and JIT the inline cache (jitPolicyScale=0.001 forces early compilation), then the object is dropped (obj = null) and gc() is invoked. If the only remaining strong reference to the custom-accessor slot base flows through the AccessCase, the collector frees it. A subsequent execution of the stub – reachable by continuing to invoke opt on fresh objects that hit the same cache – then dereferences a dangling JSCell, i.e. reads a freed object’s structure/vtable as if it were the live slot base.
Because the reclaimed slot is a JSCell, an attacker who can reallocate that memory with a controlled object before the stub reuses it turns the dangling read into a type confusion: the custom accessor is invoked with a slot base of attacker-chosen shape. That is the standard path from a JSC IC dependent-cell omission to controlled memory disclosure or corruption. The patch closes the window by making the two custom-accessor cases report customSlotBase() to the collector exactly as the custom-value cases already did.
Key code
AccessCase::forEachDependentCell now reports customSlotBase() for custom accessors (Source/JavaScriptCore/bytecode/AccessCase.cpp)
case CustomValueGetter:
case CustomValueSetter:
case CustomAccessorGetter:
case CustomAccessorSetter: {
auto& accessor = this->as<GetterSetterAccessCase>();
if (accessor.customSlotBase())
functor(accessor.customSlotBase());
Patch walkthrough
Source/JavaScriptCore/bytecode/AccessCase.cpp— In AccessCase::forEachDependentCell, the CustomAccessorGetter and CustomAccessorSetter case labels are moved out of the do-nothing trailing group and merged into the CustomValueGetter/CustomValueSetter block, so that block’s body – fetching as<GetterSetterAccessCase>() and, when accessor.customSlotBase() is non-null, calling functor(accessor.customSlotBase()) – now runs for custom accessors too. This makes the GC visit the slot base and keep it alive for the lifetime of the stub.JSTests/stress/regress-172736082.js— Added regression test. It builds a polymorphic inline cache on a custom accessor, warms it via opt() under aggressive JIT settings, drops the last script reference to the accessor’s object, forces gc(), then re-exercises the cache. Before the fix the collected slot base is used after free; after the fix it stays live.
Background
AccessCase / polymorphic inline cache — JSC caches property accesses as a list of AccessCase objects, each describing one shape (structure) it can handle and how to load/store or invoke an accessor. These stubs live independently of the objects that produced them, so any JSCell a stub needs at execution time must be kept alive by the GC through the stub, not merely by script references.
forEachDependentCell — The method the garbage collector calls on each AccessCase to enumerate the heap cells the case depends on – structures, prototypes, and for accessor cases the getter/setter object and its slot base. Any cell not reported here is invisible to marking and can be collected while the stub still points at it, producing a use-after-free the next time the stub runs.
customSlotBase() — For GetterSetterAccessCase instances backing custom (native) accessors, customSlotBase() is the JSObject the native getter/setter is invoked against. It may differ from the base object of the access when the property is inherited, so it is a distinct strong dependency that must be tracked separately.
CustomAccessorGetter vs CustomValueGetter — Both represent native property implementations. CustomValue* pass the property value through a slot, while CustomAccessor* call a function-style accessor. They share GetterSetterAccessCase and the customSlotBase concept, which is why the value cases already tracked the slot base and the accessor cases needed the same treatment – the omission was an inconsistency, not a semantic difference.
$vm.createCustomTestGetterSetter — A testing hook exposed under –useDollarVM that installs a native custom getter/setter (exposing the customAccessor property) on an object. It lets the regression test create exactly the CustomAccessorGetter inline-cache shape whose slot base was previously untracked, without needing a real host binding.
Vulnerability window
- Introduction — The custom-accessor cases were placed in the inert group of AccessCase::forEachDependentCell while the custom-value cases visited customSlotBase(), leaving the slot base of custom accessors untracked by the collector.
- Latent exposure — Any code path that built an inline cache on a native custom accessor whose slot base was reachable only through the stub created a candidate for premature collection.
- Discovery — Filed as bug 310293 / rdar://172736082 (originally landed on a safari-7624 branch as 305413.557, rdar://176061688), indicating internal/security-triage origin rather than a public functional report.
- Reproduction — A stress test using $vm custom getter/setters, aggressive JIT policy, prototype swapping to force polymorphism, then obj=null;gc() reliably frees and reuses the slot base.
- Fix — Committed as 315328@main: the two custom-accessor labels join the customSlotBase()-visiting block, so the GC keeps the slot base alive for the stub’s lifetime.
Proof of concept
The added JSTests/stress/regress-172736082.js. It warms a polymorphic inline cache on the native customAccessor property (slot base supplied by $vm.createCustomTestGetterSetter), drops the object and calls gc() to free the untracked slot base, then re-runs opt() to re-enter the stub. Under the buggy build this executes against a freed JSCell; it is a crash/UAF reproducer, not a full exploit.
//@ runDefault("--useDollarVM=1", "--useConcurrentJIT=false", "--jitPolicyScale=0.001")
function main() {
function createPoly() {
function f() {}
const object = new f();
object.__proto__ = {};
return new f();
}
$vm.createCustomTestGetterSetter();
for (let i = 0; i < 50; i++) {
createPoly();
}
let obj = createPoly();
obj.__proto__ = $vm.createCustomTestGetterSetter();
function opt(x) {
return [x.customAccessor, Math.random(), Math.random(), Math.random(), Math.random(), Math.random()];
}
for (let i = 0; i < 1000; i++) {
opt(obj);
}
obj = null;
gc();
$vm.createCustomTestGetterSetter();
for (let i = 0; i < 10; i++) {
opt({});
}
}
main();
Exploitation
- Prime the cache — Repeatedly read a native custom-accessor property so a GetterSetterAccessCase with a customSlotBase is compiled into a polymorphic inline cache, making the slot base reachable only through the stub.
- Free the slot base — Drop all script references to the accessor’s slot-base object and trigger GC. Because forEachDependentCell did not report it, the collector reclaims it while the stub survives.
- Reallocate (spray) — Allocate attacker-controlled JSCells to reoccupy the freed slot, aiming to place a fake structure/vtable where the stub expects the real slot base. This step is inferred, not demonstrated by the test.
- Re-enter the stub — Invoke the accessor again so the stub uses the dangling (now attacker-shaped) slot base, yielding type confusion and controlled disclosure or corruption. The public artifact only reaches the crash; full exploitation is inferred.
Detection & hunting
For defenders and SOC / detection engineers:
- ASan/GC-zombie crash in the IC path —
- Verifier assertions on dependent cells —
- Correlated custom-accessor + collection workload —
Audit directions
- Every switch in forEachDependentCell —
- Other per-stub cell enumerators —
- customSlotBase producers —
- Structure/prototype dependencies —