Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Sub-componentWASM
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: EqualValueType mechanism is correct. However the claim that old EqualTypeIndex mishandled one-in/one-out-of-recgroup indices is contradicted: the old code already returned false in both asymmetric cases; that hunk is a behavior-preserving refactor.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker381696874
Fix commita4d354fa54ba (v8/v8) +16/-11
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSeunghyun Lee (@0x10n)
Reported2024-12-02 · bounty $55000 (per vendor advisory)
Fix landed2024-12-02 (public git)
Disclosed2024-12-10
Patch gap8 days — fix public in source before the advisory

Background

WebAssembly type canonicalization
The process by which V8 collapses structurally-identical Wasm types across modules into a single CanonicalTypeIndex so that type checks reduce to integer comparisons.
`CanonicalValueType`
A compact encoding of a Wasm value type carrying a kind() (e.g. i32, ref) plus, for reference types, either an explicit type index (has_index()) or a generic heap representation.
Recgroup (recursion group)
A block of mutually-recursive Wasm types canonicalized together, within which type indices are compared relative to the group’s first index rather than absolutely.
`heap_representation()`
The abstract heap type (e.g. func, extern, any, none) of a generic (non-indexed) reference value type.

Root Cause Analysis

The vulnerable path is EqualValueType, used to decide whether two canonical Wasm value types are equivalent. The old code assumed the value kind() alone implied whether a type carried an index, so after matching kind() it only compared ref_index() when type1.has_index() was true — silently treating a mismatch in indexedness (one indexed, one generic) or a difference between two generic reference types as equal. In EqualTypeIndex, the comparison logic likewise mishandled the case where one index lived inside a recgroup and the other did not, because indexedness was inferred rather than checked symmetrically. The invariant violated is that two CanonicalValueTypes are equal only if they agree on indexedness and on either their relative type index (when indexed) or their heap representation (when generic).

The fix computes indexed from type1.has_index(), requires type2.has_index() to match, and, in the generic case, additionally compares heap_representation(); EqualTypeIndex is rewritten to compute relative_index once and require both operands to agree before comparing relative indices. This closes the confusion where two distinct types were reported equal.

Key insight
The single core mistake was inferring a value type’s indexedness from its kind() instead of checking has_index() on both operands, so distinct reference types (indexed vs. generic, or two different generic heap types) compared as equal; the fix makes indexedness an explicit, symmetric part of the equality test and adds a heap_representation() comparison for generic refs.

Attack Path

  1. Craft colliding types Author a Wasm module (or two) whose canonicalization produces value types that differ only in indexedness or generic heap representation, cases the old EqualValueType treated as equal.
  2. Trigger a type check Invoke an operation (e.g. a table or function reference use, or a subtype/equality check) that relies on canonical value-type equality to admit an object of one type where another is expected.
  3. Induce type confusion Because EqualValueType wrongly returns true, V8 accepts an object as a type it is not, so subsequent code accesses it through the wrong layout or interface.
  4. Corrupt memory Use the confused object to read or write outside its true bounds or reinterpret pointers, yielding a controlled out-of-bounds or fake-object primitive.
  5. Escalate Chain the primitive into arbitrary read/write and ultimately code execution within the renderer’s sandboxed process.

Impact Assessment

An attacker who can run WebAssembly in the victim’s browser gains a type-confusion primitive inside the V8 engine in the renderer process, convertible into out-of-bounds access and arbitrary read/write. The only precondition is loading an attacker-controlled page that instantiates a crafted Wasm module, with no special permissions required. Exploitation is confined to the renderer sandbox but provides a strong foundation for a full sandbox-escape chain.

Changed Functions

FunctionChangeNotes
if
src/wasm/canonical-types.h
modified

Files Changed

  • src/wasm/canonical-types.h

Audit Directions

  • Kind-implies-shape assumptions
    Search for equality or subtyping code that infers a type’s structure (indexedness, arity, layout) from a coarse tag like kind() rather than checking the discriminant on every operand.
  • Asymmetric operand checks
    Review comparison functions that test a property (has_index(), Contains()) on only one of two operands before dereferencing both, and ensure the property is validated symmetrically.
  • Generic reference comparisons
    Audit canonical/abstract reference-type equality across V8 Wasm to confirm generic heap types (func, extern, any, etc.) are distinguished by heap_representation() and never conflated when no index is present.
From a4d354fa54ba3e6a0f061c6b959be408ead5db95 Mon Sep 17 00:00:00 2001
From: Clemens Backes <clemensb@chromium.org>
Date: Mon, 02 Dec 2024 15:42:19 +0100
Subject: [PATCH] [wasm] Fix equality of canonical value types

We were incorrectly assuming that the value kind implies indexedness.
Fix this, and also compare generic (non-indexes) ref types.

R=jkummerow@chromium.org

Fixed: 381696874
Change-Id: Ie74ea8cb20d14f1b8d7d6a09701bc45cf91a913e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6063041
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#97503}
---

