← All issues

[5] [JSC] Move DataView null vector check in IC outside of register save/restore

Severity: High | Component: JSC InlineCacheCompiler | 34669c8

IC handler가 매칭되는 push 없이 register를 pop하는 문제를 수정한 diff입니다. 이 경우 손상된 register file과 stack 상태로 polymorphic continuation에 진입하게 됩니다. 이후 JIT 코드는 공격자가 영향을 줄 수 있는 register 상태에서 실행됩니다. Severity를 High로 평가한 이유입니다.

emitIntrinsicGetter에서 push 이전의 null-vector check와 push 이후의 out-of-bounds check가 동일한 JumpList를 공유하고 있었습니다. 공유된 link site는 두 경우 모두에 대해 register를 pop하는 구조였습니다. 결과적으로 push 이전의 실패 경로에서 restoreReusedRegistersByPopping이 매칭되는 push 없이 실행되었습니다.

Source/JavaScriptCore/bytecode/InlineCacheCompiler.cpp

if (isResizableOrGrowableSharedTypedArrayIncludingDataView(accessCase.structure()->classInfoForCells())) {
+ m_failAndIgnore.append(failAndIgnore);
auto allocator = makeDefaultScratchAllocator(m_scratchGPR);
...
+ CCallHelpers::JumpList postPushFailAndIgnore;
if (isDataView) {
auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(...);
- failAndIgnore.append(outOfBounds);
+ postPushFailAndIgnore.append(outOfBounds);
doneCases.link(&jit);
}
- if (allocator.didReuseRegisters() && !failAndIgnore.empty()) {
- failAndIgnore.link(&jit);
+ if (allocator.didReuseRegisters() && !postPushFailAndIgnore.empty()) {
+ postPushFailAndIgnore.link(&jit);
allocator.restoreReusedRegistersByPopping(jit, preservedState);
m_failAndIgnore.append(jit.jump());
} else
- m_failAndIgnore.append(failAndIgnore);
+ m_failAndIgnore.append(postPushFailAndIgnore);

push 이전 단계의 failAndIgnore jump는 pop 없이 m_failAndIgnore에 직접 추가됩니다. 새로 도입된 postPushFailAndIgnore 목록은 push 이후의 실패를 수집하며, restoreReusedRegistersByPopping을 올바르게 경유하는 경로로 처리됩니다.

JIT IC 실패 경로에서 register save/restore 불균형 발생: push 이전의 실패 jump가 post-push pop 경로를 통해 처리되어 stack이 비정상 상태가 되는 패턴.

JSC는 byteLengthget_by_id와 같은 property 접근 연산을 작은 machine code stub으로 캐싱합니다. check 실패 시에는 m_failAndIgnore로 fall through합니다. IntrinsicGetterAccessCaseDataView.prototype.byteLength와 같은 built-in getter를 특화 처리합니다. ScratchRegisterAllocator::preserveReusedRegistersByPushing / restoreReusedRegistersByPopping은 stub에 필요한 scratch register가 여유 register를 초과할 때, 재사용 register를 native stack에 push합니다. 이 두 함수 사이의 모든 exit 경로는 push와 pop을 모두 수행하거나, 둘 다 수행하지 않아야 합니다. loadDataViewByteLength는 underlying buffer의 length를 읽는 inline code를 생성하고, detach되거나 범위를 초과한 view에 대해 outOfBounds jump를 생성합니다.

push 이전의 null-vector check가 생성하는 jump는 매칭되는 push 없이 재사용 register를 pop하는 코드에 연결되어 있었습니다. 이로 인해 SP가 push 크기만큼 어긋나고, 재사용된 callee/scratch register가 잘못된 값을 담게 됩니다.

이후 경로는 SP가 어긋나고 재사용 register가 잘못된 값을 담은 채로 m_failAndIgnore(polymorphic continuation / slow-path entry)로 분기합니다. diff는 이 invariant 위반을 명확히 드러내지만, 그것만으로 downstream 코드에서 exploit 가능한 primitive가 성립한다고 단정할 수는 없습니다. regression test는 4-way polymorphic site를 구성하여 IC stub 생성을 강제한 뒤, ArrayBuffer.prototype.transfer()를 호출해 detach/null-vector 분기를 유발합니다. 이는 정확히 push 이전의 실패 경로에 해당합니다.

이 취약점은 모든 IC handler가 온전한 abstract machine state를 유지한 채 polymorphic continuation으로 반환되어야 한다는 IC invariant를 약화시킵니다. 해당 invariant가 깨지면, 공격자는 web JS에서 도달 가능한 JIT 생성 코드 내에서 type/state confusion을 노리는 방향으로 활용할 가능성이 존재합니다.