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
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker422313191
Fix commita970eed8995b (v8/v8) +6/-1
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 as part of TyphoonPWN 2025
Reported2025-06-04 · bounty NA (per vendor advisory)
Fix landed2025-06-05 (public git)
Disclosed2025-06-10
Patch gap5 days — fix public in source before the advisory

Background

Canonical types (Wasm)
V8 canonicalizes WebAssembly recursive type groups so structurally identical types share one canonical index, and canonical-equality decides when two CanonicalValueTypes are considered the same during that process.
`CanonicalValueType` / `bit_field_`
a packed bit-field encoding a value type’s kind, nullability, ref-type-kind, exactness and, for indexed (reference) types, a type index in the kIndexBits sub-field.
Indexed type
a reference value type that carries a type index (has_index() true), accessed via ref_index(), as opposed to non-indexed primitive types compared directly by bit_field_.
`EqualTypeIndex`
a helper that compares only the two type indices for canonical equivalence.

Root Cause Analysis

CanonicalEquality::EqualValueType in src/wasm/canonical-types.h decides whether two value types are equal for canonicalization. For indexed types the pre-patch code returned EqualTypeIndex(type1.ref_index(), type2.ref_index()), comparing only the type index while ignoring every other bit of bit_field_ — nullability, RefTypeKind, exactness, etc. Two reference types that share the same index but differ in those non-index attributes were therefore treated as equal and canonicalized together, collapsing genuinely distinct types into one and violating the invariant that canonically-equal types are actually interchangeable.

The fix adds CanonicalValueType::is_equal_except_index (in src/wasm/value-type.h), which compares (bit_field_ & ~kIndexBits), and requires it to hold in addition to EqualTypeIndex, so the non-index bits must now match too. This restores the requirement that all type-defining bits, not just the index, agree before two indexed types are declared equal, preventing the type confusion.

Key insight
The single mistake was comparing only the type index for indexed types while discarding the remaining type-defining bits, so types differing in nullability/ref-kind/exactness were wrongly canonicalized as equal; the fix additionally requires is_equal_except_index so the non-index bits must match as well.

Attack Path

  1. Craft colliding types An attacker supplies a WebAssembly module defining two reference types that share the same type index representation but differ in non-index bit_field_ attributes (e.g. nullability or RefTypeKind).
  2. Canonicalization collapses them During type canonicalization, EqualValueType returns true on the index-only comparison, so the two distinct types are assigned the same canonical identity.
  3. Bypass a type check Later type/subtype or call-site checks that rely on canonical identity treat one type as the other, admitting a value where the differing attribute (e.g. a null, or a wrong ref kind) should have been rejected, producing type confusion (the advisory’s “Type Confusion in V8”).
  4. Corrupt via confused type (inferred) Operating on the value under the wrong type leads to memory misinterpretation; the concrete corruption sink is not shown in this diff.

Impact Assessment

An attacker able to load a crafted WebAssembly module in the renderer gains a type confusion between distinct canonical Wasm reference types, per the advisory (“Type Confusion in V8”), which typically enables memory corruption. The only precondition is the ability to instantiate attacker-controlled Wasm, reachable from ordinary web content. The advisory does not report in-the-wild exploitation, and escalation beyond the type confusion (read/write primitive, RCE, sandbox escape) is not demonstrated by this diff (inferred).

Changed Functions

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

Files Changed

  • src/wasm/canonical-types.h
  • src/wasm/value-type.h

Audit Directions

  • Partial bit-field comparisons
    Audit equality/canonicalization code that compares a masked or single sub-field (like an index) and confirm no type-defining bits are silently ignored.
  • Index-only equivalence
    Review other places using ref_index()/EqualTypeIndex or similar index-keyed comparisons to ensure attributes such as nullability, RefTypeKind and exactness are also considered.
  • Canonicalization invariants
    Look across Wasm type canonicalization and subtyping for spots where “same index” is treated as “same type,” which can merge distinct types and break downstream type assumptions.
From a970eed8995bb1b2cd083c3c79cdefc79e84f8b7 Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <[email protected]>
Date: Thu, 05 Jun 2025 14:28:19 +0200
Subject: [PATCH] [wasm] Fix CanonicalEquality::EqualValueType

For indexed types, the non-index bits also need to be equal.

Fixed: 422313191
Change-Id: I09489081cca4953aff0d642721376f24cf40bda5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6621628
Reviewed-by: Matthias Liedtke <[email protected]>
Commit-Queue: Matthias Liedtke <[email protected]>
Auto-Submit: Jakob Kummerow <[email protected]>
Cr-Commit-Position: refs/heads/main@{#100693}
---

diff --git a/src/wasm/canonical-types.h b/src/wasm/canonical-types.h
index b85597d..e05ed58 100644
--- a/src/wasm/canonical-types.h
+++ b/src/wasm/canonical-types.h
@@ -359,7 +359,8 @@
       const bool indexed = type1.has_index();
       if (indexed != type2.has_index()) return false;
       if (indexed) {
-        return EqualTypeIndex(type1.ref_index(), type2.ref_index());
+        return type1.is_equal_except_index(type2) &&
+               EqualTypeIndex(type1.ref_index(), type2.ref_index());
       }
       return type1 == type2;
     }
