← All issues

[1] JSC OSR exit ScratchBuffer not scanned by GC

Severity: High | Component: JSC DFG/FTL OSR exit | 87b4375

GC가 인식하지 못하는 scratch buffer에만 live reference가 남아있는 JIT-resumed JSCell에서 attacker가 접근 가능한 UAF를 수정하는 패치입니다. PoC는 이 trigger를 안정적으로 재현하며, 해제되는 type은 web content가 선택할 수 있습니다. 그 결과 type confusion 및 WebContent 내 arbitrary R/W로 이어지는 표준 JSC stepping stone이 확보됩니다.

DFG와 FTL의 OSR exit은 exit 과정에서 스택을 재배치할 때 ScratchBuffer를 사용합니다. 스택이 덮어쓰이면 ScratchBuffer가 기존에 스택에 있던 포인터들의 유일한 보유자가 될 수 있습니다. 이 buffer들은 activeLength 값을 기준으로 GC의 conservative root로 처리되는데, OSR exit에서 이 값을 설정하지 않고 있었습니다. 이번 패치는 DFG::OSRExit::compileExitFTL::compileStub 모두에서 스택 덮어쓰기 영역 전후로 activeLength를 설정하여 이 문제를 수정합니다.

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);
+}

각 영향받는 exit compiler에는 세 가지 변경이 이루어졌습니다. 첫째, buffer를 요청하기 전에 scratchBufferSize 계산을 별도의 named local로 분리합니다. 둘째, 스택을 덮어쓰는 호출(DFG의 emitSaveCalleeSavesFor 및 FTL의 동등한 stack reshuffle) 바로 직전에 scratchBufferSizescratchBuffer->addressOfActiveLength()에 저장하는 JIT code가 추가됩니다. 셋째, 스택 복구가 완료된 후 addressOfActiveLength()0을 다시 저장하는 JIT code가 추가됩니다. Regression test는 --slowPathAllocsBetweenGCs=16--useZombieMode=1 옵션을 조합하여 OSR-compiled function 안에서 OOM을 발생시키는 s + s를 실행하고, exit window 내에서 GC를 강제로 유발합니다.

스택 덮어쓰기 구간에서 유일한 retainer가 되는 임시 spill buffer의 GC root 등록 누락.

OSR exit은 speculative type check나 다른 invariant가 실패했을 때 상위 JIT tier(DFG/FTL)에서 baseline으로 복귀하는 런타임 전환입니다. exit compiler는 최적화된 스택 프레임에서 baseline 스택 프레임을 재구성하는 코드를 생성합니다. ScratchBuffervm.scratchBufferForSize()를 통해 제공되는 VM 소유의 고정 크기 scratch 영역입니다. activeLength 필드는 GC에게 앞부분 몇 바이트를 conservative root로 처리할지 알려줍니다. Conservative root scanning은 buffer의 각 word를 순회하며 유효한 cell pointer처럼 보이는 값을 live root로 처리하는 방식입니다.

emitSaveCalleeSavesFor(DFG)와 FTL의 동등한 루틴은 exit 과정에서 JS 스택 프레임을 in-place로 재작성합니다. 테스트 trigger를 활성화하는 debug 옵션은 두 가지입니다. --useZombieMode=1은 해제된 cell을 poisoned 상태로 유지하여 stale pointer 역참조 시 항상 동일하게 fault가 발생하도록 합니다. --slowPathAllocsBetweenGCs=16은 N번의 slow-path allocation마다 GC를 유발합니다.

DFG::OSRExit::compileExitFTL::compileStub 모두 ScratchBuffer를 할당하고 live on-stack EncodedJSValue들을 spill했지만, buffer의 activeLengthscratchBufferSize를 기록하지 않았습니다. 결과적으로 GC는 해당 buffer에서 스캔 가능한 바이트가 0이라고 인식했습니다. 이후 exit은 emitSaveCalleeSavesFor(DFG) 또는 FTL의 동등한 reshuffle을 호출하여 원래 on-stack 슬롯을 덮어씁니다. 스택이 덮어써진 시점부터 값이 복원되기까지의 window 동안, scratch buffer는 해당 object reference의 유일한 live retainer였지만 GC marker에게는 보이지 않는 상태였습니다.

🔒

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

더 확인하려면 구독해 주세요

🔒

Four reusable audit patterns covering JIT-spill / GC-visibility omissions, with concrete grep targets and analogous subsystems to inspect

더 확인하려면 구독해 주세요