[1] JSC OSR exit ScratchBuffer not scanned by GC
Severity: High | Component: JSC DFG/FTL OSR exit | 87b4375
Rated High because the diff fixes an attacker-reachable UAF on JIT-resumed JSCells whose only live reference was held in a scratch buffer the GC could not see; the PoC drives the trigger reliably and the freed type is selectable by web content, yielding the standard JSC stepping stone toward type confusion and arbitrary R/W in WebContent.
DFG and FTL OSR exits use ScratchBuffers when shuffling the stack during the exit itself. If the stack is overwritten, it is possible that the ScratchBuffer becomes the sole retainer of the previously on-stack pointers. These buffers are treated as conservative roots by the GC according to their activeLength, which the OSR exits were not setting. This patch fixes that by setting the activeLength around the stack-clobbering region in both DFG::OSRExit::compileExit and FTL::compileStub.
Source/JavaScriptCore/dfg/DFGOSRExit.cpp
- ScratchBuffer* scratchBuffer = vm.scratchBufferForSize(sizeof(EncodedJSValue) * operands.size());
+ const size_t scratchBufferSize = sizeof(EncodedJSValue) * operands.size();
+ ScratchBuffer* scratchBuffer = vm.scratchBufferForSize(scratchBufferSize);
...
+ // The scratch buffer can become the sole retainer of saved on-stack values if the
+ // stack is overwritten by emitSaveCalleeSavesFor below, so set the active length
+ // for the GC.
+ if (scratchBuffer) {
+ jit.move(CCallHelpers::TrustedImmPtr(scratchBuffer->addressOfActiveLength()), GPRInfo::regT0);
+ jit.storePtr(CCallHelpers::TrustedImm32(scratchBufferSize), CCallHelpers::Address(GPRInfo::regT0));
+ }
...
+ if (scratchBuffer) {
+ jit.move(CCallHelpers::TrustedImmPtr(scratchBuffer->addressOfActiveLength()), GPRInfo::regT0);
+ jit.storePtr(CCallHelpers::TrustedImm32(0), CCallHelpers::Address(GPRInfo::regT0));
+ }
JSTests/stress/osr-exit-scratch-buffer-gc.js
+// @requireOptions("--useConcurrentJIT=0", "--useZombieMode=1", "--slowPathAllocsBetweenGCs=16")
+function opt(s) {
+ const o = {};
+ try { return s + s; } catch { return o; }
+}
+function main() {
+ noDFG(main); noFTL(main);
+ for (let i = 0; i < 100; i++) opt("hello");
+ const s = 's'.repeat(0x40000000);
+ const a = [opt(s), opt(s), opt(s), opt(s), opt(s), opt(s), opt(s), opt(s)];
+ setTimeout(() => { a.toString(); }, 100);
+}
Patch Details
Each affected exit compiler now (1) hoists the scratchBufferSize computation into a named local before requesting the buffer; (2) emits JIT code that stores scratchBufferSize into scratchBuffer->addressOfActiveLength() immediately before the calls that overwrite the stack (emitSaveCalleeSavesFor in DFG and the equivalent stack reshuffle in FTL); and (3) emits JIT code that stores 0 back into addressOfActiveLength() after stack recovery completes. A regression test runs an OOMing s + s inside an OSR-compiled function with --slowPathAllocsBetweenGCs=16 and --useZombieMode=1 to force a GC inside the exit window.
Missing GC root registration for a temporary spill buffer that becomes the sole retainer across a stack-clobbering window.
Background
OSR exit is the runtime transition from a higher JIT tier (DFG/FTL) back to baseline when a speculative type check or other invariant fails; the exit compiler emits code that reconstructs the baseline stack frame from the optimised one. A ScratchBuffer is a VM-owned, fixed-region scratch area handed out by vm.scratchBufferForSize(); its activeLength field tells the GC how many leading bytes to treat as conservative roots. Conservative root scanning means the GC walks the words in a buffer and treats anything that looks like a valid cell pointer as a live root.
emitSaveCalleeSavesFor (DFG) and the equivalent FTL routine rewrite the JS stack frame in place during the exit. Two debug options enable the test trigger: --useZombieMode=1 retains freed cells in a poisoned state so dereferences of stale pointers fault reliably, and --slowPathAllocsBetweenGCs=16 triggers a GC every N slow-path allocations.
Analysis
Both DFG::OSRExit::compileExit and FTL::compileStub allocated a ScratchBuffer and spilled live on-stack EncodedJSValues into it, but never wrote scratchBufferSize into the buffer's activeLength. The GC therefore saw zero scannable bytes in the buffer. The exit then called emitSaveCalleeSavesFor (DFG) or the analogous FTL reshuffle, which overwrites the original on-stack slots. For the window between the stack being clobbered and the values being restored, the scratch buffer was the sole live retainer of those object references — yet invisible to the marker.
Aaa Aaa Aaaaaaaaaaa Aa Aaaaaaaaa Aaaaa Aa Aaaaaaaaaaaa Aa Aaaaaaa Aaaaa Aaa Aaaaaaa Aaaa Aa Aaa Aaaaaaaaaaaa Aaaaaa Aaaa Aaaa Aaaa Aaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaaaaaa Aa a Aaa Aaaaa Aaaaaaaaa Aaaaaaaaaaaa Aaaaaa Aaaaa Aaa Aaaaaaa Aaaaaaaa Aaa Aaaaaaa Aaaaaa Aaaa Aaaaaaa Aaa Aaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaa Aaaa Aaaaaa Aa Aaa Aaaa Aaa Aa Aaaaaaaaa Aaaaaa Aaaa Aa Aaaaaa Aaaa Aaa Aaaaaaaaa Aaaaa Aaaaaaaaaa Aaaa Aaa Aaaaaaa Aaaa Aaa Aaaaaaa Aaaaaaa Aaaaaa Aaa Aaaaa Aaa Aaaaa Aa Aaaaaaaaaa Aa Aaa Aaaaaaa Aaaaaa Aa Aaa Aaa Aaaa Aaaaaa Aa Aaaaaaaa Aa Aaaaa Aaaaaa a Aaaaaaaaa Aaaaaaaaaa Aaaaaa Aaa Aaaaaa Aaaaaaaaaaaaaaa Aaaa a Aaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaa Aa Aaa Aaaaaaaaa Aaaaa Aaaaaaaa Aaaa Aaa Aaaaaaaaaaa Aaaa a Aaaaa a Aaa Aaa Aa Aaaaa Aaa Aaaaaa Aaaa Aaaaaaaaaaaaa Aa Aa Aaa Aaaa Aaa Aaa Aaaa Aaa Aa Aaaaaaaaaa Aaa Aaaa Aaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaa Aaaaaaa Aaaa Aaaa Aaa Aaaaaaaa Aaaaaa Aaa Aaaa Aa Aaaaaaa Aaaaa Aaaaaaaa Aaaaaaaaaa Aaa Aaaaaaaaaaaaaa Aaaaa Aaaaa Aaaa Aaaaaaaaaaaa Aaa Aaaaaaaaaaa Aaaaaaa Aa a Aaaa Aaaaaa Aaa Aaaaa Aaaa Aaaaa Aaaa Aaaa Aaaaaaaaa Aaa Aaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaa
Aa Aaa Aaaaaaaa Aaaaaaaa Aaa Aaaaa Aaaa Aaaa a Aaaaaa Aaaaaaaaaaaaaaaa Aaaa Aaaaaa Aaa Aaaaaaa Aaaaaaaa Aaaaa Aaaa Aaa Aaaa Aaaaa Aaaa a Aaaaaaaaaaaaaa Aaaaaaaaa Aaaa a Aaaaaaaa a Aaa Aaaaaaa Aaa Aaaaaaaa Aaaaa Aa Aaaaaaaaa Aaaa Aaa Aaaaaaaaa Aaaaa Aaaaaa Aaaaaaaaaaa Aaa Aaaaaaa Aaa Aaaaaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaa Aaaaaaaa Aaaaaaaaaa Aaaa Aaaaaaaaa Aaaa Aaa Aa Aaa Aaaa Aaaaa Aaaaaaa a Aaaaaaaa Aaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaaaaaaaaa Aa Aaaaaaaaa Aaa Aaaaaaaaa Aaaa Aaa Aaaa Aaaaa Aaaa Aaaa Aaaaaaa Aa Aa Aa Aaa Aaaaaa Aaaaaaaaaaaaaaa Aaaaaaa Aaaaaaa Aa Aaa Aa Aaa Aaa Aaaaaaaa Aa Aaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaa Aaaaaaaa Aaaaa Aa Aaa Aaa Aaaaaaaaaa Aaaaaaa Aaaaaa Aaa Aaa Aaaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaa Aaaaaa Aaa Aaa Aaa Aaaaaaa Aaa Aaaa Aaaaaaaa Aaaa Aaa Aaaaaaa a Aaaa Aaaaaaaaaaa Aaaaa Aaaa Aaaa a Aaaaaaaaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaa Aaaaaaaa
🔒A detailed walkthrough of how the OSR exit window between stack-spill and stack-restore becomes a UAF, including the conditions an attacker needs to align to weaponise it
Subscribe to read more
Audit directions
a Aaaaaaaaaaaaa Aaaa Aaaa Aaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaa Aaaa a Aaaaaaaa Aaaaaaa Aaaaaa Aaaaaa a Aaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaa Aaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaa Aaa Aaaaaa Aaaaa Aaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaa Aa Aa Aaa Aaa Aaaaa Aaa Aaaaaa Aa Aaaaaaaaaa
a Aaaaaaaaaaaaaaa Aaaaaaaa Aaaa Aaaa Aaaa Aaaaaa Aaaaaaa a Aaaaaaaaa Aaaaaaaa Aaaaa Aaaaa Aaaa Aaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aa Aaa Aaaaaa Aaaa Aaaaaaaaaa Aaa Aaaaaaa Aaaaa a Aaaaaa Aaaa Aaaa Aaaa Aaaaaaa Aaaaa Aa Aaaaaa Aaaaaa Aaa Aaaaaaa Aaaaaa Aaaaa Aaaaa Aaaaa Aa Aaaaaaaaaa Aaaaaaa Aa Aaaaaaaaaa Aaaaaaaaaa Aa Aaa Aaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a Aaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaa Aa Aaaa Aaa Aa Aaa Aaaaaaaaa Aaa Aa Aaaaaaaaaaa Aaaaa Aaaa Aaaaa Aaa Aaaa Aaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaa Aaaaaaaa a Aaa Aaa Aaaa Aaaa Aaaaaa Aa Aaaaaaaaaaaaaa Aaaaaaa Aaaa Aaaaaaa Aaaaaaaaaaaaaa Aa Aaaaaaaa
a Aaaaaa Aaaaaaa Aaa Aaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaa Aaaaaa Aaa Aaaaaaaa Aaaaaaa Aaaaa Aaaaaa Aaaa Aaaaaaaaaaa Aaa Aaaaa Aaaaaaa Aaa Aaaa Aaa Aa Aaaaaaaaaaaaaa Aa Aaaa Aa Aaaa Aaaaaa a Aaaaaaaaaaaa Aaaaaaaaaaa
🔒Four reusable audit patterns covering JIT-spill / GC-visibility omissions, with concrete grep targets and analogous subsystems to inspect
Subscribe to read more