High chrome Type Confusion ⚠️ Exploited in the wild 🔧 Commit mapped

Overview

High
Severity
CVSS
Yes
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker460017370
Fix commit4cf9311810b0 (v8/v8) +109/-25
CISA KEVNot listed
CreditedGoogle Threat Analysis Group
Disclosed2025-11-17

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
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.