High patch-now ✔︎ verified CVSS 8.8 chrome Type Confusion 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
Sub-componentObjects
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.
Tracker560121552
Fix commit22e5290486f4 (v8/v8) +93/-0
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
CreditedGoogle
Reported2026-09-11 (per vendor advisory)
Fix landed2026-09-17 (public git)
Disclosed2026-09-17

Background

Wasm object
a WebAssembly GC value (struct or array) exposed to JavaScript as an opaque object with a fixed, immutable memory layout described by its Map.
`Map` transition
the process by which V8 changes an object’s hidden class (Map) to a new one when a property is added, giving the object a different field layout.
Private symbol store
writing a property keyed by a private symbol (including class #field return-override), which normally triggers a layout-extending Map transition via LookupIterator.
`ExtendingNonExtensible`
the LookupIterator predicate that decides whether adding a private field to a non-extensible receiver is permitted or must be blocked.

Root Cause Analysis

When a private symbol (or a class #field via constructor return-override) was stored on a Wasm struct or array, the store flowed through LookupIterator and reached ExtendingNonExtensible in lookup-inl.h, which only special-cased always-shared-space objects and otherwise allowed the private field to be added. Because Wasm objects have a fixed layout tied to their Map, transitioning that Map to add a field violated the invariant that a Wasm object’s Map must never change, leaving an object whose runtime Map no longer matched its true Wasm type. This produced a type confusion: subsequent code trusting the object’s Wasm type could operate on a mismatched layout.

The fix adds an IsWasmObjectMap(receiver_map) check (guarded by V8_ENABLE_WEBASSEMBLY) that returns true early, treating Wasm objects like shared objects so the store throws a TypeError instead of transitioning the Map. A corresponding DCHECK(!IsWasmObject(*receiver)) in lookup.cc enforces that a Wasm receiver never reaches the transition path.

Key insight
The single mistake was that ExtendingNonExtensible guarded shared-space objects but not Wasm objects, so private-field stores could transition a Wasm object’s fixed Map; the fix closes the gap by disallowing map transitions for any IsWasmObjectMap receiver, matching the existing shared-object rule.

Attack Path

  1. Obtain a Wasm GC object Instantiate a module exporting functions that return a struct or array and get a live Wasm object reference in JavaScript.
  2. Create a private key Produce a private symbol (or use a class with a #field and a Base constructor that returns the Wasm object via return-override).
  3. Store the private field Assign through the private key, driving the store into LookupIterator and reaching ExtendingNonExtensible.
  4. Force the illegal transition With the pre-patch code, the store is permitted and transitions the Wasm object’s Map to a new extended layout that no longer matches its Wasm type.
  5. Exploit the confusion Manipulate the object through code that relies on its original Wasm type/layout, achieving type confusion between the transitioned Map and the object’s true fixed layout.

Impact Assessment

An attacker running attacker-controlled JavaScript in the renderer process gains a type confusion in V8 by corrupting a Wasm object’s Map, which can be leveraged toward memory corruption and further exploitation. The only precondition is the ability to execute script that instantiates a WebAssembly GC module and perform a private-symbol/#field store on a returned object. It requires no special privileges beyond ordinary in-page script execution.

Changed Functions

FunctionChangeNotes
Base
test/mjsunit/wasm/gc-js-interop-private-symbols.js
modified
constructor
test/mjsunit/wasm/gc-js-interop-private-symbols.js
modified
DerivedWithPrivateField
test/mjsunit/wasm/gc-js-interop-private-symbols.js
modified

Files Changed

  • src/objects/lookup.cc
  • src/objects/lookup-inl.h
  • test/mjsunit/wasm/gc-js-interop-private-symbols.js

Audit Directions

  • Fixed-layout receivers in transition paths
    Audit every LookupIterator predicate and transition site that special-cases shared objects to confirm Wasm objects and other fixed-layout receivers are equally excluded.
  • Private-symbol and return-override stores
    Review paths that add private fields (private symbols, class #field, constructor return-override) for objects whose Map must remain immutable.
  • Opaque V8 object types
    Look for other object classes with a fixed Map-tied layout (shared objects, Wasm, sandboxed externals) that could be mutated by property-add operations lacking a guard.
From 22e5290486f46e9c908dbb840ca7738afb23797b Mon Sep 17 00:00:00 2001
From: Daniel Lehmann <[email protected]>
Date: Thu, 17 Sep 2026 15:50:35 +0200
Subject: [PATCH] [objects] Forbid adding private symbols to Wasm objects

Wasm objects have a fixed layout and must not transition their maps.
Harden LookupIterator::ExtendingNonExtensible by generally disallowing
map transitions for Wasm objects. This matches the existing check for
shared objects. This causes private symbol stores on Wasm objects to
throw a TypeError instead of transitioning their map.

[email protected], [email protected]
TAG=agy
CONV=c66784a6-5c96-4c98-9763-0148a1c0b03d

Bug: 560121552
Change-Id: I2c569670746901123359a9abce04fbbd55f1719d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8421259
Reviewed-by: Leszek Swirski <[email protected]>
Auto-Submit: Daniel Lehmann <[email protected]>
Commit-Queue: Daniel Lehmann <[email protected]>
Reviewed-by: Jakob Kummerow <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109922}
---
diff --git a/src/objects/lookup-inl.h b/src/objects/lookup-inl.h
index c737386..45a4cf4 100644
--- a/src/objects/lookup-inl.h
+++ b/src/objects/lookup-inl.h
@@ -294,6 +294,13 @@
   if (IsAlwaysSharedSpaceJSObjectMap(receiver_map)) {
     return true;
   }
+#if V8_ENABLE_WEBASSEMBLY
+  // Wasm objects have a fixed layout and must never transition their map.
+  if (IsWasmObjectMap(receiver_map)) {
+    return true;
+  }
+#endif  // V8_ENABLE_WEBASSEMBLY
+
   // Extending non-extensible objects with private fields is currently allowed,
   // but we're disallowing it soon.
   DCHECK(!receiver_map->is_extensible());
diff --git a/src/objects/lookup.cc b/src/objects/lookup.cc
index 07834bc..872d78c 100644
--- a/src/objects/lookup.cc
+++ b/src/objects/lookup.cc
@@ -672,6 +672,9 @@
   DCHECK_IMPLIES(!receiver.is_identical_to(GetStoreTarget<JSReceiver>()),
                  name_for_transition()->IsAnyPrivateName());
   DCHECK(!IsAlwaysSharedSpaceJSObject(*receiver));
+#if V8_ENABLE_WEBASSEMBLY
+  DCHECK(!IsWasmObject(*receiver));
+#endif
   if (state_ == TRANSITION) return;
 
   if (!IsElement() && name_for_transition()->IsAnyPrivate()) {
diff --git a/test/mjsunit/wasm/gc-js-interop-private-symbols.js b/test/mjsunit/wasm/gc-js-interop-private-symbols.js
new file mode 100644
index 0000000..9215406
--- /dev/null
+++ b/test/mjsunit/wasm/gc-js-interop-private-symbols.js
@@ -0,0 +1,83 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+// Wasm objects are opaque and have a fixed layout; their maps must never
+// transition and no properties may be added to them.
+
+const builder = new WasmModuleBuilder();
+const $struct = builder.addStruct([makeField(kWasmI64, true)]);
+const $array = builder.addArray(kWasmI64);
+
+builder.addFunction('makeStruct', makeSig([], [wasmRefType($struct)]))
+    .exportFunc()
+    .addBody([
+      ...wasmI64Const(0),
+      kGCPrefix, kExprStructNew, $struct,
+    ]);
+
+builder.addFunction('makeArray', makeSig([], [wasmRefType($array)]))
+    .exportFunc()
+    .addBody([
+      ...wasmI64Const(0),
+      ...wasmI32Const(1),
+      kGCPrefix, kExprArrayNew, $array,
+    ]);
+
+builder.addFunction('isStruct', makeSig([kWasmExternRef], [kWasmI32]))
+    .exportFunc()
+    .addBody([
+      kExprLocalGet, 0,
+      kGCPrefix, kExprAnyConvertExtern,
+      kGCPrefix, kExprRefTest, $struct,
+    ]);
+
+builder.addFunction('isArray', makeSig([kWasmExternRef], [kWasmI32]))
+    .exportFunc()
+    .addBody([
+      kExprLocalGet, 0,
+      kGCPrefix, kExprAnyConvertExtern,
+      kGCPrefix, kExprRefTest, $array,
+    ]);
+
+const instance = builder.instantiate();
+const privateSymbol = %CreatePrivateSymbol('private');
+
+// Structs and arrays must retain their type identity after failed assignment.
+const wasmStruct = instance.exports.makeStruct();
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+assertThrows(() => wasmStruct[privateSymbol] = 1, TypeError);
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+
+const wasmArray = instance.exports.makeArray();
+assertEquals(1, instance.exports.isArray(wasmArray));
+assertThrows(() => wasmArray[privateSymbol] = 1, TypeError);
+assertEquals(1, instance.exports.isArray(wasmArray));
+
+// Standard JS private fields (#field) on class return-override must also throw
+// and preserve type identity.
+class Base {
+  constructor(o) {
+    return o;
+  }
+}
+class DerivedWithPrivateField extends Base {
+  #field = 42;
+}
+
+assertThrows(() => new DerivedWithPrivateField(wasmStruct), TypeError);
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+
+assertThrows(() => new DerivedWithPrivateField(wasmArray), TypeError);
+assertEquals(1, instance.exports.isArray(wasmArray));
+
+// Identity hashes used by WeakMap/WeakSet must continue to work.
+const wm = new WeakMap();
+wm.set(wasmStruct, 'struct-value');
+wm.set(wasmArray, 'array-value');
+assertEquals('struct-value', wm.get(wasmStruct));
+assertEquals('array-value', wm.get(wasmArray));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/wasm/gc-js-interop-private-symbols.js b/test/mjsunit/wasm/gc-js-interop-private-symbols.js
new file mode 100644
index 0000000..9215406
--- /dev/null
+++ b/test/mjsunit/wasm/gc-js-interop-private-symbols.js
@@ -0,0 +1,83 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+// Wasm objects are opaque and have a fixed layout; their maps must never
+// transition and no properties may be added to them.
+
+const builder = new WasmModuleBuilder();
+const $struct = builder.addStruct([makeField(kWasmI64, true)]);
+const $array = builder.addArray(kWasmI64);
+
+builder.addFunction('makeStruct', makeSig([], [wasmRefType($struct)]))
+    .exportFunc()
+    .addBody([
+      ...wasmI64Const(0),
+      kGCPrefix, kExprStructNew, $struct,
+    ]);
+
+builder.addFunction('makeArray', makeSig([], [wasmRefType($array)]))
+    .exportFunc()
+    .addBody([
+      ...wasmI64Const(0),
+      ...wasmI32Const(1),
+      kGCPrefix, kExprArrayNew, $array,
+    ]);
+
+builder.addFunction('isStruct', makeSig([kWasmExternRef], [kWasmI32]))
+    .exportFunc()
+    .addBody([
+      kExprLocalGet, 0,
+      kGCPrefix, kExprAnyConvertExtern,
+      kGCPrefix, kExprRefTest, $struct,
+    ]);
+
+builder.addFunction('isArray', makeSig([kWasmExternRef], [kWasmI32]))
+    .exportFunc()
+    .addBody([
+      kExprLocalGet, 0,
+      kGCPrefix, kExprAnyConvertExtern,
+      kGCPrefix, kExprRefTest, $array,
+    ]);
+
+const instance = builder.instantiate();
+const privateSymbol = %CreatePrivateSymbol('private');
+
+// Structs and arrays must retain their type identity after failed assignment.
+const wasmStruct = instance.exports.makeStruct();
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+assertThrows(() => wasmStruct[privateSymbol] = 1, TypeError);
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+
+const wasmArray = instance.exports.makeArray();
+assertEquals(1, instance.exports.isArray(wasmArray));
+assertThrows(() => wasmArray[privateSymbol] = 1, TypeError);
+assertEquals(1, instance.exports.isArray(wasmArray));
+
+// Standard JS private fields (#field) on class return-override must also throw
+// and preserve type identity.
+class Base {
+  constructor(o) {
+    return o;
+  }
+}
+class DerivedWithPrivateField extends Base {
+  #field = 42;
+}
+
+assertThrows(() => new DerivedWithPrivateField(wasmStruct), TypeError);
+assertEquals(1, instance.exports.isStruct(wasmStruct));
+
+assertThrows(() => new DerivedWithPrivateField(wasmArray), TypeError);
+assertEquals(1, instance.exports.isArray(wasmArray));
+
+// Identity hashes used by WeakMap/WeakSet must continue to work.
+const wm = new WeakMap();
+wm.set(wasmStruct, 'struct-value');
+wm.set(wasmArray, 'array-value');
+assertEquals('struct-value', wm.get(wasmStruct));
+assertEquals('array-value', wm.get(wasmArray));
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.