diff --git a/src/wasm/value-type.h b/src/wasm/value-type.h
index bdbdc3e..2eca8b4 100644
--- a/src/wasm/value-type.h
+++ b/src/wasm/value-type.h
@@ -1068,6 +1068,10 @@
     return bit_field_ == other.bit_field_;
   }
 
+  constexpr bool is_equal_except_index(CanonicalValueType other) const {
+    return (bit_field_ & ~kIndexBits) == (other.bit_field_ & ~kIndexBits);
+  }
+
   constexpr bool IsFunctionType() const {
     return ref_type_kind() == RefTypeKind::kFunction;
   }
Loading diff…

Original Bug Report

reported by [email protected]

Google Chrome RCE (no sandbox) via CanonicalEquality::EqualValueType()


Report description

Google Chrome RCE (no sandbox) via CanonicalEquality::EqualValueType()


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

TyphoonPWN 2025 Whitepaper

Submitted by Seunghyun Lee (@0x10n)

Target

Google Chrome RCE (no sandbox)

Repro (RCE)

  1. Run a webserver to serve the given exp.html file (e.g. python3 -m http.server -b 127.0.0.1 8000)
  2. Start Chrome with --no-sandbox flag
  3. Browse to http://127.0.0.1:8000/exp.html

Result should be a command prompt opening with arbitrary commands executed (echoing of some ASCII art followed by calc).

TL;DR

Initial Bug

WebAssembly type canonicalization bug again. CanonicalEquality::EqualValueType() does not take into account nullability for indexed reference types (ref $t0 vs. ref null $t0) due to a regression in 44171ac, causing nullability confusion for indexed reference types within a type definition. Canonicalization equivalence check bugs have been constantly pointed out as a significant problem (b/380397544, CVE-2024-12381, CVE-2024-12692, …) but still plagues the fundamental type safety guarantees of Wasm.

The bug is shadowed behind finding a hash collision between two rectypes that differ only by the nullability of indexed reference types. This has already been demonstrated in prior reports to be computationally feasible through birthday attack on MurmurHash64A.

V8 Sandbox bypass

> Note: Windows still does not have ShadowMetadata, so just using well-known attacks on PartitionAlloc (e.g. freelist hijack) suffices to bypass the v8 sandbox.

For the sakes of the contest, the exploit chain uses a novel V8 Sandbox bypass using JSPI to acquire full control over the stack. This is a variant of b/384553540 which has been reintroduced due to an improper fix in https://crrev.com/c/6426203, where an attacker may exploit memory corruption within the V8 Sandbox to skip over inactive secondary stacks (switched to the central stack for JS execution) and resume them later on after clobbering the stack contents.

Bug / Root Cause Analysis

This is a variant of the never-ending Wasm type canonicalization equivalence check bugs. CanonicalEquality::EqualValueType() does not take into account nullability for indexed reference types as shown below:

    bool EqualValueType(CanonicalValueType type1,
                        CanonicalValueType type2) const {
      const bool indexed = type1.has_index();
      if (indexed != type2.has_index()) return false;
      if (indexed) {
        return EqualTypeIndex(type1.ref_index(), type2.ref_index());        // [!] only checks type index equivalence and nothing else
      }
      return type1 == type2;
    }

This puts us in the same position as many prior bugs that I’ve reported, e.g. CVE-2024-12381 and CVE-2024-12692, where we have a CanonicalEquality bug which is shadowed behind finding a hash collision for CanonicalHashing due to the use of std::unordered_set<CanonicalGroup> for TypeCanonicalizer::canonical_group_.

Simply put, if we find two CanonicalGroup that differs only in the nullability for its indexed reference value types but which hashes into the same hash value, then the two recgroup canonicalizes into the same canonical index which results in nullability confusion.

Exploit

Hash Collision

As also described in previous reports (now public), birthday attack makes it computationally feasible to find such recgroups. We compute hash values for the following recgroup structure:

- t0: array { mut kWasmI32 }
- t1: struct { mut kWasmI64 * 8191, mut kWasmI32, mut ref null $t0 }
- t2: struct { mut {kWasmI32, kWasmI64} * BASE_RND_FIELDS, mut ref null? $t1 * BRUTE_FIELDS }

mut {kWasmI32, kWasmI64} * BASE_RND_FIELDS is fixed for each collision attempt. mut ref null? $t1 * BRUTE_FIELDS is the brute-force field where we try all 2^BRUTE_FIELDS possible combinations of nullabilities. Birthday attack gives us that with BRUTE_FIELDS of only around 32 we have 2^32 64-bit hash values that is highly likely to have at least one hash collision, where we can take the colliding pairs to confuse ref null $t1 into ref $t1.

> Note: The CanonicalEquality bug given here allows a straightforward construction that admits birthday attack. Even when we have a stronger constraint of a preimage attack, it is still computationally feasible to find a suitable preimage by a meet-in-the-middle attack.

