High patch-now ✔︎ verified CVSS 8.8 chrome Type Confusion CISA KEV ⚠️ Exploited in the wild 🔧 Commit mapped 🧬 Optimizer reuses a type assumption across a map change🧬 Unsound representation selection and phi untagging in V8 optimizers

Overview

High
Severity
8.8
CVSS
Yes
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Sub-componentCompiler
Bug ClassType Confusion
Action tierpatch-now — exploited in the wild / KEV (itw) — real-world evidence overrides analysis — 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: none -- diff replaces ForFixedArraySlot(i) (hardcoded AnyTagged) with ForPropertyArraySlot(i, representation) threading each field's Representation via descriptor-array walk, matching the claimed representation/type-confusion root cause.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker460017370
Fix commit4cf9311810b0 (v8/v8) +109/-25
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 KEVListed
CreditedGoogle Threat Analysis Group
Reported2025-11-12 (per vendor advisory)
Fix landed2025-11-12 (public git)
Disclosed2025-11-17
Patch gap5 days — fix public in source before the advisory

Background

`PropertyArray`
the out-of-object backing store that holds a JS object’s named fields once its in-object slots are full; slots are addressed by index via PropertyArray::OffsetOfElementAt.
`Representation`
V8’s per-field type tag (Smi, Double, HeapObject, Tagged) recorded in the DescriptorArray PropertyDetails; it determines how a field’s value is physically stored and accessed.
`FieldAccess` / `MachineType`
the compiler descriptor that tells TurboFan/Turboshaft how to load and store a slot; MachineType::AnyTagged() means “Smi-or-pointer”, MachineType::TaggedPointer() means “always a pointer”.
Property-backing-store extension
when an object gains a field with no free slots, the JIT emits code to allocate a larger PropertyArray, copy the old field values across, and append JSObject::kFieldsAdded undefined slots.

Root Cause Analysis

When JSNativeContextSpecialization (and the Turboshaft ExtendPropertiesBackingStore) inlined the growth of a property backing store, it loaded and re-stored every existing field value using AccessBuilder::ForFixedArraySlot(i), which hardcodes MachineType::AnyTagged(). This ignored each field’s actual Representation: a Double- or HeapObject-representation field, whose slot the compiler should treat as a guaranteed TaggedPointer, was instead described to the optimizer as a possibly-Smi AnyTagged value. Because the copy loop never consulted the DescriptorArray, the compiler’s static type/representation tracking for the copied values did not match the fields’ real representations, producing a type confusion in the generated code.

The fix walks the descriptor array in lockstep with the property array — locating the first out-of-object field descriptor and skipping non-field/in-object descriptors — so each slot’s PropertyDetails::representation() is threaded into the new AccessBuilder::ForPropertyArraySlot(index, representation), which selects TaggedPointer for heap-object/double fields and AnyTagged otherwise. The load and store are now both representation-correct, keeping the compiler’s view of each value consistent with the field it came from.

Key insight
Copying field values by raw index while treating them all as generic tagged values discards the fields’ Representation, so the optimizer reasons about a value with the wrong machine type — a compiler-level type confusion that survives into JITed code.

Attack Path

  1. Shape an object Craft a JS object whose fields include Double/HeapObject-representation properties and whose in-object slots are exhausted so further additions land in the PropertyArray.
  2. Trigger optimized extension Force the JIT to compile the property-addition path that inlines the backing-store growth, invoking the buggy ForFixedArraySlot-based copy loop.
  3. Induce representation mismatch The copied field values are loaded/stored as AnyTagged, letting the optimizer make incorrect assumptions about a value that is actually a pointer (or double box), yielding type confusion.
  4. Weaponize the confusion Leverage the mismatched type reasoning to read or write memory with an attacker-controlled interpretation, building an out-of-bounds or fake-object primitive.
  5. Escalate Chain the primitive into arbitrary read/write within the V8 heap and toward code execution.

Impact Assessment

This is a high-severity type confusion in V8’s optimizing compiler that yields powerful memory-corruption primitives from ordinary JavaScript, reachable in a renderer without any special privileges. It was exploited in the wild as a 0-day, making it suitable for a full renderer compromise as the first stage of a sandbox-escape chain.

Changed Functions

FunctionChangeNotes
for
src/compiler/js-native-context-specialization.cc
modified

Files Changed

  • src/compiler/access-builder.cc
  • src/compiler/access-builder.h
  • src/compiler/js-native-context-specialization.cc
  • src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h