diff --git a/src/wasm/canonical-types.h b/src/wasm/canonical-types.h
index 2f5de53..a730f23 100644
--- a/src/wasm/canonical-types.h
+++ b/src/wasm/canonical-types.h
@@ -262,15 +262,15 @@
 
     bool EqualTypeIndex(CanonicalTypeIndex index1,
                         CanonicalTypeIndex index2) const {
-      if (recgroup1.Contains(index1)) {
-        // Compare relative supertypes in the recgroups.
-        if (!recgroup2.Contains(index2)) return false;
-        uint32_t rel_supertype1 = index1.index - recgroup1.first.index;
-        uint32_t rel_supertype2 = index2.index - recgroup2.first.index;
-        if (rel_supertype1 != rel_supertype2) return false;
-      } else {
-        if (recgroup2.Contains(index2)) return false;
-        if (index1 != index2) return false;
+      const bool relative_index = recgroup1.Contains(index1);
+      if (relative_index != recgroup2.Contains(index2)) return false;
+      if (relative_index) {
+        // Compare relative type indexes within the respective recgroups.
+        uint32_t rel_type1 = index1.index - recgroup1.first.index;
+        uint32_t rel_type2 = index2.index - recgroup2.first.index;
+        if (rel_type1 != rel_type2) return false;
+      } else if (index1 != index2) {
+        return false;
       }
       return true;
     }
@@ -303,8 +303,13 @@
     bool EqualValueType(CanonicalValueType type1,
                         CanonicalValueType type2) const {
       if (type1.kind() != type2.kind()) return false;
-      if (type1.has_index() &&
-          !EqualTypeIndex(type1.ref_index(), type2.ref_index())) {
+      const bool indexed = type1.has_index();
+      if (indexed != type2.has_index()) return false;
+      if (indexed) return EqualTypeIndex(type1.ref_index(), type2.ref_index());
+      const bool is_ref = type1.is_object_reference();
+      DCHECK_EQ(is_ref, type2.is_object_reference());
+      if (is_ref &&
+          type1.heap_representation() != type2.heap_representation()) {
         return false;
       }
       return true;
Loading diff…

Original Bug Report

reported by se...@gmail.com

Arbitrary Wasm type confusion due to improper fix of b/380397544

VULNERABILITY DETAILS

A bit unfortunate that even after a series of patches canonicalization is still broken, we really need a proactive approach rather than reactive bug discovery & patches.

Summary

Arbitrary Wasm type confusion due to improper fix of b/380397544 (which attempts to fix a broken patch for b/379009132, which in turn attempts to fix a broken patch for b/371565065 + b/354408144). HeapType checks are missing for generic Wasm heap types at CanonicalEquality::EqualValueType(), allowing type confusion between arbitrary Wasm types.

Details

I’ve pointed out in b/380397544 that after https://crrev.com/c/6035175 v8 does not distinguish between (relative) recursion group based index vs. (absolute) canonical index, which leads to different recursion group to be mistakenly canonicalized into the same index and thus lead to arbitrary type confusion between Wasm types. https://crrev.com/c/6048961 attempts to fix this by checking for relative types on type index comparison. Equality checks for CanonicalValueTypes go through the following code at CanonicalEquality::EqualValueType():

    bool EqualValueType(CanonicalValueType type1,
                        CanonicalValueType type2) const {
      if (type1.kind() != type2.kind()) return false;
      if (type1.has_index() &&
          !EqualTypeIndex(type1.ref_index(), type2.ref_index())) {
        return false;
      }
      return true;
    }

We see that if !type1.has_index(), that is, if the left-hand side of the equality comparison has a generic heap type, then the comparison always returns true no matter what the heap type of type2 is. This results in the equality comparator of Canonical(Singleton)Group to consider different reference types to be the same under the aformentioned case and thus may lead to arbitrary Wasm type confusion again.

However, this issue is not immediately evident as we use std::unordered_set<Canonical(Singleton)Group> to find pre-existing canonicalization results. This uses CanonicalHashing as a hashing function for the hashmap. Thus, to trigger this issue we must find two different recursion group that is considered equal by CanonicalEquality, but which at the same time also has a hash collision when hashed via CanonicalHashing.

CanonicalHashing uses base::Hasher which is based on MurmurHash64A and thus returns a 64bit hash value. Birthday attack allows us to find a collision in ~50% chance with 2^32 samples which is very feasible (in minutes, if not seconds). By precomputing offline the hash values for struct types that either has ref null any or ref null none as its fields and iterating this selection for >32 fields we can easily create >2^32 different inputs that are all considerered equal by CanonicalEquality, but which has random-ish hash values in which we are likely to find at least a single duplicate hash value. By using such precomputed colliding struct types we can canonicalize two different struct types into the same canonical index and cause arbitrary Wasm type confusion.

The attached PoC/exploit has a precomputed hash-colliding struct type that uses either ref null any or ref null none for its 40 field types. It tries two colliding pairs, one that works before https://crrev.com/c/6055121 and one that works after that as the patch affects hashing results.

Bisect

Bug introduced by https://crrev.com/c/6048961 in M133 that attempts to fix b/380397544 by accounting for relative type indices. Note that the commit is already backported to M132 and M131.

VERSION

Chrome Version: 133.0.6862.0 ~ latest / M131 head / M132 head
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.6871.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

View on issue tracker