← All issues

[5] OMG Wasm B3 IR type-tag mismatch on struct/array.new_default

OMG picked its zero-initialiser type from `elementSize()`. An `f32` field is 4 bytes — same width as `i32` — so the JIT tagged the zero `Int32`.

Severity: Low | Component: JSC OMG Wasm tier — WasmOMGIRGenerator | 7842f48

이 diff는 B3의 CSE / store-to-load forwarding에서 발생하는 RELEASE_ASSERT를 방지합니다. f32/f64 Wasm 필드의 zero-initializer로 Int32(0) 또는 Int64(0) 상수가 사용될 때 유발되는 문제입니다. Low 평가의 근거가 여기 있습니다. 정수 슬롯과 float 슬롯에서 0의 bit pattern이 동일하므로, 실제 피해는 assertion abort 자체에 국한됩니다. 이번 commit에서는 downstream miscompilation이나 memory-safety primitive가 드러나지 않습니다.

struct.new_default Wasm 명령어는 f32 타입 필드에 Int32(0)을 기록하고 있었습니다. store-to-load forwarding이 동작하면 기대 타입이 i32가 아닌 f32이기 때문에 B3::Value::replaceWithIdentity에서 RELEASE_ASSERT가 실패합니다. 패치에서는 OMGIRGenerator::addStructNewDefaultOMGIRGenerator::addArrayNewDefault가 수정되었습니다. toB3Type(<wasmType>.unpacked())를 통해 올바른 타입의 상수를 생성하도록 변경된 것입니다. regression test(new_default-f32.js)는 f32로 구성된 struct와 array를 생성합니다. 각각의 *.new_default를 호출하고 즉시 값을 읽은 뒤, OMG tier 컴파일을 유발할 만큼 충분한 횟수의 hot loop에서 함수를 실행합니다.

Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

// addArrayNewDefault
initValue = m_currentBlock->appendNew<WasmConstRefValue>(m_proc, origin(), JSValue::encode(jsNull()));
else if (elementType.elementSize() == 16)
initValue = constant(V128, v128_t { });
- else if (elementType.elementSize() <= 4)
- initValue = constant(Int32, 0);
else
- initValue = constant(Int64, 0);
+ initValue = constant(toB3Type(elementType.unpacked()), 0);
 
// addStructNewDefault
initValue = m_currentBlock->appendNew<WasmConstRefValue>(m_proc, origin(), JSValue::encode(jsNull()));
else if (typeSizeInBytes(fieldType) == 16)
initValue = constant(V128, v128_t { });
- else if (typeSizeInBytes(fieldType) <= 4)
- initValue = constant(Int32, 0);
else
- initValue = constant(Int64, 0);
+ initValue = constant(toB3Type(fieldType.unpacked()), 0);

JSTests/wasm/stress/new_default-f32.js

+ // (type (struct (field f32))) ; struct.new_default 0; struct.get 0 0; global.set 0
+ for (let i = 0; i < wasmTestLoopCount; ++i) fn();
+ // (type (array f32)) ; array.new_default 0; array.get 0; global.set 0
+ for (let i = 0; i < wasmTestLoopCount; ++i) fn();

이 패치는 byte 크기 기반의 두 분기(<= 4Int32(0), elseInt64(0))를 단일 호출로 통합합니다. toB3Type(<wasmType>.unpacked())를 통해 선언된 Wasm value type에서 B3 타입을 직접 도출하는 방식입니다. addArrayNewDefaultaddStructNewDefault 모두 동일하게 수정되었습니다. 16바이트 vector 분기와 reference type 분기는 변경되지 않았습니다. 새로운 regression test는 f32를 구체적으로 검증하며, struct 필드와 array 요소 두 가지 변형 모두를 OMG로 승격될 만큼 충분히 큰 hot loop에서 실행합니다.

JIT IR 구성 단계의 type-tagging mismatch — 선언된 Wasm value type 대신 byte 크기 휴리스틱으로 생성된 B3 상수로 인한 store-to-load forwarding 타입 불변성 위반.

B3는 FTL과 OMG가 공유하는 JSC의 mid-level optimizing IR입니다. 모든 B3 ValueType(Int32, Int64, Float, Double, V128 등)을 보유하며, IR 수준의 불변성은 연산의 피연산자 타입이 서로 일치해야 함을 요구합니다. CSE(common subexpression elimination)와 store-to-load forwarding은 B3의 최적화 기법입니다. 방금 기록된 주소에서의 load를 감지하면, Value::replaceWithIdentity를 통해 저장된 값으로 대체합니다. 이때 source와 target B3 타입이 반드시 일치해야 합니다. WebKit의 RELEASE_ASSERT는 debug와 release 빌드 모두에서 동작하며, 조건이 false이면 프로세스를 중단합니다.

