ScopedArgumentsTable moved out of Primitive Gigacage
Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
Gigacage는 JSC의 공간 안전성 완화 기법으로, 특정 heap allocation을 예약된 가상 주소 영역 안에 격리합니다. 이 cage에 한정된 pointer는 손상되더라도 해당 영역 밖으로 탈출할 수 없습니다. Primitive cage는 TypedArray storage, ArrayBuffer 내용 등 사용자 페이로드 버퍼만을 위한 전용 공간입니다. ScopeOffset은 함수의 인자 목록에서 named argument의 위치를 JSLexicalEnvironment 내부의 변수 슬롯 인덱스로 매핑하는 engine 내부 메타데이터입니다.
이 commit은 ScopedArgumentsTable::m_arguments를 CagedUniquePtr<ScopeOffset, Gigacage::Primitive>에서 일반 Vector<ScopeOffset>으로 변경했습니다. 이를 통해 engine 내부 scope 메타데이터가 Primitive Gigacage 밖으로 이동되었습니다. 아울러 trySetLength에서 잠겨 있어야 할(immutable) 테이블 인스턴스를 변경하던 낡은 m_watchpointSets.resize(newLength) 호출도 함께 제거되었습니다.
Significance
이 변경은 알려진 cage-pivot 경로를 차단합니다. 기존에는 Primitive Gigacage에 대한 write primitive(예: 손상된 TypedArray backing store를 통한 경우)로 ScopeOffset 메타데이터에 도달하는 것이 이론적으로 가능했습니다. 이 경우 해당 값이 JSLexicalEnvironment::variables()에 대한 unchecked index로 전환되어, 범위를 벗어난 변수 슬롯 접근이 발생할 수 있었습니다.
Audit directions
- 사용자 페이로드가 아닌 engine 메타데이터를 보유하는 다른
CagedUniquePtr<T, Gigacage::Primitive>인스턴스. 이 commit은 해당 패턴이 실제로 존재했으며 간과되었음을 증명합니다. 유사한 인스턴스가 남아 있을 가능성이 있습니다.Source/JavaScriptCore에서CagedUniquePtr<.*Primitive>를 검색하여, 내부 버퍼가 사용자 제어 바이트를 담는지 아니면 engine 내부 인덱스나 pointer를 담는지 분류해야 합니다. - JIT/LLInt offset 참조. JSC JIT와 LLInt는
ScopedArgumentsTable필드를 바이트 offset(offsetOfLength(),offsetOfArguments())으로 참조하는 코드를 생성합니다. 레이아웃이CagedUniquePtr(pointer + metadata)에서Vector(pointer + size + capacity)로 변경되었습니다. 이 offset을 사용하는 모든 JIT codegen 지점이 동기화되어 업데이트되었는지 점검해야 합니다. 특히 alignment가 달라질 수 있는 32-bit 빌드에서는 주의가 필요합니다. - Locked-table invariant 위반. 낡은
m_watchpointSets.resize()호출의 제거는 locked-table invariant가 그동안 조용히 위반되고 있었음을 드러냅니다.m_locked == true상태의 인스턴스에 대해 유사한 변경을 수행하는 다른ScopedArgumentsTable메서드가 있는지 점검해야 합니다. 아울러SymbolTable::trySetArgumentsLength와 swap 직전 이전 테이블을 읽는 JIT 스레드 사이에서 data race를 통해watchpointSetsresize가 외부에서 관찰될 수 있었는지도 점검이 필요합니다.