Audit Directions

  • Index-based field copies
    Flag any compiler code that iterates PropertyArray/field slots by raw index using ForFixedArraySlot/AnyTagged without consulting PropertyDetails::representation(); representation must be preserved across load/store.
  • Descriptor/property-array desync
    Review paths that map property-array indices to descriptor indices; non-field (kAccessor/kDescriptor) and in-object descriptors must be skipped, and field_index() - GetInObjectProperties() alignment verified.
  • MachineType selection for tagged slots
    Audit other AccessBuilder helpers that emit MachineType::AnyTagged() for slots whose true representation is known, since a too-loose machine type feeds type confusion into the optimizer.
From 4cf9311810b0561ee8b532a694526c7904e18367 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <[email protected]>
Date: Wed, 12 Nov 2025 16:46:01 +0100
Subject: [PATCH] [compiler] Preserve field repr in property array extension

Walk the descriptor array in lockstep with the property array when
extending the latter.

Fixed: 460017370
Change-Id: If0b4fc3c5f62fc0cc373588cbddc3c0a95c7225c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7146166
Commit-Queue: Leszek Swirski <[email protected]>
Reviewed-by: Nico Hartmann <[email protected]>
Reviewed-by: Igor Sheludko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103674}
---

diff --git a/src/compiler/access-builder.cc b/src/compiler/access-builder.cc
index d8f618f..ad6172b 100644
--- a/src/compiler/access-builder.cc
+++ b/src/compiler/access-builder.cc
@@ -4,6 +4,8 @@
 
 #include "src/compiler/access-builder.h"
 
+#include "src/codegen/machine-type.h"
+#include "src/compiler/property-access-builder.h"
 #include "src/compiler/type-cache.h"
 #include "src/handles/handles-inl.h"
 #include "src/objects/arguments.h"
@@ -1070,12 +1072,16 @@
 }
 
 // static
