← All issues

[JSC] Active element segment offsets are truncated for a table64

An out-of-bounds table offset didn't trap — it wrote to slot zero.

Component: JSC WebAssembly | e942b93

WebAssembly table은 funcref처럼 간접 호출에 쓰이는 typed reference를 담습니다. Active element segment는 인스턴스화 시점에 offset expression을 이용해 table slot을 채우는데, 이 offset은 상수, imported global, 또는 constant expression 중 하나일 수 있습니다. table64 proposal은 table index를 i32에서 i64로 확장하기 때문에, offset은 처음부터 끝까지 64비트 값으로 전달되고 비교되어야 합니다. bounds check 이전 어딘가에서 offset을 좁히는 지점이 있다면, 더 넓은 타입을 도입한 목적 자체를 무력화하는 wraparound 클래스가 그대로 재현됩니다.

Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp

- uint32_t elementIndex = 0;
- if (offset.isGlobalImport())
- elementIndex = static_cast<uint32_t>(m_instance->loadI32Global(...));
- else if (offset.isConst())
- elementIndex = offset.constValue();
- else {
- uint64_t result;
- evaluateConstantExpression(..., Wasm::Types::I32, result);
- elementIndex = static_cast<uint32_t>(result);
- }
+ const bool isTable64 = moduleInformation.table(*element.tableIndexIfActive).addressType().is64Bit();
+ uint64_t elementIndex = 0;
+ if (offset.isGlobalImport()) {
+ if (isTable64)
+ elementIndex = static_cast<uint64_t>(m_instance->loadI64Global(...));
+ else
+ elementIndex = static_cast<uint32_t>(m_instance->loadI32Global(...));
+ } else if (offset.isConst()) {
+ elementIndex = isTable64 ? offset.constValue() : static_cast<uint32_t>(offset.constValue());
+ } else {
+ uint64_t result;
+ evaluateConstantExpression(..., isTable64 ? Wasm::Types::I64 : Wasm::Types::I32, result);
+ elementIndex = isTable64 ? result : static_cast<uint32_t>(result);
+ }

forEachActiveElement는 table 크기와 비교해 검증하기 전에 64비트 segment offset을 uint32_t로 잘라내고 있었습니다. 이번 fix는 elementIndexuint64_t로 확장하고, imported global·constant·constant expression 세 가지 offset 소스 모두를 table에 선언된 address type을 기준으로 분기시킵니다. table이 64비트인 경우에는 i64 global을 로드하고 Wasm::Types::I64를 기준으로 평가하며, i32 table에서는 기존의 명시적인 32비트 narrowing을 그대로 유지합니다.

offset 값 4294967296이 0으로 잘려나가면서, 원래 out-of-bounds로 trap되어야 할 segment가 오히려 table의 0번 slot에 조용히 function reference를 써넣는 결과로 이어졌습니다. 이 offset의 출처는 attacker가 제어 가능한 module data이거나 imported global일 수 있어, 인스턴스화 시점에 도달 가능한 table-content corruption에 해당합니다. commit message는 같은 버그 클래스가 317633@main에서 다른 code path에 대해 이미 한 번 수정된 바 있다고 밝히고 있으며, 이 점이 이번 건을 단발성 이슈가 아닌 재발 사례로 규정짓는 근거입니다.

이 패턴은 64비트 addressing 하에서 32비트로 값을 잘라내는 것으로, 원래 살아남아야 할 값을 bounds check 이전에 좁혀버리는 형태입니다. 그리고 이 패턴은 이미 두 개의 인접한 code path에서 발견된 바 있어, 추가 인스턴스가 존재할 가능성을 시사하는 가장 강력한 신호에 해당합니다. 좁혀서 보면, WebAssemblyModuleRecord 안에서 constValue(), loadI64Global, constant-expression 평가 결과가 table 또는 memory indexing으로 흘러 들어가는 모든 지점을 점검하고, i64 경로가 중간에 조용히 downcast되지 않고 끝까지 유지되는지 확인해야 합니다. 범위를 넓히면, 같은 offset-expression 처리 로직과 global-load helper를 공유하는 인접 연산들 — table.init, table.copy, memory64용 active data segment — 로 점검 범위를 확장할 필요가 있습니다. 이들 모두 동일한 constant-expression evaluator를 사용하기 때문입니다. 가장 넓은 관점에서 보면, 타입이 proposal 경계를 넘어 확장될 때 위험은 오히려 새로 추가된 호출 지점이 아니라, 더 좁은 local 타입으로도 여전히 컴파일이 되는 기존 호출 지점에 집중됩니다. 리뷰 시 주목해야 할 신호는, 이후 downstream의 validation 비교에서 authoritative한 값으로 취급되는 값에 걸린 static_cast<uint32_t> (또는 uint32_t local 변수)입니다.