← All issues

[JSC] Do not truncate table64 maximum size to uint32_t

A correct import check on a number that wrapped before it ever arrived.

Component: JSC WebAssembly | b91045c

TableInformation은 table의 선언된 bounds를 validation, linking, JIT compilation 전 과정에 걸쳐 운반하는 struct입니다. table64 하에서는 table이 64-bit maximum size를 선언할 수 있는데, 정작 이 struct는 maximumstd::optional<uint32_t>로 저장하고 있었습니다. 그 결과 2^32를 넘는 값은 특정 호출 지점에서만이 아니라 저장되는 시점 자체에서 좁혀지게 됩니다. 이 값을 사용하는 소비자는 크게 두 곳입니다. 하나는 imported table에 대해 모듈이 initial == maximum을 선언한 경우로, 이때 JSC의 BBQ/OMG tier는 이를 고정 크기로 간주하여 길이를 compiled code에 상수로 박아 넣고 runtime bounds check를 생략합니다. 다른 하나는 link-time import check로, imported table의 실제 maximum을 모듈이 선언한 maximum과 비교하는 지점입니다.

Source/JavaScriptCore/wasm/WasmFormat.h

- TableInformation(uint32_t initial, std::optional<uint32_t> maximum, ...)
+ TableInformation(uint32_t initial, std::optional<uint64_t> maximum, ...)
- std::optional<uint32_t> maximum() const { return m_maximum; }
+ std::optional<uint64_t> maximum() const { return m_maximum; }
private:
- std::optional<uint32_t> m_maximum;
+ std::optional<uint64_t> m_maximum;

Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp

- if (std::optional<uint32_t> expectedMaximum = moduleInformation.tables[import.kindIndex].maximum()) {
- std::optional<uint32_t> actualMaximum = table->maximum();
+ if (std::optional<uint64_t> expectedMaximum = moduleInformation.tables[import.kindIndex].maximum()) {
+ std::optional<uint64_t> actualMaximum = table->maximum();
if (!actualMaximum)
return exception(...);
if (*actualMaximum > *expectedMaximum)

이번 fix는 WasmFormat.h, JSWebAssemblyTable, WebAssemblyModuleConstructor, WebAssemblyModuleRecord 전반에 걸쳐 저장 및 전달 타입을 uint64_t로 넓혔습니다. 이로써 link-time import 호환성 검사, 모듈이 직접 선언한(non-imported) table의 growth, JS API type reflection이라는 세 소비자 클래스를 한 번에 커버하게 되었습니다.

이 truncation은 서로 다른 두 가지 failure mode로 이어집니다. 먼저 실제 maximum이 2^32를 초과하는 table64는 값이 wrap되어 내려가면서 Wasm::Table의 maximum-≥-length invariant를 위반할 수 있고, 이 경우 standalone table이 더 이상 grow하지 못하게 됩니다. 더 심각하게는, wrap되어 내려간 maximum이 fixed-size(initial == maximum) import check를 부당하게 통과시킨 뒤, BBQ/OMG code가 table 길이를 compile-time 상수로 접어 넣은 상태에서 runtime에 실제로 grow가 일어나는 상황도 가능합니다. 4294967301처럼 2^32를 살짝 넘는 실제 maximum 값 하나만으로도 두 경로 중 어느 쪽이든 도달할 수 있습니다.

이 버그는 truncate-then-trust 패턴에 해당합니다. 64-bit bound가 저장되는 시점에 좁혀졌고, 이렇게 잘못된 값이 JIT의 compile-time-constant assumption과 link-time compatibility check 양쪽 모두로 전파되었습니다. 좁게 보면, wasm 파이프라인 안에서 64-bit table 또는 memory bound를 여전히 32-bit 타입의 struct나 API로 운반하는 다른 지점들을 점검할 필요가 있습니다. growth 경로와 memory64/table64의 다른 reflection 표면들이 직접적인 형제 케이스에 해당합니다. 넓게 보면, 이 버그의 더 위험한 절반은 2차적인 영향 쪽입니다. 따라서 어떤 tier가 link 이후 checked bound가 그대로 유지된다고 가정하는 모든 지점을 나열하고, 그 tier가 접어 넣은 bound가 upstream에서 좁혀진 적이 없는지 확인해야 합니다. truncated된 입력에 대한 올바른 check는 call site에서의 올바른 check와 겉으로 구분되지 않기 때문입니다. 가장 넓게 보면, storage-point에서의 narrowing은 call-site 감사 자체를 무력화시킵니다. 각 소비자를 개별적으로 보면 모두 정상으로 보이기 때문입니다. 이때 리뷰에서 드러나는 단서는 struct member의 선언된 width가 그 accessor를 호출하는 코드들이 요구하는 width보다 좁다는 점이며, 이는 단일 파일 diff 리뷰만으로는 드러나지 않습니다.