-FieldAccess AccessBuilder::ForPropertyArraySlot(int index) {
+FieldAccess AccessBuilder::ForPropertyArraySlot(int index,
+                                                Representation representation) {
   int offset = PropertyArray::OffsetOfElementAt(index);
-  FieldAccess access = {kTaggedBase,       offset,
-                        Handle<Name>(),    OptionalMapRef(),
-                        Type::Any(),       MachineType::AnyTagged(),
-                        kFullWriteBarrier, "PropertyArraySlot"};
+  MachineType machine_type =
+      representation.IsHeapObject() || representation.IsDouble()
+          ? MachineType::TaggedPointer()
+          : MachineType::AnyTagged();
+  FieldAccess access = {
+      kTaggedBase, offset,       Handle<Name>(),    OptionalMapRef(),
+      Type::Any(), machine_type, kFullWriteBarrier, "PropertyArraySlot"};
   return access;
 }
 
diff --git a/src/compiler/access-builder.h b/src/compiler/access-builder.h
index 8e7d9cd..4feaefc 100644
--- a/src/compiler/access-builder.h
+++ b/src/compiler/access-builder.h
@@ -11,6 +11,7 @@
 #include "src/compiler/write-barrier-kind.h"
 #include "src/objects/elements-kind.h"
 #include "src/objects/js-objects.h"
+#include "src/objects/property-details.h"
 
 namespace v8 {
 namespace internal {
@@ -318,7 +319,8 @@
   static FieldAccess ForFeedbackVectorSlot(int index);
 
   // Provides access to PropertyArray slots.
-  static FieldAccess ForPropertyArraySlot(int index);
+  static FieldAccess ForPropertyArraySlot(int index,
+                                          Representation representation);
 
   // Provides access to ScopeInfo flags.
   static FieldAccess ForScopeInfoFlags();
diff --git a/src/compiler/js-native-context-specialization.cc b/src/compiler/js-native-context-specialization.cc
index faeda3e..dc8fcd4 100644
--- a/src/compiler/js-native-context-specialization.cc
+++ b/src/compiler/js-native-context-specialization.cc
@@ -38,6 +38,7 @@
 #include "src/objects/elements-kind.h"
 #include "src/objects/feedback-vector.h"
 #include "src/objects/heap-number.h"
+#include "src/objects/property-details.h"
 #include "src/objects/string.h"
 
 namespace v8 {
@@ -4235,25 +4236,59 @@
   // for intermediate states of chains of property additions. That makes
   // it unclear what the best approach is here.
   DCHECK_EQ(map.UnusedPropertyFields(), 0);
-  int length = map.NextFreePropertyIndex() - map.GetInObjectProperties();
+  int in_object_length = map.GetInObjectProperties();
+  int length = map.NextFreePropertyIndex() - in_object_length;
   // Under normal circumstances, NextFreePropertyIndex() will always be larger
   // than GetInObjectProperties(). However, an attacker able to corrupt heap
   // memory can break this invariant, in which case we'll get confused here,
   // potentially causing a sandbox violation. This CHECK defends against that.
   SBXCHECK_GE(length, 0);
   int new_length = length + JSObject::kFieldsAdded;
+
+  // Find the descriptor index corresponding to the first out-of-object
+  // property.
+  DescriptorArrayRef descs = map.instance_descriptors(broker());
+  InternalIndex first_out_of_object_descriptor(in_object_length);
+  InternalIndex number_of_descriptors(descs.object()->number_of_descriptors());
+  for (InternalIndex i(in_object_length); i < number_of_descriptors; ++i) {
+    PropertyDetails details = descs.GetPropertyDetails(i);
+    // Skip over non-field properties.
+    if (details.location() != PropertyLocation::kField) {
+      continue;
+    }
+    // Skip over in-object fields.
+    // TODO(leszeks): We could make this smarter, like a binary search.
+    if (details.field_index() < in_object_length) {
+      continue;
+    }
+    first_out_of_object_descriptor = i;
+    break;
+  }
+
   // Collect the field values from the {properties}.
-  ZoneVector<Node*> values(zone());
+  ZoneVector<std::pair<Node*, Representation>> values(zone());
   values.reserve(new_length);
-  for (int i = 0; i < length; ++i) {
+
+  // Walk the property descriptors alongside the property values, to make
+  // sure to get and store them with the right machine type.
+  InternalIndex descriptor = first_out_of_object_descriptor;
+  for (int i = 0; i < length; ++i, ++descriptor) {
+    PropertyDetails details = descs.GetPropertyDetails(descriptor);
+    while (details.location() != PropertyLocation::kField) {
+      ++descriptor;
+      details = descs.GetPropertyDetails(descriptor);
+    }
+    DCHECK_EQ(i, details.field_index() - in_object_length);
     Node* value = effect = graph()->NewNode(
-        simplified()->LoadField(AccessBuilder::ForFixedArraySlot(i)),
+        simplified()->LoadField(
+            AccessBuilder::ForPropertyArraySlot(i, details.representation())),
         properties, effect, control);
-    values.push_back(value);
+    values.push_back({value, details.representation()});
   }
   // Initialize the new fields to undefined.
   for (int i = 0; i < JSObject::kFieldsAdded; ++i) {
-    values.push_back(jsgraph()->UndefinedConstant());
+    values.push_back(
+        {jsgraph()->UndefinedConstant(), Representation::Tagged()});
   }
 
   // Compute new length and hash.
@@ -4291,7 +4326,8 @@
   a.Store(AccessBuilder::ForMap(), jsgraph()->PropertyArrayMapConstant());
   a.Store(AccessBuilder::ForPropertyArrayLengthAndHash(), new_length_and_hash);
   for (int i = 0; i < new_length; ++i) {
-    a.Store(AccessBuilder::ForFixedArraySlot(i), values[i]);
+    a.Store(AccessBuilder::ForPropertyArraySlot(i, values[i].second),
+            values[i].first);
   }
   return a.Finish();
 }
diff --git a/src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h b/src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h
index 0c63f5c..d8b5571 100644
--- a/src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h
+++ b/src/compiler/turboshaft/turbolev-early-lowering-reducer-inl.h
@@ -14,6 +14,7 @@
 #include "src/compiler/turboshaft/representations.h"
 #include "src/deoptimizer/deoptimize-reason.h"
 #include "src/objects/contexts.h"
+#include "src/objects/descriptor-array-inl.h"
 #include "src/objects/instance-type-inl.h"
 
 namespace v8::internal::compiler::turboshaft {
@@ -318,8 +319,32 @@
   }
 
   V<PropertyArray> ExtendPropertiesBackingStore(
-      V<PropertyArray> old_property_array, V<JSObject> object, int old_length,
+      V<PropertyArray> old_property_array, V<JSObject> object,
+      const compiler::MapRef& old_map, int old_length,
       V<FrameState> frame_state, const FeedbackSource& feedback) {
+    int in_object_length = old_map.GetInObjectProperties();
+
+    // Find the descriptor index corresponding to the first out-of-object
+    // property.
+    DescriptorArrayRef descs = old_map.instance_descriptors(broker_);
+    InternalIndex first_out_of_object_descriptor(in_object_length);
+    InternalIndex number_of_descriptors(
+        descs.object()->number_of_descriptors());
+    for (InternalIndex i(in_object_length); i < number_of_descriptors; ++i) {
+      PropertyDetails details = descs.GetPropertyDetails(i);
+      // Skip over non-field properties.
+      if (details.location() != PropertyLocation::kField) {
+        continue;
+      }
+      // Skip over in-object fields.
+      // TODO(leszeks): We could make this smarter, like a binary search.
+      if (details.field_index() < in_object_length) {
+        continue;
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.