OMG는 JSC의 optimizing Wasm JIT tier입니다. OMGIRGenerator는 Wasm bytecode를 순회하며 B3 IR을 생성합니다. struct.new_default / array.new_default는 struct 또는 array를 할당하고 모든 필드/요소를 zero-initialize하는 Wasm GC 명령어입니다. Wasm value type에는 i32, i64, f32, f64, v128, reference type이 포함됩니다. f32는 4바이트, f64는 8바이트입니다. toB3Type은 Wasm value type을 B3 타입으로 매핑하는 helper 함수입니다(f32 → Float, f64 → Double).

패치 이전에는 OMG가 struct.new_defaultarray.new_default를 B3 IR로 낮출 때, zero-initializer의 타입을 요소/필드의 byte 크기만으로 결정했습니다. <= 4이면 Int32(0), 그 외에는 Int64(0)을 사용하는 구조였습니다. 4바이트 Wasm f32 필드/요소의 경우, 필드의 선언된 B3 타입이 Float임에도 불구하고 Int32 B3 값이 WasmStructSet / WasmArraySet에 전달되었습니다. f64도 마찬가지인데, 기존의 else 분기가 B3 타입이 Double인 8바이트 필드에도 Int64(0)을 생성하고 있었습니다.

이후 B3의 CSE / store-to-load forwarding이 같은 struct 필드나 array 슬롯에서 발생하는 load를 감지하면, Value::replaceWithIdentity를 통해 이전에 저장된 값으로 대체를 시도합니다. replaceWithIdentity는 대체 값의 B3 타입이 load 타입과 일치해야 합니다. 그런데 load는 Float(또는 Double)이고 저장된 상수는 Int32(또는 Int64)입니다. 타입이 불일치하므로 OMG 컴파일 중 B3의 RELEASE_ASSERT가 발생하고, WebContent process가 종료됩니다. toB3Type(<wasmType>.unpacked())를 통해 타입을 도출하면 저장된 값의 타입이 필드/요소의 선언 타입과 일치하게 되어 B3의 타입 불변성이 복원됩니다.

이 버그는 source language의 선언 타입 대신 byte 크기나 storage 크기 휴리스틱으로 IR 타입을 선택하는 JIT IR generator에서 반복적으로 나타나는 유형입니다. if (size == 4) Int32 else Int64라는 동일한 패턴은 정수 타입 필드에서는 조용히 동작하지만, 같은 크기의 floating-point 또는 vector 필드가 등장하는 순간 문제가 발생합니다.

regression test는 사실상 PoC 역할을 합니다. f32(또는 f64) 필드를 하나 이상 가진 struct 또는 array 타입을 선언한 뒤, struct.new_default / array.new_default를 호출합니다. 이후 즉시 필드를 읽어 관찰 가능한 위치에 저장하는 것이 전부입니다. 이 내부 시퀀스 struct.new_default 0 ; struct.get 0 0 ; global.set 0(및 array 변형)이 핵심입니다. B3의 store-to-load forwarding이 중복 load를 감지하고, 타입이 불일치하는 replaceWithIdentity를 시도하게 만드는 정확한 구조이기 때문입니다.

diff에는 downstream memory-safety primitive의 흔적이 없습니다. Int32(0)Float(0.0)의 bit pattern이 동일하므로, assertion이 생략되더라도 저장된 비트는 올바릅니다. 타입이 혼동된 B3 IR이 이후 최적화 pass를 잘못 이끌 가능성은 이론적으로 존재하지만, 패치나 주변 코드에서 그러한 downstream miscompilation은 드러나지 않습니다.

이 vulnerability는 WebContent process의 가용성을 약화시킵니다. HTML/Wasm trust boundary는 공격적인 모듈을 포함한 어떤 well-formed Wasm 모듈도 renderer를 중단시키지 않고 OMG를 통해 컴파일되어야 한다고 가정합니다. 패치 이전에는 단 한 줄의 Wasm 모듈만으로도 이 문제를 재현할 수 있었습니다. f32/f64 타입에 struct.new_default 또는 array.new_default를 담은 모듈을 hot loop에서 실행해 OMG로 승격시키면, assertion이 안정적으로 유발됩니다. 결과적으로 renderer가 종료됩니다. 이런 모듈을 web content에서 제공하는 공격자는 자신의 페이지를 로드하는 모든 탭을 항상 동일하게 crash시킬 수 있습니다.

Note: B3의 정확한 assertion 위치(CSE / store-to-load forwarding 내의 Value::replaceWithIdentity)와 toB3Type의 매핑은 이번 commit에서 직접 확인되는 것이 아니라 B3 convention으로부터 도출된 것입니다. Int32(0)/Float(0.0) bit-pattern이 일치하여 assert 없이는 무해하다는 전제 역시 마찬가지입니다. 수정된 IR 타입 연결은 diff에서 완전히 드러납니다.