[5] [JSC] Move DataView null vector check in IC outside of register save/restore
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
Patch Details
push 이전 단계의 failAndIgnore jump는 pop 없이 m_failAndIgnore에 직접 추가됩니다. 새로 도입된 postPushFailAndIgnore 목록은 push 이후의 실패를 수집하며, restoreReusedRegistersByPopping을 올바르게 경유하는 경로로 처리됩니다.
JIT IC 실패 경로에서 register save/restore 불균형 발생: push 이전의 실패 jump가 post-push pop 경로를 통해 처리되어 stack이 비정상 상태가 되는 패턴.
Background
JSC는 byteLength의 get_by_id와 같은 property 접근 연산을 작은 machine code stub으로 캐싱합니다. check 실패 시에는 m_failAndIgnore로 fall through합니다. IntrinsicGetterAccessCase는 DataView.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를 생성합니다.
Analysis
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을 노리는 방향으로 활용할 가능성이 존재합니다.
Audit directions
preserveReusedRegistersByPushing전후 양쪽 영역의 실패 edge를 단일JumpList로 수집하는 패턴.InlineCacheCompiler.cpp,Source/JavaScriptCore/jit/,dfg/DFGSpeculativeJIT*.cpp전반에서preserveReusedRegistersByPushing사용 지점을 점검해야 합니다. push 호출 상단에 선언된JumpList가 양쪽에서 추가되는 경우가 없는지 확인하는 것이 핵심입니다.allocator.didReuseRegisters()가 true일 때만 조건부로restoreReusedRegistersByPopping을 호출하는 stub epilogue. 해당 레이블에 연결된 모든 jump가 매칭되는 push와 pop 사이에서 생성되었는지 검증해야 합니다.loadDataViewByteLength/loadTypedArrayByteLength호출 지점. 이 함수들이 반환하는 실패 edgeJumpList가 어디에 연결되는지 추적해야 합니다.- Detach/resize 가능한 buffer 상태 guard.
InlineCacheCompiler와DFGSpeculativeJIT내부에서 null-vector 또는 detach 상태를 검사하는 모든 guard는 register-save 경계의 잘못된 쪽에 위치할 가능성이 있는 대상입니다.