WasmNull to in-sandbox primitives

The previous primitive grants us the ability to invalidly cast ref null $t1 into a ref $t1, essentially removing any nullity checks on following operations. A straightforward approach is to then cast a null value into a non-null type, that is, WasmNull: ref $t1. This requires us to look into what WasmNull is.

WasmNull is just another object at 0xfffd, but followed by a 0x10000 guard page region for implicit null trapping via segfault handlers. Struct $t1 simply skips over this region and positions the last ref null $t0 field at offset 0x20000, which holds MetaMap (null) - we now consider this as a WasmArray object of mutable i32 array, where the object conveniently has WasmArray::length of 0x0c000112 (specific values might vary across v8 versions). This translates to caged read/write primitive of up to approximately offset 0x30000000, which is sufficient for our needs.

Similar to the exploit demonstrated in CVE-2024-9602, we simply allocate a large array of size 0x10000000 / 4 and use the caged read/write primitive to directly read and write on this array, granting us addrof/fakeobj primitive.

> Note: We can choose a better object, e.g. undefined at 0x11 which has a larger “length” field, to obtain full caged read/write on the whole pointer compression cage. For the purposes of this exploit it is unnecessary.

V8 Sandbox bypass by exploiting JSPI stack switching

M137 enables Wasm JSPI by default. We exploit the stack switching behavior in JSPI to switch to an invalid stack of fully controlled values.

On entering a WebAssembly.promising function from JS, a secondary stack is allocated and the stack is pivoted to continue execution within Wasm on the secondary stack. On returning back to JS or re-entering JS (e.g. by imported JS function execution), the stack switches back to the central (JS) stack. These secondary stacks may be nested, for example in the following case:

  1. Enter promising function f1
    • This causes a stack switch to the secondary stack #0
  2. Execute imported JS function imp1 within Wasm
    • This causes a stack switch to the central stack
  3. Enter promising function f2 from imp1
    • This causes a stack switch to the secondary stack #1
  4. …

On returns we must unwind any stack switches that it has done in the correct order. For example, returning back from steps 3 -> 2 -> 1 -> JS should first switch back from secondary stack #1 to the central stack used by step 2 (3 -> 2), then switch back to the secondary stack #0 (2 -> 1), then return to the position in the central stack just before entering secondary stack #0 (1 -> JS).

This whole feature is very fragile in terms of the V8 Sandbox as a slightly faulty operation may directly lead to full stack control, and indeed has led to various bypasses. The initial implementation had no notion of stack state and allowed an attacker to switch to any stack (b/356419168). This has been mitigated by adding four stack states: Active, Suspended, Inactive and Retired, and verifying the stack state whenever switching to/from a stack.

However, this was still vulnerable to an intra-state confusion attack where an attacker may return to any compatible stack state in an invalid order: for example in an inactive stack chain of S0 -> S1 -> S2, it was possible to skip S1 and directly return to S0 from S2 and later on return to S1 after clobbering its stack contents (b/384553540). This has been fixed in 9942165 by enforcing the chain of stacks.

Interestingly, after https://crrev.com/c/6426203 which attempts to fix b/404285918 and JSPI-related V8 Sandbox issues once and for all, the mitigation for b/384553540 has been demoted to a DCHECK() from a SBXCHECK() - this reintroduces a variant of b/384553540. Thus, the latter part of the exploit simply creates a stack chain, skips a few stacks in between (which effectively “suspends” frames within the central stack), clobber the “suspended” frame by spraying the stack with Wasm i64 locals, then returning back to the “suspended” frame which now holds attacker-controlled stack values. Spray the stack with retsled + ropchain to VirtualProtect shellcode as executable, then return to the shellcode to gain arbitrary code execution. Addresses can be read from PartitionAlloc metadata region as usual regardless of whether or not ShadowMetadata is implemented / in effect.

Affected Version

CanonicalEquality::EqualValueType nullity check bug

All Chrome builds having the ValueType refactoring commit 44171ac - M135 and above in stable channel.

Prior to the commit, CanonicalEquality::EqualValueType() had a ValueKind check that enforces nullity equivalence (kRef vs kRefNull).

V8 Sandbox bypass via JSPI intra-state confusion

All Chrome builds having commit d4700da - M137 and above in stable channel. This also conveniently coincides with JSPI being enabled by default (w/o the need of origin trials) from M137 and above.

Impact analysis – Please briefly explain who can exploit the vulnerability, and what they gain when doing so

The vulnerability allows an attacker that has convinced a victim to visit a certain web site to cause his Chrome to execute arbitrary code (though an additional bug would be needed to bypass the SBX in order to make this a fully capable RCE)


The cause

What version of Chrome have you found the security issue in?

All Chrome builds having the ValueType refactoring commit [44171ac]

No, it is not related to a crash.

Choose the type of vulnerability

Remote Code Execution (RCE)

How would you like to be publicly acknowledged for your report?

Seunghyun Lee - TyphoonPWN 2025

View on issue tracker