[11] [JSC] Use span's length in genericTypedArrayViewProtoFuncSortImpl
GSAB 기반 TypedArray length에 대한 TOCTOU 수정. 병렬 grow 연산이
length()와typedSpan().size()를 desynchronize시키며, sort 본문이 두 값 모두 사용하여 element copy 중 OOB 발생. High 평가.
genericTypedArrayViewProtoFuncSortImpl은 thisObject->length()와 thisObject->typedSpan()으로 length를 두 번 독립적으로 읽었습니다. Growable SharedArrayBuffer를 backing으로 사용하는 경우, 두 읽기 사이에 다른 agent가 grow()를 호출하면 두 값이 일치하지 않게 됩니다.
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
Patch Details
순서가 바뀌었습니다. originalSpan을 먼저 생성하고, length는 originalSpan.size()에서 파생합니다. 결과적으로 단일 snapshot에서 단일 source of truth를 유지하게 됩니다.
공유 메모리 length에 대한 TOCTOU: race가 발생하기 쉬운 동일한 값을 단일 snapshot 대신 독립적인 두 번의 읽기로 파생하는 패턴.
Background
maxByteLength를 지정해 생성한 Growable SharedArrayBuffer는 SharedArrayBuffer.prototype.grow를 통해 확장할 수 있습니다. GSAB 위에 생성된 length-tracking TypedArray view는 현재 SAB의 byte length에서 파생된 length를 반환합니다. SAB는 agent(main thread와 Worker) 간에 공유되므로, length를 다시 파생할 때마다 구현 아래에서 변경될 수 있는 값을 읽게 됩니다.
std::span은 호출 시점에 캡처된 {pointer, size} 쌍입니다. %TypedArray%.prototype.sort는 사용자 comparator를 받으며, C++에서는 work buffer로 복사한 뒤 정렬하고 다시 복사하는 방식으로 구현됩니다.
Analysis
length는 grow 이전 크기를 반영하고, originalSpan은 grow 이후를 반영합니다. sort 본문은 두 값 모두 사용합니다. work vector의 크기는 length 기준으로 잡으며, span은 element copy와 sort callback의 source이자 destination이 됩니다. 이로 인해 length == originalSpan.size() 불변식이 더 이상 유지되지 않습니다.
Exploit 형태는 다음과 같습니다. 먼저 초기 크기보다 훨씬 큰 maxByteLength로 GSAB를 생성하고, 그 위에 length-tracking Int32Array를 만듭니다. SAB를 Worker에 전달하여 sab.grow()를 반복 호출하게 합니다. main thread에서는 ta.sort(compareFn)을 반복 호출합니다. 그러면 sort 본문이 불일치하는 bounds로 동작하게 됩니다. grow 이전과 이후 length의 크기 차이 범위 안에서, element copy나 write-back 중 OOB read 또는 write가 발생합니다. regression test를 보면 이미 with/toReversed/toSorted에는 적용되어 있었으나, sort에는 누락되어 있었음을 확인할 수 있습니다.
이 vulnerability는 WebContent process 내부의 memory type safety를 약화시킵니다. main thread와 Worker를 모두 제어할 수 있는 공격자는 sort work vector 또는 GSAB 기반 storage에 인접한 bounded OOB primitive를 유도할 수 있습니다.
Audit directions
- GSAB 기반 length-tracking TypedArray에서 독립적인 두 번의 length 읽기.
JSGenericTypedArrayViewPrototypeFunctions.h의 모든 메서드에서length()와typedSpan()을 각각 별도로 호출하거나,length(),byteLength(),typedVector()를 두 번 이상 조합해 호출하는 경우가 있는지 점검합니다. regression test에 아직 포함되지 않은 메서드:copyWithin,fill,set,slice,subarray,indexOf,lastIndexOf,includes,reverse,join. - JS callback 경계 이전의 유효성 검사가 bounds를 신뢰하는 코드로 복귀하는 경우. comparator callback은 re-entrancy 지점입니다. JS-callable 인자를 받는 TypedArray 메서드를 추적하여, callback 이후 경로가 원본 snapshot을 참조하는지 확인합니다.
- JSC와 JIT inlining 사이의 경계. TypedArray accessor를 inline하는 thunk를
Source/JavaScriptCore/dfg/와ftl/에서 검색합니다. 이 경로의 length 파생 로직은 역사적으로 동기화 업데이트가 이루어지지 않은 사례가 있었습니다. - regression test를 denylist 방식에서 allowlist 방식으로 전환.
Object.getOwnPropertyNames(TypedArray.prototype)을 프로그래밍 방식으로 순회하여, 다음에 누락될 케이스를 자동으로 탐지하도록 합니다.