[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::compileExit와 FTL::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);
+}
Patch Details
각 영향받는 exit compiler에는 세 가지 변경이 이루어졌습니다. 첫째, buffer를 요청하기 전에 scratchBufferSize 계산을 별도의 named local로 분리합니다. 둘째, 스택을 덮어쓰는 호출(DFG의 emitSaveCalleeSavesFor 및 FTL의 동등한 stack reshuffle) 바로 직전에 scratchBufferSize를 scratchBuffer->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 등록 누락.
Background
OSR exit은 speculative type check나 다른 invariant가 실패했을 때 상위 JIT tier(DFG/FTL)에서 baseline으로 복귀하는 런타임 전환입니다. exit compiler는 최적화된 스택 프레임에서 baseline 스택 프레임을 재구성하는 코드를 생성합니다. ScratchBuffer는 vm.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를 유발합니다.
Analysis
DFG::OSRExit::compileExit와 FTL::compileStub 모두 ScratchBuffer를 할당하고 live on-stack EncodedJSValue들을 spill했지만, buffer의 activeLength에 scratchBufferSize를 기록하지 않았습니다. 결과적으로 GC는 해당 buffer에서 스캔 가능한 바이트가 0이라고 인식했습니다. 이후 exit은 emitSaveCalleeSavesFor(DFG) 또는 FTL의 동등한 reshuffle을 호출하여 원래 on-stack 슬롯을 덮어씁니다. 스택이 덮어써진 시점부터 값이 복원되기까지의 window 동안, scratch buffer는 해당 object reference의 유일한 live retainer였지만 GC marker에게는 보이지 않는 상태였습니다.
Aaaa Aaa Aaaaa Aaaaaa Aa Aaaaaa Aaaaa Aaaaaaaaaaaa Aaa Aaaa Aaaaaaa Aaaa Aa Aaaaaaaa Aaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaa Aaaa Aa a Aaa Aaaaaaaaaaa Aa Aaa Aaa Aaa Aaaaaaa Aaaaaaa Aa Aaaa Aaaaa Aa Aaaa Aaaaaa a Aaaaaaaaaaaaaaa Aaa Aaaa Aaaaaaa Aaa Aaaaa Aaaaaa Aaaa Aaaa Aaaa Aa Aaaaaa Aaaa Aaaaaaa Aaaaaaa Aaaaaaaaa Aaaa Aaaa Aaa Aaaaaaa a Aaaa Aaaaaaa Aaaaaaa Aa Aaaaaa Aa Aaaa Aaa Aaaa Aaaa
Aaaaa Aaaaaaaaaaaaaaa Aa a Aaaaaaaaa Aaaaaaaaaaa Aaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaa a Aaaa Aaaaaa Aa Aa Aaaa Aaa Aaa Aaaaaa Aa Aaaa Aaa Aaaaa Aaa Aaaaaaaaaaaaa Aa Aaa Aaa Aaaaaaa Aaaaa Aaaa Aaaa Aaaaa Aa Aaaa Aaaaaa Aaaaa Aaaa Aaaaaaaa Aaaaaaaa Aaaaaaaa Aaaa Aa Aaaaaaaa Aaaaa Aaaa Aaa Aaaaaaaa Aaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaa Aa a Aaa Aaaaaa Aaaaaaaaaaaaaaa Aaaa Aaaaaa Aaaaaa Aa Aaaaa Aaa Aaa Aaaaaaaaa Aaaa Aaaaaaaa Aaaaaa Aaaa Aaaaaa Aa Aaaaa Aaaaa Aaaaaa
Aaaa Aaa Aaaaaaaa Aaaa Aaa Aaaaa Aaaa Aaa Aaa Aaaaaaaaaaaaaaaa Aaaaaa Aa Aaa Aaaa a Aaaaa a Aa Aaaaaaaaa Aa Aaaa Aaaaaaaaa Aaaaaaaaaa Aaaa Aaaa Aa Aaaaaaaaaa a Aaaaaaaaa Aaaa a Aaaaaaaaa Aaaaaa Aaaa Aaaa Aaa Aaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaaaaaa Aaaaaaa Aaaaaaa Aa Aaaaaa a Aaaaaaaaaa Aaaaaa Aaaa Aaaaa Aaa Aaaaaaa Aaaaaaa Aaaaaa
a Aaaaaaaaaaaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaa Aaaa Aaaaaaaa Aaaa Aaaaa Aaaa Aa Aa Aa Aaa Aaa Aaa Aaaaaaaaaa Aaaa Aaaaaaaaaa a Aaa Aaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaa Aa Aaa Aaa Aa Aaaaa Aaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaa Aaaaaaa Aa Aa Aaaa Aaaaaa Aaaaaaa Aaa Aaaa Aaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaa Aaaaa Aaa Aa Aaaaa Aa Aaa Aaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaaaa
🔒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
더 확인하려면 구독해 주세요
Audit directions
a Aaaa Aa Aaa Aaaaaaaa Aaaa Aaaaaaa Aaaa Aaaaa Aa Aa Aaaaaaa Aaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaa Aaaaaaaaaaa Aaaaaa Aa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaa a Aa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaa Aaa Aaaa Aa Aaaaaaa Aaaaaaaaaa Aa Aaa Aaa a Aaa Aaaaa Aaaaaa
a Aaaa Aaaa Aa Aaaa Aa Aaaaa Aaaaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aaaa Aaaa Aaaaaaa Aaaa Aa Aaa Aaaaaa Aaa Aa Aaaaa Aaaa Aaaaaaa a Aaaa Aaaaaaaaaa Aaaa Aa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aaaaaa Aa Aaa Aaaaa Aaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa
a Aaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaa Aa Aaa Aaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaa a Aa Aaaaaaaaaaaaaa Aa Aaa Aaaaaa Aaaaaaaaaaaaaaa Aa Aaaa Aa Aaaaaaaaaaaaaaa Aa Aaa Aa Aaa Aa Aa Aaaaaa
a Aaaaaa Aaaaaaa a Aaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aa Aa Aa Aa Aaaaaaa Aaa Aa Aaaa Aa Aaaaaaa Aa Aa Aaa Aaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaa Aaaaa Aaa Aaaa Aaa Aaaa Aaaaa
🔒Four reusable audit patterns covering JIT-spill / GC-visibility omissions, with concrete grep targets and analogous subsystems to inspect
더 확인하려면 구독해 주세요