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
Tracker447613211
Fix commit55496daf9022 (v8/v8) +106/-14
CISA KEVNot listed
CreditedMan Yue Mo of GitHub Security Lab
Disclosed2025-10-28

Changed Functions

FunctionChangeNotes
if
src/objects/js-objects.cc
modified

Files Changed

  • src/objects/js-objects.cc
  • src/objects/map-inl.h
  • src/objects/map.cc
  • src/objects/map.h
  • src/objects/objects-inl.h
From 55496daf90227fb93311c535922f4b2142eeb72c Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <jkummerow@chromium.org>
Date: Mon, 06 Oct 2025 19:23:36 +0200
Subject: [PATCH] [wasm-custom-desc] Fix prototype validity cells

For prototype chains consisting of interleaved JS and Wasm objects,
prototype chain tracking must not bail out at the Wasm objects.

Fixed: 447613211
Change-Id: Ibd1a1ffdc7ba1a7540770eecaa908e66b3450268
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7003558
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Auto-Submit: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#102944}
---

diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc
index 8d5ad7c..7da6d46 100644
--- a/src/objects/js-objects.cc
+++ b/src/objects/js-objects.cc
@@ -4936,11 +4936,13 @@
                                   WhereToStart where_to_start,
                                   Isolate* isolate) {
   if (!IsJSReceiver(*receiver)) return;
-  if (IsWasmObject(*receiver)) where_to_start = kStartAtPrototype;
   for (PrototypeIterator iter(isolate, Cast<JSReceiver>(receiver),
                               where_to_start);
        !iter.IsAtEnd(); iter.Advance()) {
     DirectHandle<Object> current = PrototypeIterator::GetCurrent(iter);
+#if V8_ENABLE_WEBASSEMBLY
+    if (IsWasmObject(*current)) continue;
+#endif  // V8_ENABLE_WEBASSEMBLY
     if (!IsJSObjectThatCanBeTrackedAsPrototype(*current)) return;
     DirectHandle<JSObject> current_obj = Cast<JSObject>(current);
     Tagged<Map> current_map = current_obj->map();
@@ -5079,7 +5081,11 @@
                                          Isolate* isolate) {
   // Contract: In line with InvalidatePrototypeChains()'s requirements,
   // leaf maps don't need to register as users, only prototypes do.
+#if V8_ENABLE_WEBASSEMBLY
+  DCHECK(user->is_prototype_map() || IsWasmObjectMap(*user));
+#else
   DCHECK(user->is_prototype_map());
+#endif  // V8_ENABLE_WEBASSEMBLY
 
   DirectHandle<Map> current_user = user;
   DirectHandle<PrototypeInfo> current_user_info =
@@ -5099,8 +5105,8 @@
     // change, so they don't need to be tracked as prototypes
     // anyway. Additionally, registering users of shared objects is not
     // threadsafe.
-    if (!IsJSObjectThatCanBeTrackedAsPrototype(*maybe_proto)) continue;
-    auto proto = Cast<JSObject>(maybe_proto);
+    if (!IsAnyObjectThatCanBeTrackedAsPrototype(*maybe_proto)) continue;
+    DirectHandle<JSReceiver> proto = Cast<JSReceiver>(maybe_proto);
     DirectHandle<PrototypeInfo> proto_info =
         Map::GetOrCreatePrototypeInfo(proto, isolate);
     Handle<Object> maybe_registry(proto_info->prototype_users(), isolate);
@@ -5173,7 +5179,12 @@
 // AccessorAssembler::InvalidateValidityCellIfPrototype() which does pre-checks
 // before jumping here.
 void InvalidateOnePrototypeValidityCellInternal(Tagged<Map> map) {
+#if V8_ENABLE_WEBASSEMBLY
+  DCHECK(map->is_prototype_map() || IsWasmObjectMap(map));
+#else
   DCHECK(map->is_prototype_map());
+#endif  // V8_ENABLE_WEBASSEMBLY
+
   if (v8_flags.trace_prototype_users) {
     PrintF("Invalidating prototype map %p 's cell\n",
            reinterpret_cast<void*>(map.ptr()));
diff --git a/src/objects/map-inl.h b/src/objects/map-inl.h
index 23736fc..c8bcf98 100644
--- a/src/objects/map-inl.h
+++ b/src/objects/map-inl.h
@@ -75,7 +75,11 @@
   Tagged<UnionOf<Smi, PrototypeInfo>> value =
       TaggedField<UnionOf<Smi, PrototypeInfo>,
                   kTransitionsOrPrototypeInfoOffset>::load(cage_base, *this);
+#if V8_ENABLE_WEBASSEMBLY
+  DCHECK(this->is_prototype_map() || IsWasmObjectMap(*this));
+#else
   DCHECK(this->is_prototype_map());
+#endif  // V8_ENABLE_WEBASSEMBLY
   return value;
 }
 RELEASE_ACQUIRE_ACCESSORS(Map, prototype_info,
@@ -588,7 +592,11 @@
 }
 
 bool Map::TryGetPrototypeInfo(Tagged<PrototypeInfo>* result) const {
+#if V8_ENABLE_WEBASSEMBLY
+  DCHECK(is_prototype_map() || IsWasmObjectMap(*this));
+#else
   DCHECK(is_prototype_map());
+#endif  // V8_ENABLE_WEBASSEMBLY
   Tagged<Object> maybe_proto_info = prototype_info();
   if (!PrototypeInfo::IsPrototypeInfoFast(maybe_proto_info)) return false;
   *result = Cast<PrototypeInfo>(maybe_proto_info);
@@ -608,10 +616,10 @@
   Tagged<Object> maybe_prototype =
       map->GetPrototypeChainRootMap(isolate)->prototype();
 
-  if (!IsJSObjectThatCanBeTrackedAsPrototype(maybe_prototype)) {
+  if (!IsAnyObjectThatCanBeTrackedAsPrototype(maybe_prototype)) {
     return false;
   }
-  *out_validity_cell_holder_map = Cast<JSObject>(maybe_prototype)->map();
+  *out_validity_cell_holder_map = Cast<JSReceiver>(maybe_prototype)->map();
   return true;
 }
 
diff --git a/src/objects/map.cc b/src/objects/map.cc
index d536848..1428c1c 100644
--- a/src/objects/map.cc
+++ b/src/objects/map.cc
@@ -2382,8 +2382,8 @@
 
 // static
 DirectHandle<PrototypeInfo> Map::GetOrCreatePrototypeInfo(
-    DirectHandle<JSObject> prototype, Isolate* isolate) {
-  DCHECK(IsJSObjectThatCanBeTrackedAsPrototype(*prototype));
+    DirectHandle<JSReceiver> prototype, Isolate* isolate) {
+  DCHECK(IsAnyObjectThatCanBeTrackedAsPrototype(*prototype));
   {
     Tagged<PrototypeInfo> prototype_info;
     if (prototype->map()->TryGetPrototypeInfo(&prototype_info)) {
diff --git a/src/objects/map.h b/src/objects/map.h
index 0059008..2ff17d2 100644
--- a/src/objects/map.h
+++ b/src/objects/map.h
@@ -208,7 +208,7 @@
 // |               |   - is_deprecated (bit 24)                      |
 // |               |   - is_unstable (bit 25)                        |
 // |               |   - is_migration_target (bit 26)                |
-// |               |   - is_extensible (bit 28)                      |
+// |               |   - is_extensible (bit 27)                      |
 // |               |   - may_have_interesting_properties (bit 28)    |
 // |               |   - construction_counter (bit 29..31)           |
 // |               |                                                 |
@@ -219,6 +219,7 @@
 // | TaggedPointer | [prototype]                                     |
 // +---------------+-------------------------------------------------+
 // | TaggedPointer | [constructor_or_back_pointer_or_native_context] |
+// |               | [WasmTypeInfo] (if Wasm map)                    |
 // +---------------+-------------------------------------------------+
 // | TaggedPointer | [instance_descriptors] (if JS object)           |
 // |               | [custom_descriptor]    (if WasmStruct)          |
@@ -509,7 +510,7 @@
   // PrototypeInfo is created lazily using this helper (which installs it on
   // the given prototype's map).
   static DirectHandle<PrototypeInfo> GetOrCreatePrototypeInfo(
-      DirectHandle<JSObject> prototype, Isolate* isolate);
+      DirectHandle<JSReceiver> prototype, Isolate* isolate);
   static DirectHandle<PrototypeInfo> GetOrCreatePrototypeInfo(
       DirectHandle<Map> prototype_map, Isolate* isolate);
   inline bool should_be_fast_prototype_map() const;
diff --git a/src/objects/objects-inl.h b/src/objects/objects-inl.h
index 93f6111..0076c0e 100644
--- a/src/objects/objects-inl.h
+++ b/src/objects/objects-inl.h
@@ -95,11 +95,6 @@
          TaggedIndex::IsValid(Tagged<TaggedIndex>(obj.ptr()).value());
 }
 
-bool IsJSObjectThatCanBeTrackedAsPrototype(Tagged<Object> obj) {
-  return IsHeapObject(obj) &&
-         IsJSObjectThatCanBeTrackedAsPrototype(Cast<HeapObject>(obj));
-}
-
 #define IS_TYPE_FUNCTION_DEF(type_)                                          \
   bool Is##type_(Tagged<Object> obj) {                                       \
     return IsHeapObject(obj) && Is##type_(Cast<HeapObject>(obj));            \
@@ -506,6 +501,11 @@
 #endif
 }
 
+bool IsJSObjectThatCanBeTrackedAsPrototype(Tagged<Object> obj) {
+  return IsHeapObject(obj) &&
+         IsJSObjectThatCanBeTrackedAsPrototype(Cast<HeapObject>(obj));
+}
+
 bool IsJSObjectThatCanBeTrackedAsPrototype(Tagged<HeapObject> obj) {
   // Do not optimize objects in the shared heap because it is not
   // threadsafe. Objects in the shared heap have fixed layouts and their maps
@@ -513,6 +513,19 @@
   return IsJSObject(obj) && !HeapLayout::InWritableSharedSpace(*obj);
 }
 
+bool IsAnyObjectThatCanBeTrackedAsPrototype(Tagged<Object> obj) {
+  return IsHeapObject(obj) &&
+         IsAnyObjectThatCanBeTrackedAsPrototype(Cast<HeapObject>(obj));
+}
+
+bool IsAnyObjectThatCanBeTrackedAsPrototype(Tagged<HeapObject> obj) {
+  // Do not optimize objects in the shared heap because it is not
+  // threadsafe. Objects in the shared heap have fixed layouts and their maps
+  // never change.
+  return (IsJSObject(obj) || IsWasmObject(obj)) &&
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/wasm/regress-447613211.js b/test/mjsunit/regress/wasm/regress-447613211.js
new file mode 100644
index 0000000..aebd2b85
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-447613211.js
@@ -0,0 +1,55 @@
+// Copyright 2025 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: --experimental-wasm-custom-descriptors
+
+d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
+
+let builder = new WasmModuleBuilder();
+
+builder.startRecGroup();
+let $desc0 = builder.nextTypeIndex() + 1;
+let $struct0 = builder.addStruct({ descriptor: $desc0 });
+/* $desc0 */ builder.addStruct({
+  describes: $struct0,
+  fields: [makeField(kWasmExternRef, false)],
+});
+builder.endRecGroup();
+
+builder.addFunction("make", makeSig([kWasmExternRef], [kWasmAnyRef]))
+  .exportFunc()
+  .addBody([
+    kExprLocalGet, 0,
+    kGCPrefix, kExprStructNew, $desc0,
+    kGCPrefix, kExprStructNew, $struct0,
+  ]);
+
+let instance = builder.instantiate();
+
+let proto = {};
+proto.inobj0 = 1;
+proto.inobj1 = 1;
+proto.inobj2 = 1;
+proto.inobj3 = 1;
+proto.c = 1;
+
+let wasm_obj = instance.exports.make(proto);
+wasm_obj.a;   // Make prototypes fast.
+proto.c = 2;  // Defeat constant tracking.
+
+let obj = {__proto__: wasm_obj};
+
+function foo(x) {
+  return x.c;
+}
+
+for (let i = 0; i < 20; i++) {
+  foo(obj);
+}
+
+assertEquals(2, foo(obj));
+delete proto.c;
+assertEquals(undefined, foo(obj));
+obj.__proto__ = {};
+assertEquals(undefined, foo(obj));
Loading diff…

Original Bug Report

reported by [Deleted User]

Type confusion in inline cache prototype loading with Webassembly object prototype

Vulnerability details

When loading properties from prototype, inline cache caches the prototype property holder and accesses its properties via optimize handlers. In order to avoid map changes in the prototype which may invalidate assumptions in the optimized handlers, inline cache also stores the PrototypeChainValidityCell, which gets invalidated when a map change happens in the prototype chain. The GetOrCreatePrototypeChainValidityCell function calls TryGetValidityCellHolderMap, which returns Map::kNoValidityCellSentinel when the prototype is not a JSObject.

bool Map::TryGetValidityCellHolderMap(
    Tagged<Map> map, Isolate* isolate,
    Tagged<Map>* out_validity_cell_holder_map) {
  ...
  if (!IsJSObjectThatCanBeTrackedAsPrototype(maybe_prototype)) {   //<--- returns false if prototype is not a JSObject
    return false;
  }
  *out_validity_cell_holder_map = Cast<JSObject>(maybe_prototype)->map();
  return true;
}
...
Handle<UnionOf<Smi, Cell>> Map::GetOrCreatePrototypeChainValidityCell(
    DirectHandle<Map> map, Isolate* isolate,
    DirectHandle<PrototypeInfo>* out_prototype_info) {
  DirectHandle<Map> validity_cell_holder_map;
  {
    Tagged<Map> holder_map;
    if (!TryGetValidityCellHolderMap(*map, isolate, &holder_map)) {
      // Prototype value is not a JSObject.
      return handle(Map::kNoValidityCellSentinel, isolate);  //<------ returns Map::kNoValidityCellSentinel
    }
  ...

This becomes problematic with the custom descriptors proposal in WebAssembly, which allows Wasm objects to have prototypes. Consider an object with the following prototype chain:

obj ({}) -> wasm_obj -> obj_1 ({a : 1})

When accessing the property a, the prototype chain will be followed, which then fetches the property a from obj_1. The prototype obj_1 is then cached in the inline cache handler, but because wasm_obj is not a JSObject, the validity_cell in the handler is going to be Map::kNoValidityCellSentinel, allowing the prototype validity cell check to be bypassed:

TNode<MaybeObject> AccessorAssembler::CheckPrototypeValidityCell(
    TNode<Object> maybe_validity_cell, Label* miss) {
  TVARIABLE(MaybeObject, var_cell_value,
            SmiConstant(Map::kNoValidityCellSentinel));

  Label done(this);
  GotoIf(TaggedEqual(maybe_validity_cell,
                     SmiConstant(Map::kNoValidityCellSentinel)),
         &done);
  ...

In particular, any change in the map of obj_1 will not invalidate the inline cache handler, causing type confusion.

As there is currently an origin trial for the Webassembly custom descriptors, anyone can register for the origin trial and host a website that can trigger this bug.

Thank you very much for your help and please let me know if there is anything I can help.

REPRODUCTION CASE

To test locally, run the poc.js with the --experimental-wasm-custom-descriptors flag and the --allow-natives-syntax flag to print out debug message. The flag --experimental-wasm-custom-descriptors is only needed for local testing to emulate the origin trial. It should cause an OOB access which then loads an object from outside of the boundary of proto (A heap number map in my case).

VERSION v8 commit d32e674 OS: Ubuntu 24.04 LTS

CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited?

Reporter credit: Man Yue Mo of GitHub Security Lab

View on issue tracker