High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker422313191
Fix commita970eed8995b (v8/v8) +6/-1
CISA KEVNot listed
CreditedSeunghyun Lee as part of TyphoonPWN 2025
Disclosed2025-06-10

Changed Functions

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

Files Changed

  • src/wasm/canonical-types.h
  • src/wasm/value-type.h
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

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