[2] [JSC YARR] Fix integer overflow in AssemblerBuffer leading to heap overflow
Await a cross-realm promise in JSC and the next thenable's resolver landed in the wrong realm — the fast path skipped the spec's realm fix-up.
공격자가 조작한 regex 입력 하에서 32-bit wrap이 발생할 수 있었던 code-buffer 길이 연산이 이번 diff에서 확장되었습니다. 하위 단계의 putIntegralUnchecked는 실제 allocation 범위를 넘어 unchecked write를 수행합니다. 그 결과 JIT가 내보내는 내용을 인접 heap에 선형으로 기록하는 primitive가 성립합니다. High로 평가된 이유입니다.
m_index와 m_capacity, 그리고 isAvailable(), putIntegral(), grow() 내의 산술 연산이 unsigned에서, YARR 구동 code emission 중 wrap이 발생하지 않는 너비로 확장되었습니다. 아울러 YARR에 TooManyCaptures와 FrameTooLarge 오류가 추가되었습니다. commit 메시지는 이 두 가지를 "not actually critical"로 명시하고 있으며, 패턴 컴파일 단계에서의 DoS 및 frame bookkeeping overflow에 해당합니다.
Source/JavaScriptCore/assembler/AssemblerBuffer.h
JIT code buffer 길이 연산의 32-bit overflow: wrap된 nextIndex가 capacity check를 우회하고, putIntegralUnchecked가 실제 allocation 범위를 넘어 내보낸 machine code를 기록합니다.
Patch Details
buffer 너비가 확장되었습니다. isAvailable, putIntegral, grow의 산술 연산은 확장된 타입으로 재계산됩니다. YARR의 패턴 컴파일러에는 TooManyCaptures와 FrameTooLarge 조기 종료 조건이 추가되어, 비정상적인 패턴이 emitter에 도달하기 전에 실패하도록 처리됩니다.
Background
AssemblerBuffer는 JSC의 모든 JIT 백엔드가 linking 전 machine code를 축적하는 emission buffer입니다. YARR, baseline, DFG, FTL, Wasm BBQ/OMG가 모두 이 buffer를 사용합니다.
isAvailable(n)은 putIntegralUnchecked<T> 호출의 사전 조건입니다. putIntegralUnchecked<T>는 WTF::unalignedStore<T>(m_storageBuffer + m_index, value)를 수행하고 m_index를 증가시킵니다. putIntegral은 isAvailable이 공간 부족을 보고하면 grow()를 호출합니다.
buffer의 크기는 unsigned 기준으로 관리되고 있었습니다. 그러나 YARR은 공격자가 조작한 패턴으로 emission 크기를 2^32 너머까지 유도할 수 있습니다. 이 불일치가 이번 취약점의 근원입니다.
Analysis
패치 이전의 산술 연산 지점 세 곳은 모두 32-bit로 계산되었습니다:
isAvailable(space): return m_index + space <= m_capacity; // unsigned wrap
putIntegral(value): nextIndex = m_index + sizeof(IntegralType); // unsigned wrap, "nextIndex > capacity" guard 우회됨
grow(extra): newCapacity = m_capacity + m_capacity/2 + extra; // unsigned wrap, 실제 기록 크기보다 작은 크기로 allocation됨
YARR 패턴이 emission을 2^32 바이트 방향으로 몰아가면, 이 연산들에서 wrap이 발생합니다. wrap 이후에는 isAvailable이 true를 반환합니다. 이어서 putIntegral의 nextIndex > capacity 검사가 우회됩니다. grow()는 실제 기록 크기보다 작은 크기로 buffer를 조정합니다. 결과적으로 putIntegralUnchecked가 live allocation 범위를 넘어 인코딩된 machine code를 unaligned store로 기록하게 됩니다.
기록은 선형으로 이루어지며, payload는 JIT가 내보낸 바이트입니다. 패턴 컴파일러가 immediate, displacement, opcode를 제어하기 때문에, overflow로 기록되는 비트는 모두 공격자의 영향을 받습니다.
어느 구조체가 먼저 덮이는지는 heap layout에 달려 있습니다. JIT memory pool은 JSC heap의 나머지 영역과 인접해 있습니다. 그 결과 기록이 다음 code linking pass 또는 다음 JS-visible allocation에 영향을 미치는 metadata에 도달합니다.
YARR 측의 두 가지 변경 사항은 중요도가 낮은 bookkeeping 수정입니다. TooManyCaptures는 파싱된 capture 수가 per-term backtrack frame layout을 overflow시킬 때 발생합니다. FrameTooLarge는 stack-slot 계산 중 currentCallFrameSize가 wrap될 때 발생합니다. 패치 이전에는 이 두 경우가 일관성 없는 backtrack frame layout이나 컴파일러 hang/crash를 유발했습니다.
이 취약점은 emitter가 live buffer를 넘어 기록하지 않는다는 JIT memory invariant를 위반합니다. exploit primitive는 heap write이며, JSC allocator에서 assembler data buffer 다음 영역에 공격자가 제어하는 바이트가 기록됩니다.
Audit directions
- 공격자의 입력에 영향을 받는 buffer에서
unsigned타입으로 수행되는 모든 길이/offset 산술 연산 지점.AssemblerBuffer,LinkBuffer,MacroAssembler에서 position 또는 capacity에 대한unsigned연산이 남아 있는지 검색하여 확인합니다. - 공격자가 code size 확장을 유도할 수 있는 다른 JIT client. Wasm BBQ/OMG, 깊이 중첩된 loop를 처리하는 FTL B3 lowering, 극단적인 constant folding이 적용된 DFG는 모두 매우 큰 buffer를 내보낼 수 있습니다. 이 중 어느 것도 limit 산술에
unsigned를 사용하지 않는지 확인이 필요합니다. - YARR 패턴 컴파일러의 accumulator (
currentCallFrameSize, capture-count, alternative-count)에서 유사한 wrap-on-overflow 패턴을 점검합니다.TooManyCaptures/FrameTooLarge는 추가되었지만, 다른 accumulator에는 동등한 보호가 없을 수 있습니다.