CVE-2024-12692
Overview
Background
- Wasm canonical types
- V8 deduplicates structurally equivalent WebAssembly types into a global registry so that two modules declaring the “same” type share one
CanonicalTypeIndex, letting the engine treat their values as interchangeable. - `CanonicalStructType`
- the canonicalized representation of a Wasm
struct, holding the list of its field value types (fields()) and a parallel list of per-field mutability flags (mutabilities()). - Field mutability
- a boolean per struct field distinguishing an immutable field from a mutable one, which governs whether
struct.setis permitted and is part of a field’s subtyping/identity semantics. - Type confusion
- treating a value of one type as if it were an unrelated type, which in V8 lets an attacker forge object layouts and bypass memory-safety invariants.
Root Cause Analysis
When registering a Wasm struct type in the canonical type registry, V8’s CanonicalEquality::EqualStructType decided whether two struct types were identical so it could deduplicate them under a single canonical index. The vulnerable implementation compared only the two structs’ field value types via std::equal over type1.fields() and type2.fields(), and never compared their mutabilities().
As a result, two structs whose fields had identical types but differing mutability (for example one with an immutable field and one with a mutable field) were wrongly judged equal and collapsed to the same CanonicalTypeIndex, violating the invariant that a canonical type must fully capture a value’s observable type semantics including mutability. This let the type system treat an immutable-field object and a mutable-field object as the same type, enabling struct.set writes (or reference-subtyping decisions) that the immutable variant should have forbidden, i.e. a type confusion.
The fix adds a second std::equal comparison over type1.mutabilities() and type2.mutabilities(), so structs that differ only in mutability are no longer canonicalized together and the type identity again reflects the full field contract.
mutabilities() arrays to match.Attack Path
- Declare colliding struct types Craft a Wasm module defining two struct types with identical field value types but different mutability on at least one field (immutable vs. mutable).
- Trigger canonicalization
Instantiate the module so V8 registers the types through
CanonicalEquality::EqualStructType, which erroneously assigns both structs the sameCanonicalTypeIndex. - Alias across the confusion Use the shared canonical index to pass or store an object of the immutable-field type where the mutable-field type is expected (or vice versa), which the verifier now accepts as the same type.
- Perform an illegal mutation
Execute a
struct.set(or exploit relaxed subtyping) that mutates a field the true type declared immutable, corrupting an invariant the engine relied on. - Escalate to memory corruption Leverage the resulting type confusion to forge object contents or pointers, achieving out-of-bounds access and controlled read/write inside the renderer.
Impact Assessment
Files Changed
src/wasm/canonical-types.h
Audit Directions
- Incomplete structural equalityReview every canonical-type comparison (
EqualArrayType, function/signature equality, recursive-group hashing) to confirm each checks the full set of identity-bearing attributes, not just the obvious ones. - Type-property parity with hashingEnsure any hash function used to bucket canonical types incorporates the same fields (including mutability) as the equality predicate, since a mismatch reintroduces collisions.
- Mutability-sensitive invariantsHunt for other places that key subtyping, deduplication, or caching on field/element types while dropping mutability or nullability, especially in Wasm GC struct/array handling.
Patch
From 3852cf8b5bceed6a76e4537fc0aa191f9ed672a3 Mon Sep 17 00:00:00 2001 From: Clemens Backes <[email protected]> Date: Thu, 05 Dec 2024 19:41:22 +0100 Subject: [PATCH] [wasm] Fix comparison of canonical struct types In addition to the field types we should also check the mutability. [email protected] Fixed: 382291459 Change-Id: I46c5d9ece184a49699dd1c1e44c3b6f08646334b Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6074536 Reviewed-by: Jakob Kummerow <[email protected]> Commit-Queue: Clemens Backes <[email protected]> Cr-Commit-Position: refs/heads/main@{#97587} --- diff --git a/src/wasm/canonical-types.h b/src/wasm/canonical-types.h index 7c8618f..bcb6913 100644 --- a/src/wasm/canonical-types.h +++ b/src/wasm/canonical-types.h @@ -325,10 +325,15 @@ bool EqualStructType(const CanonicalStructType& type1, const CanonicalStructType& type2) const { - return std::equal( - type1.fields().begin(), type1.fields().end(), type2.fields().begin(), - type2.fields().end(), - std::bind_front(&CanonicalEquality::EqualValueType, this)); + return + // Compare fields, including a check that the size is the same. + std::equal( + type1.fields().begin(), type1.fields().end(), + type2.fields().begin(), type2.fields().end(), + std::bind_front(&CanonicalEquality::EqualValueType, this)) && + // Compare mutabilities, skipping the check for the size. + std::equal(type1.mutabilities().begin(), type1.mutabilities().end(), + type2.mutabilities().begin()); } bool EqualArrayType(const CanonicalArrayType& type1,
Original Bug Report
Arbitrary Wasm type confusion due to missing struct field mutability check on canonicalization
VULNERABILITY DETAILS
Summary
Arbitrary Wasm type confusion due to missing struct field mutability check on canonicalization. Mutability checks are missing for struct fields at CanonicalEquality::EqualStructType(), allowing type confusion between arbitrary Wasm types.
This is a variant of b/381696874.
Details
I’ve pointed out in b/381696874 that if we have a broken CanonicalEquality check that may be exploitable, we can use the birthday attack to cause a hash collision and trigger the bug. CanonicalEquality::EqualStructType() is missing mutability checks for its fields:
bool EqualStructType(const CanonicalStructType& type1,
const CanonicalStructType& type2) const {
return std::equal(
type1.fields().begin(), type1.fields().end(), type2.fields().begin(),
type2.fields().end(),
std::bind_front(&CanonicalEquality::EqualValueType, this));
}
This can be exploited by a casting chain of struct {const ref null none} -> struct {const ref null any} -> struct {mut ref null any} where the last cast is due to broken canonicalization, and the first cast is through legal subtype relationship. This allows us to overwrite ref null none with a value of ref null any type, resulting in arbitrary Wasm type confusion.
The attached PoC/exploit has a precomputed hash-colliding struct type that uses either const ref null any or mut ref null any for its 40 field types.
Bisect
Bug likely introduced by https://crrev.com/c/6049646 in M133 that attempts to fix b/379009132 by removing relative type indexs from canonical types. Note that the commit is already backported to M132 and M131.
VERSION
- M133: https://crrev.com/c/6049646
- M132: https://crrev.com/c/6049645
- M131: https://crrev.com/c/6049646
Chrome Version: 131.0.6778.108, 132.0.6834.32, 133.0.6848.0 ~ latest
Operating System: All
REPRODUCTION CASE
Attached as poc.js which exploits the hash collision + type confusion to obtain in-sandbox exploit primitives, and then crashes on arbitrary caged write attempt.
Also attached is yet another full exploit exp.html that pops calc on Windows x64 Chrome, tested against Canary 133.0.6877.0.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: Renderer
Crash State: Crashes on arbitrary caged write attempt from JIT-compiled Wasm function (on d8, poc.js), arbitrary code execution (on Chrome, exp.html)
CREDIT INFORMATION
Reporter credit: Seunghyun Lee (@0x10n) of CMU CyLab