CVE-2024-12053
Overview
Background
- Type canonicalizer
- The
TypeCanonicalizercomponent insrc/wasm/canonical-types.ccmaps each WebAssembly module’s local type definitions to a single global set ofCanonicalTypeIndexvalues so that structurally identical types across modules are treated as equal. - Recursion group
- A block of mutually recursive Wasm type definitions canonicalized together, whose members may refer to one another; in the diff its bounds are carried via
first_new_canonical_indexandModuleTypeIndex{start_index}. - Relative type index
- A type reference expressed as an offset within its own recursion group rather than an absolute canonical index, previously flagged by the
CanonicalRelativeFieldbit inValueTypeBaseand theis_relative_supertypefield ofCanonicalType. - Absolute canonical index
- A
CanonicalTypeIndexvalid across the whole engine, computed fromcanonical_supertypes_.size(), that every caller of the canonicalizer expects to receive.
Root Cause Analysis
During canonicalization the TypeCanonicalizer stored some type references as relative indexes tagged by is_relative_supertype and the CanonicalRelativeField bit, converting them to absolute indexes only inside AddRecursiveGroup via canonical_type.supertype.index + first_canonical_index. Because the relative form was carried on CanonicalType/ValueTypeBase rather than being confined to the canonicalizer, these relative indexes could leak out to callers who assumed every returned index was absolute, violating the invariant that a CanonicalTypeIndex is globally meaningful. A caller interpreting a relative index as an absolute one would resolve it to the wrong canonical type, producing type confusion.
The fix removes the concept entirely: the recursion group’s start and end (first_new_canonical_index, ModuleTypeIndex{start_index}) are threaded into CanonicalizeTypeDef, hashing, and equality so relative indexes are computed on demand only during comparison, while the stored canonical_supertypes_[...] always holds canonical_type.supertype as an absolute index.
CanonicalTypeIndex; the fix eliminates the relative representation and always stores absolute indexes, deriving relative positions transiently during canonicalization only.Attack Path
- Craft a Wasm module
An attacker supplies a WebAssembly module with a recursion group whose type definitions reference one another via relative indexes, exercising the
AddRecursiveGroupcanonicalization path. - Trigger canonicalization
Instantiation drives
CanonicalizeTypeDefandAddRecursiveGroup, where a supertype flaggedis_relative_supertypeis stored without being resolved to an absoluteCanonicalTypeIndex. - Leak the relative index
The canonical type carrying the relative index and
CanonicalRelativeFieldbit is returned to a caller that assumes the index is absolute. - Induce type confusion The caller resolves the relative index as an absolute canonical id, associating operations with the wrong type and confusing one type for another.
- Exploit the confusion The mismatched type layout enables out-of-bounds or otherwise illegal memory access within the sandbox, corrupting engine state.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/wasm/canonical-types.cc |
modified | |
ifsrc/wasm/canonical-types.cc |
modified |
Files Changed
src/base/bounds.hsrc/wasm/canonical-types.cc
Audit Directions
- Leaking internal representationsLook for other places where a canonicalizer or interner returns objects still carrying a local/relative form that callers assume is fully resolved to a global identifier.
- Relative vs absolute indexesAudit remaining Wasm type paths for any surviving offset-based index arithmetic (e.g. adding a base like
first_canonical_index) where an absoluteCanonicalTypeIndexis expected. - On-demand vs stored invariantsReview data structures where a transient computed property (relative position) is mistakenly persisted onto long-lived stored objects, and confirm stored fields always hold the absolute, caller-safe value.
Patch
From 20d9a7f760c018183c836283017a321638b66810 Mon Sep 17 00:00:00 2001 From: Clemens Backes <[email protected]> Date: Tue, 19 Nov 2024 18:17:33 +0100 Subject: [PATCH] [wasm] Remove relative type indexes from canonical types Those relative types were leaking from the type canonicalizer, which leads to type confusion in callers. This CL fully removes the concept of relative type indexes (and thus removes the `CanonicalRelativeField` bit from the bitfield in `ValueTypeBase`). During canonicalization we pass the start and end of the recursion group into hashing and equality checking, and use this to compute relative indexes within the recursion group on demand. The stored version will always have absolute indexes though. [email protected] Bug: 379612177 Change-Id: I24154785c38dd3d8abb3d252bef4752024bad223 Fixed: 379009132 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6035175 Reviewed-by: Jakob Kummerow <[email protected]> Commit-Queue: Clemens Backes <[email protected]> Cr-Commit-Position: refs/heads/main@{#97279} --- diff --git a/src/base/bounds.h b/src/base/bounds.h index 85f7bba..1646e81 100644 --- a/src/base/bounds.h +++ b/src/base/bounds.h @@ -14,9 +14,11 @@ // Checks if value is in range [lower_limit, higher_limit] using a single // branch. template <typename T, typename U> + requires((std::is_integral_v<T> || std::is_enum_v<T>) && + (std::is_integral_v<U> || std::is_enum_v<U>)) && + (sizeof(U) <= sizeof(T)) inline constexpr bool IsInRange(T value, U lower_limit, U higher_limit) { DCHECK_LE(lower_limit, higher_limit); - static_assert(sizeof(U) <= sizeof(T)); using unsigned_T = typename std::make_unsigned<T>::type; // Use static_cast to support enum classes. return static_cast<unsigned_T>(static_cast<unsigned_T>(value) - @@ -27,10 +29,12 @@ // Like IsInRange but for the half-open range [lower_limit, higher_limit). template <typename T, typename U> + requires((std::is_integral_v<T> || std::is_enum_v<T>) && + (std::is_integral_v<U> || std::is_enum_v<U>)) && + (sizeof(U) <= sizeof(T)) inline constexpr bool IsInHalfOpenRange(T value, U lower_limit, U higher_limit) { DCHECK_LE(lower_limit, higher_limit); - static_assert(sizeof(U) <= sizeof(T)); using unsigned_T = typename std::make_unsigned<T>::type; // Use static_cast to support enum classes. return static_cast<unsigned_T>(static_cast<unsigned_T>(value) - diff --git a/src/wasm/canonical-types.cc b/src/wasm/canonical-types.cc index 3443018..2ecb78b 100644 --- a/src/wasm/canonical-types.cc +++ b/src/wasm/canonical-types.cc @@ -43,11 +43,17 @@ // Multiple threads could try to register recursive groups concurrently. // TODO(manoskouk): Investigate if we can fine-grain the synchronization. base::MutexGuard mutex_guard(&mutex_); + // Compute the first canonical index in the recgroup in the case that it does + // not already exist. + CanonicalTypeIndex first_new_canonical_index{ + static_cast<uint32_t>(canonical_supertypes_.size())}; + DCHECK_GE(module->types.size(), start_index + size); - CanonicalGroup group{&zone_, size}; + CanonicalGroup group{&zone_, size, first_new_canonical_index}; for (uint32_t i = 0; i < size; i++) { - group.types[i] = CanonicalizeTypeDef(module, module->types[start_index + i], - start_index); + group.types[i] = CanonicalizeTypeDef( + module, ModuleTypeIndex{start_index + i}, ModuleTypeIndex{start_index}, + first_new_canonical_index); } if (CanonicalTypeIndex canonical_index = FindCanonicalGroup(group); canonical_index.valid()) { @@ -62,22 +68,13 @@ // allocated in {CanonicalizeTypeDef{). return; } - // Identical group not found. Add new canonical representatives for the new - // types. - uint32_t first_canonical_index = - static_cast<uint32_t>(canonical_supertypes_.size()); - canonical_supertypes_.resize(first_canonical_index + size); + canonical_supertypes_.resize(first_new_canonical_index.index + size); CheckMaxCanonicalIndex(); for (uint32_t i = 0; i < size; i++) { CanonicalType& canonical_type = group.types[i]; - // Compute the canonical index of the supertype: If it is relative, we - // need to add {first_canonical_index}. - canonical_supertypes_[first_canonical_index + i] = - canonical_type.is_relative_supertype - ? CanonicalTypeIndex{canonical_type.supertype.index + - first_canonical_index} - : canonical_type.supertype; - CanonicalTypeIndex canonical_id{first_canonical_index + i}; + canonical_supertypes_[first_new_canonical_index.index + i] = + canonical_type.supertype; + CanonicalTypeIndex canonical_id{first_new_canonical_index.index + i}; module->isorecursive_canonical_type_ids[start_index + i] = canonical_id; if (canonical_type.kind == CanonicalType::kFunction) { const CanonicalSig* sig = canonical_type.function_sig; @@ -85,15 +82,13 @@ } } // Check that this canonical ID is not used yet. - DCHECK(std::none_of(canonical_singleton_groups_.begin(), - canonical_singleton_groups_.end(), [=](auto& entry) { - return entry.second.index == first_canonical_index; - })); - DCHECK(std::none_of(canonical_groups_.begin(), canonical_groups_.end(), - [=](auto& entry) { - return entry.second.index == first_canonical_index; - })); - canonical_groups_.emplace(group, CanonicalTypeIndex{first_canonical_index}); + DCHECK(std::none_of( + canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(), + [=](auto& entry) { return entry.index == first_new_canonical_index; })); + DCHECK(std::none_of( + canonical_groups_.begin(), canonical_groups_.end(), + [=](auto& entry) { return entry.start == first_new_canonical_index; })); + canonical_groups_.emplace(group); } void TypeCanonicalizer::AddRecursiveSingletonGroup(WasmModule* module) { @@ -105,8 +100,11 @@ uint32_t start_index) { base::MutexGuard guard(&mutex_); DCHECK_GT(module->types.size(), start_index); - CanonicalTypeIndex canonical_index = AddRecursiveGroup( - CanonicalizeTypeDef(module, module->types[start_index], start_index)); + CanonicalTypeIndex first_new_canonical_index{ + static_cast<uint32_t>(canonical_supertypes_.size())}; + CanonicalTypeIndex canonical_index = AddRecursiveGroup(CanonicalizeTypeDef( + module, ModuleTypeIndex{start_index}, ModuleTypeIndex{start_index}, + first_new_canonical_index)); module->isorecursive_canonical_type_ids[start_index] = canonical_index; } @@ -118,7 +116,6 @@ #endif const bool kFinal = true; const bool kNotShared = false; - const bool kNonRelativeSupertype = false; // Because of the checks above, we can treat the type_def as canonical. // TODO(366180605): It would be nice to not have to rely on a cast here. // Is there a way to avoid it? In the meantime, these asserts provide at @@ -127,13 +124,14 @@ static_assert(CanonicalValueType::Primitive(kI32).raw_bit_field() == ValueType::Primitive(kI32).raw_bit_field()); CanonicalType canonical{reinterpret_cast<const CanonicalSig*>(sig), - CanonicalTypeIndex{kNoSuperType}, kFinal, kNotShared, - kNonRelativeSupertype}; + CanonicalTypeIndex{kNoSuperType}, kFinal, kNotShared}; base::MutexGuard guard(&mutex_); // Fast path lookup before canonicalizing (== copying into the // TypeCanonicalizer's zone) the function signature. - CanonicalTypeIndex index = - FindCanonicalGroup(CanonicalSingletonGroup{canonical}); + CanonicalTypeIndex hypothetical_new_canonical_index{ + static_cast<uint32_t>(canonical_supertypes_.size())}; + CanonicalTypeIndex index = FindCanonicalGroup( + CanonicalSingletonGroup{canonical, hypothetical_new_canonical_index}); if (index.valid()) return index; // Copy into this class's zone, then call the generic {AddRecursiveGroup}. CanonicalSig::Builder builder(&zone_, sig->return_count(), @@ -145,12 +143,16 @@ builder.AddParam(CanonicalValueType{param}); } canonical.function_sig = builder.Get(); - return AddRecursiveGroup(canonical); + CanonicalTypeIndex canonical_index = AddRecursiveGroup(canonical); + DCHECK_EQ(canonical_index, hypothetical_new_canonical_index); + return canonical_index; } CanonicalTypeIndex TypeCanonicalizer::AddRecursiveGroup(CanonicalType type) { mutex_.AssertHeld(); // The caller must hold the mutex. - CanonicalSingletonGroup group{type}; + CanonicalTypeIndex new_canonical_index{ + static_cast<uint32_t>(canonical_supertypes_.size())}; + CanonicalSingletonGroup group{type, new_canonical_index}; if (CanonicalTypeIndex index = FindCanonicalGroup(group); index.valid()) { // Make sure this signature can be looked up later. DCHECK_IMPLIES(type.kind == CanonicalType::kFunction, @@ -158,26 +160,21 @@ return index; } static_assert(kMaxCanonicalTypes <= kMaxUInt32); - CanonicalTypeIndex index{static_cast<uint32_t>(canonical_supertypes_.size())}; // Check that this canonical ID is not used yet. - DCHECK(std::none_of(canonical_singleton_groups_.begin(), - canonical_singleton_groups_.end(),
Regression Test / PoC
diff --git a/test/unittests/wasm/subtyping-unittest.cc b/test/unittests/wasm/subtyping-unittest.cc
index 4d02be7..33a8649 100644
--- a/test/unittests/wasm/subtyping-unittest.cc
+++ b/test/unittests/wasm/subtyping-unittest.cc
@@ -74,9 +74,15 @@
// Set up two identical modules.
for (WasmModule* module : {module1, module2}) {
- /* 0 */ DefineStruct(module, {mut(ref(2)), immut(refNull(2))});
- /* 1 */ DefineStruct(module, {mut(ref(2)), immut(ref(2))}, Idx{0});
- /* 2 */ DefineArray(module, immut(ref(0)));
+ // Three mutually recursive types.
+ /* 0 */ DefineStruct(module, {mut(ref(2)), immut(refNull(2))},
+ kNoSuperType, false, false, false);
+ /* 1 */ DefineStruct(module, {mut(ref(2)), immut(ref(2))}, Idx{0}, false,
+ false, false);
+ /* 2 */ DefineArray(module, immut(ref(0)), kNoSuperType, false, false,
+ false);
+ GetTypeCanonicalizer()->AddRecursiveGroup(module, 3);
+
/* 3 */ DefineArray(module, immut(ref(1)), Idx{2});
/* 4 */ DefineStruct(module, {mut(ref(2)), immut(ref(3)), immut(kWasmF64)},
Idx{1});
Original Bug Report
Potential type confusion in wasm and js interaction
VULNERABILITY DETAILS
It crashes when calling toString() on a wasm function’s return value in javascript (the last line of the poc).
VERSION
v8 Version: commit 7a9e78e98f59b7adf79e6ead0459718e4ed249e7 (Nov 14 2024)
Operating System: Ubuntu Linux 5.4.0-167-generic
REPRODUCTION CASE
gn gen out/release
./out/release/d8 --jit-fuzzing ./poc.js
Note that the flag --jit-fuzzing is necessary for reproducing the crash.
ADDITIONAL INFORMATION
provided in the attached crash.log