[JSC][Wasm] Force BoundsChecking for Memory64 accesses in BBQ
Component: JSC WebAssembly | d6d0926
JSC의 BBQ/OMG JIT tier에서 WebAssembly memory access는 두 가지 전략 중 하나를 사용합니다. Signaling 방식은 memory 뒤에 거대한 PROT_NONE guard 영역을 매핑해두는 방식으로, 범위를 벗어난 32-bit access가 SIGSEGV handler를 통해 trap되도록 만듭니다. 이 방식은 explicit한 bounds-check instruction 없이 속도를 확보할 수 있습니다. 반면 BoundsChecking 방식은 모든 access 앞에 compare-and-branch instruction을 명시적으로 삽입합니다. Signaling은 address가 32-bit라는 전제를 깔고 있습니다. guard 영역의 크기 자체가 32-bit offset을 잡아내는 데 맞춰져 있기 때문입니다. 문제는 Memory64에서 발생합니다. 64-bit pointer에 offset이 더해지면 guard 영역을 훨씬 벗어난 위치에 도달할 수 있는데, 이 경우 trap이 걸리지 않습니다.
Source/JavaScriptCore/wasm/WasmModuleInformation.h
Source/JavaScriptCore/wasm/WasmBBQJIT.h
기존 BBQ의 pointer-materialization 코드는 memoryIndex != 0일 때만 bounds-checking을 강제했고, 그 외의 경우에는 현재의 m_mode로 그대로 fall through했습니다. 결과적으로 index 0에 위치한 Memory64 memory가 여전히 Signaling 경로를 선택할 수 있는 상태였습니다. 이번 변경으로 해당 정책은 ModuleInformation::memoryModeForAccess()로 일원화되었고, WasmBBQJIT.h와 그 64-bit CPU 전용 구현인 WasmBBQJIT64.h, 그리고 OMG의 index 0 memory-access 경로에서 이 함수를 호출하도록 변경되었습니다. OMG의 호출 지점은 기존 if (!memoryIndex) 블록 안에 위치하므로, 그 영향 범위는 index 0에 위치한 Memory64 memory로 한정됩니다.
Significance
Wasm memory-mode 선택 로직이 하나의 정책 함수로 통합되면서, 이제 어떤 JIT 호출 지점도 Memory64 memory에 대해 독자적으로 Signaling fast path를 선택할 수 없게 되었습니다. 기존에도 Memory64 memory가 Signaling 경로에 도달하면 RELEASE_ASSERT로 인해 hard crash가 발생하는 구조였습니다. 따라서 이번 변경은 실제로 exploit 가능한 live OOB를 고치는 패치라기보다는, BBQ와 OMG 호출 지점 사이에 잠재해 있던 불일치에 대한 defense-in-depth 성격이 강합니다. assert 역시 RELEASE_ASSERT_WITH_SECURITY_IMPLICATION으로 격상되어 backstop 형태로 유지됩니다.
Audit directions
정책을 한 곳으로 모으는 작업의 실효성은 결국 호출 지점을 얼마나 빠짐없이 커버했는가에 달려 있습니다. 그래서 앞으로 살펴봐야 할 방향은 여전히 자체적으로 mode를 판단하는 경로가 남아 있는지 찾는 것입니다. 좁게는, load나 store를 emit하는 모든 BBQ/OMG 지점에서 memoryModeForAccess를 실제로 참조하는지 확인해야 합니다. 여기에는 constant offset을 다루는 fast path, SIMD/vector load, 그리고 자체적인 mode 선택 로직을 손으로 구현해두었을 수 있는 bulk memory operation도 포함됩니다. 별도로, OMG의 non-zero memoryIndex 경로는 이번 diff에서 손대지 않은 부분이므로, 해당 경로가 Memory64에 대해 BoundsChecking을 독립적으로 강제하고 있는지 따로 확인할 필요가 있습니다. 넓게는, Memory64가 shared memory나 growable memory와 결합되었을 때 tiering 과정에서 일시적으로 Signaling-mode 컴파일된 함수에 도달할 수 있는지 살펴봐야 합니다. 또한 강화된 RELEASE_ASSERT_WITH_SECURITY_IMPLICATION이 dead code가 아니라 실제로 도달 가능하고 테스트 가능한 상태인지도 확인해야 합니다. memoryModeForAccess에 조금이라도 빈틈이 있다면, mismatched mode와 exploit 가능한 OOB 사이를 막아주는 것은 결국 이 assert 하나뿐이기 때문입니다. 가장 넓게 보면, 여러 호출 지점에 중복되어 있던 safety policy를 한 곳으로 통합할 때는 항상 빠뜨린 지점이 남아 있을 가능성이 있습니다. 리뷰 과정에서 눈여겨봐야 할 신호는, 같은 파일 안에 이미 공용 accessor가 존재함에도 불구하고 ternary나 switch scrutinee가 정책 값을 지역적으로 다시 계산하고 있는 코드입니다.