High chrome Type Confusion 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in V8
DescriptionMemory corruption in V8
ComponentV8
Sub-componentTurbofan
Bug ClassType Confusion
Tracker573378812
Fix commitd4374eaec817 (v8/v8) +78/-95
CISA KEVNot listed
Creditedzenneth (randomguy6407)
Disclosed2026-10-27

Background

V8 sandbox / trusted pointers
with V8_ENABLE_SANDBOX, pointers to “trusted” objects (e.g. WasmTrustedInstanceData, function data) are not stored inline but as 32-bit handles indexing a per-isolate TrustedPointerTable. Decoding a handle loads the table entry, validates its tag, and masks off tag bits to recover the real pointer.
`BuildDecodeTrustedPointer`
the old TurboFan helper (src/compiler/wasm-graph-assembler.cc) that emitted the handle decode as a sequence of plain machine ops: shift the handle to an index, load the table base, load the entry, tag-check, mask, and BitcastWordToTagged.
Load elimination
a TurboFan optimization that removes loads it proves redundant (same address/effect). It reasons over the individual machine Load nodes emitted inside the decode sequence.
`LoadTrustedPointer` operator
the new dedicated IrOpcode::kLoadTrustedPointer (common-operator.*) carrying {offset, tag}, marked kEliminatable, lowered in Turboshaft (graph-builder.cc) and ultimately the instruction selector, so the whole decode is one opaque node.

Root Cause Analysis

The advisory says “Memory corruption in V8” (MemoryCorruption); the diff shows a TurboFan miscompilation of sandbox trusted-pointer loads that corrupts V8’s object graph — a type-confusion-flavored bug. Pre-patch, LoadTrustedPointerFromObject / LoadImmutableTrustedPointerFromObject / LoadTrustedPointerFromObjectTrapOnNull first loaded the 32-bit handle from the object (LoadFromObject(MachineType::Uint32(), object, offset)) and then called BuildDecodeTrustedPointer, which expanded into separate machine Loads (table base, table entry) plus the tag check. Because these were ordinary eliminable loads, load elimination could treat the handle load (or an inner table load) as redundant with an earlier, unrelated load and erroneously eliminate it, reusing a stale value. The commit states this directly: emitting the decode as a series of instructions let “load elimination … erroneously eliminat[e] the load to the pointer handle.” A wrong/stale handle decodes to a different TrustedPointerTable entry than intended, so the recovered pointer refers to the wrong trusted object — a type confusion among trusted objects that defeats the sandbox’s integrity guarantee.

The fix emits a single dedicated operator, common_.LoadTrustedPointer(field_offset, tag), as one IrOpcode::kLoadTrustedPointer node (plumbed through opcodes.h, the verifiers, the typer, and simplified-lowering-verifier), and lowers it only later in Turboshaft via __ LoadTrustedPointer(...) and the instruction selector. By representing the entire handle-fetch-and-decode as one opaque node rather than a sequence of generic loads, load elimination can no longer match and remove the inner handle load against an unrelated load, so the decode always reads the current handle from the object. The redundant BuildDecodeTrustedPointer, LoadImmutableTrustedPointerFromObject, and LoadTrustedPointerFromObjectTrapOnNull helpers are removed, and callers (LoadFunctionDataFromJSFunction, LoadTrustedDataFromInstanceObject) now route through the single LoadTrustedPointerFromObject.

Key insight
Emitting a trusted-pointer handle decode as a sequence of generic eliminable loads let TurboFan’s load elimination drop the handle load and reuse a stale handle, decoding to the wrong TrustedPointerTable entry — a type confusion of trusted objects; the fix makes the decode one opaque LoadTrustedPointer operator that elimination cannot tear apart.

Attack Path

  1. Web attacker serves a page that runs WebAssembly, since the affected decodes (LoadFunctionDataFromJSFunction, LoadTrustedDataFromInstanceObject) are on the Wasm/TurboFan path (web-reachable).
  2. Trigger optimization crafts Wasm/JS that TurboFan optimizes with multiple trusted-pointer loads positioned so load elimination considers the handle load redundant with an earlier load (inferred: specific graph shape needed to trip elimination).
  3. Stale handle decode the optimizer removes the real handle load; the decode reuses a prior/stale handle value and indexes the wrong TrustedPointerTable entry.
  4. Wrong trusted object the recovered “trusted” pointer refers to a different trusted object (e.g. mismatched trusted instance/function data) than the code assumes.
  5. Primitive type confusion among sandbox-trusted objects, enabling corruption of V8 heap/sandbox state; the full read/write-what-where and sandbox-escape chain is (inferred), consistent with the “memory corruption” severity.

Impact Assessment

The bug is in the renderer’s V8 engine on the WebAssembly compilation path, reachable by any page running Wasm with no special privilege. It is an optimizer correctness defect: a miscompiled trusted-pointer load yields a wrong trusted object, i.e. type confusion that undermines the V8 sandbox’s trusted-pointer integrity. The diff substantiates the mechanism (load elimination eliminating the handle load) clearly; the precise exploitation primitive — whether the confusion yields an arbitrary read/write and then a sandbox escape — is not shown by the diff and is (inferred). Because trusted pointers are exactly the sandbox’s integrity anchor, a reliable confusion here is high-severity even though the diff proves only the miscompile, not the final primitive.

Changed Functions

FunctionChangeNotes
LoadTrustedPointerOperator
src/compiler/common-operator.cc
modified
V8_EXPORT_PRIVATE
src/compiler/common-operator.h
modified
simplified_
src/compiler/wasm-graph-assembler.h
modified
common_
src/compiler/wasm-graph-assembler.h
modified

Files Changed

  • src/compiler/common-operator.cc
  • src/compiler/turbofan-typer.cc
  • src/compiler/common-operator.h
  • src/compiler/turboshaft/graph-builder.cc
  • src/compiler/verifier.cc
  • src/compiler/wasm-graph-assembler.h
  • src/compiler/simplified-lowering-verifier.cc
  • src/compiler/wasm-graph-assembler.cc
  • src/compiler/opcodes.h

Audit Directions

  • Other decode-as-sequence loads
    audit remaining sandbox pointer decoders (external-pointer, code-pointer, indirect-pointer) still emitted as sequences of generic loads that load elimination or CSE could tear apart.
  • Eliminability of security-critical loads
    review loads whose result identity matters (handles, tags) for whether marking them eliminable lets the optimizer reuse stale values; consider dedicated opaque operators.
  • Turboshaft parity
    verify the Turboshaft/instruction-selector lowering of LoadTrustedPointer preserves the tag check and cannot itself be hoisted/merged incorrectly.
  • Immutable vs mutable trusted loads
    the removed LoadImmutable... variant implied stronger aliasing assumptions; check other LoadImmutable* trusted/external loads for the same erroneous-elimination risk.
From d4374eaec817692d144b16ade997040e60ee6139 Mon Sep 17 00:00:00 2001
From: Manos Koukoutos <[email protected]>
Date: Fri, 16 Oct 2026 10:35:05 +0000
Subject: [PATCH] [turbofan] Emit dedicated operator for trusted-pointer loads

Instead of emitting a trusted-pointer load as a series of instructions
in WasmGraphAssembler::BuildDecodeTrustedPointer, we emit a dedicated
operator for it (LoadTrustedPointerOperator), which is translated into
Turboshaft and then lowered in the instruction selector. This prevents
load elimination from erroneously eliminating the load to the pointer
handle.

Fixed: 573378812
Change-Id: Ic651fe29cec26aa4b23039db481dee6dcabcb5c6
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8591942
Reviewed-by: Jakob Kummerow <[email protected]>
Commit-Queue: Manos Koukoutos <[email protected]>
Cr-Commit-Position: refs/heads/main@{#110863}
---
diff --git a/src/compiler/common-operator.cc b/src/compiler/common-operator.cc
index 37ab33d..fe96407 100644
--- a/src/compiler/common-operator.cc
+++ b/src/compiler/common-operator.cc
@@ -515,6 +515,32 @@
   return OpParameter<ExitMachineGraphParameters>(op);
 }
 
+#if V8_ENABLE_SANDBOX
+size_t hash_value(const LoadTrustedPointerParameters& params) {
+  return base::hash_combine(params.offset, params.tag);
+}
+
+V8_EXPORT_PRIVATE std::ostream& operator<<(
+    std::ostream& os, LoadTrustedPointerParameters params) {
+  return os << "offset=" << params.offset << ", tag.first=" << params.tag.first
+            << ", tag.last=" << params.tag.last;
+}
+
+class LoadTrustedPointerOperator final
+    : public Operator1<LoadTrustedPointerParameters> {
+ public:
+  LoadTrustedPointerOperator(int offset, TrustedPointerTagRange tag)
+      : Operator1<LoadTrustedPointerParameters>(
+            IrOpcode::kLoadTrustedPointer, Operator::kEliminatable,
+            "LoadTrustedPointer", 1, 1, 1, 1, 1, 0, {offset, tag}) {}
+};
+
+const Operator* CommonOperatorBuilder::LoadTrustedPointer(
+    int offset, TrustedPointerTagRange tag) {
+  return zone_->New<LoadTrustedPointerOperator>(offset, tag);
+}
+#endif  // V8_ENABLE_SANDBOX
+
 #define COMMON_CACHED_OP_LIST(V)                          \
   V(Plug, Operator::kNoProperties, 0, 0, 0, 1, 0, 0)      \
   V(Dead, Operator::kFoldable, 0, 0, 0, 1, 1, 1)          \
diff --git a/src/compiler/common-operator.h b/src/compiler/common-operator.h
index 6499e23..1a0abe5 100644
--- a/src/compiler/common-operator.h
+++ b/src/compiler/common-operator.h
@@ -538,6 +538,20 @@
 V8_EXPORT_PRIVATE const ExitMachineGraphParameters&
 ExitMachineGraphParametersOf(const Operator* op) V8_WARN_UNUSED_RESULT;
 
+#if V8_ENABLE_SANDBOX
+struct LoadTrustedPointerParameters {
+  int offset;
+  TrustedPointerTagRange tag;
+
+  bool operator==(const LoadTrustedPointerParameters&) const = default;
+};
+
+size_t hash_value(const LoadTrustedPointerParameters& params);
+
+V8_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& os,
+                                           LoadTrustedPointerParameters params);
+#endif  // V8_ENABLE_SANDBOX
+
 // Interface for building common operators that can be used at any level of IR,
 // including JavaScript, mid-level, and low-level.
 class V8_EXPORT_PRIVATE CommonOperatorBuilder final
@@ -616,6 +630,9 @@
   const Operator* HeapConstant(const Handle<HeapObject>&);
   const Operator* CompressedHeapConstant(const Handle<HeapObject>&);
   const Operator* TrustedHeapConstant(const Handle<HeapObject>&);
+#if V8_ENABLE_SANDBOX
+  const Operator* LoadTrustedPointer(int offset, TrustedPointerTagRange tag);
+#endif
   const Operator* ObjectId(uint32_t);
 
   const Operator* RelocatableInt32Constant(int32_t value,
diff --git a/src/compiler/opcodes.h b/src/compiler/opcodes.h
index de11918..d74406b 100644
--- a/src/compiler/opcodes.h
+++ b/src/compiler/opcodes.h
@@ -85,15 +85,16 @@
   V(EnterMachineGraph)      \
   V(ExitMachineGraph)
 
-#define COMMON_OP_LIST(V)      \
-  CONSTANT_OP_LIST(V)          \
-  INNER_OP_LIST(V)             \
-  V(Unreachable)               \
-  V(DeadValue)                 \
-  V(Dead)                      \
-  V(Plug)                      \
-  V(SLVerifierHint)            \
-  V(MajorGCForCompilerTesting) \
+#define COMMON_OP_LIST(V)           \
+  CONSTANT_OP_LIST(V)               \
+  INNER_OP_LIST(V)                  \
+  V(Unreachable)                    \
+  V(DeadValue)                      \
+  V(Dead)                           \
+  V(Plug)                           \
+  V(SLVerifierHint)                 \
+  V(MajorGCForCompilerTesting)      \
+  IF_SANDBOX(V, LoadTrustedPointer) \
   V(StaticAssert)
 
 // Opcodes for JavaScript operators.
diff --git a/src/compiler/simplified-lowering-verifier.cc b/src/compiler/simplified-lowering-verifier.cc
index d288ed1..6d41bbb 100644
--- a/src/compiler/simplified-lowering-verifier.cc
+++ b/src/compiler/simplified-lowering-verifier.cc
@@ -504,6 +504,7 @@
       CASE(Dead)
       CASE(Plug)
       CASE(MajorGCForCompilerTesting)
+      IF_SANDBOX(CASE, LoadTrustedPointer)
       CASE(StaticAssert)
       // Simplified change operators
       CASE(ChangeTaggedSignedToInt32)
diff --git a/src/compiler/turbofan-typer.cc b/src/compiler/turbofan-typer.cc
index 16fa4b6..04df612 100644
--- a/src/compiler/turbofan-typer.cc
+++ b/src/compiler/turbofan-typer.cc
@@ -1297,6 +1297,9 @@
   UNREACHABLE();
 }
 Type Typer::Visitor::TypeStaticAssert(Node* node) { UNREACHABLE(); }
+#if V8_ENABLE_SANDBOX
+Type Typer::Visitor::TypeLoadTrustedPointer(Node* node) { UNREACHABLE(); }
+#endif
 Type Typer::Visitor::TypeSLVerifierHint(Node* node) { UNREACHABLE(); }
 
 // JS comparison operators.
diff --git a/src/compiler/turboshaft/graph-builder.cc b/src/compiler/turboshaft/graph-builder.cc
index 4ab5422..2b824d5 100644
--- a/src/compiler/turboshaft/graph-builder.cc
+++ b/src/compiler/turboshaft/graph-builder.cc
@@ -1434,6 +1434,14 @@
       return __ Load(Map(node->InputAt(0)), Map(node->InputAt(1)),
                      LoadOp::Kind::Trapping(), loaded_rep);
     }
+#if V8_ENABLE_SANDBOX
+    case IrOpcode::kLoadTrustedPointer: {
+      auto params = OpParameter<LoadTrustedPointerParameters>(op);
+      return __ LoadTrustedPointer(Map(node->InputAt(0)),
+                                   LoadOp::Kind::TaggedBase(), params.tag,
+                                   params.offset + kHeapObjectTag);
+    }
+#endif
 
     case IrOpcode::kStore:
     case IrOpcode::kUnalignedStore: {
diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc
index a4787d8..39d2dbc 100644
--- a/src/compiler/verifier.cc
+++ b/src/compiler/verifier.cc
@@ -653,6 +653,11 @@
     case IrOpcode::kExitMachineGraph:
       CheckValueInputIs(node, 0, Type::Machine());
       break;
+#if V8_ENABLE_SANDBOX
+    case IrOpcode::kLoadTrustedPointer:
+      // TODO(manoskouk): what are the constraints here?
+      break;
+#endif
 
     // JavaScript operators
     // --------------------
diff --git a/src/compiler/wasm-graph-assembler.cc b/src/compiler/wasm-graph-assembler.cc
index 77869ef..4c46921 100644
--- a/src/compiler/wasm-graph-assembler.cc
+++ b/src/compiler/wasm-graph-assembler.cc
@@ -242,55 +242,6 @@
 #endif  // V8_ENABLE_SANDBOX
 }
 
-Node* WasmGraphAssembler::BuildDecodeTrustedPointer(
-    Node* handle, TrustedPointerTagRange tag_range) {
-#if V8_ENABLE_SANDBOX
-  Node* index = Word32Shr(handle, Int32Constant(kTrustedPointerHandleShift));
-  Node* offset = ChangeUint32ToUint64(
-      Word32Shl(index, Int32Constant(kTrustedPointerTableEntrySizeLog2)));
-  Node* table = Load(MachineType::Pointer(), LoadRootRegister(),
-                     IsolateData::trusted_pointer_table_offset() +
-                         Internals::kExternalEntityTableBasePointerOffset);
-  Node* decoded_ptr = Load(MachineType::Pointer(), table, offset);
-
-  if (IsFastTrustedPointerTagRange(tag_range)) {
-    uint64_t mask = ComputeUntaggingMaskForFastTrustedPointerTag(tag_range);
-    decoded_ptr = WordAnd(decoded_ptr, IntPtrConstant(mask));
-  } else {
-    Node* tag =
-        WordShr(decoded_ptr, IntPtrConstant(kTrustedPointerTableTagShift));
-
-    auto ok = MakeLabel();
-    if (tag_range.Size() == 1) {
-      // The common and simple case: we expect exactly one tag.
-      Node* expected_tag = IntPtrConstant(tag_range.first);
-      GotoIf(WordEqual(tag, expected_tag), &ok, BranchHint::kTrue);
-      RuntimeAbort(AbortReason::kTrustedPointerTagMismatch);
-      Goto(&ok);  // Unreachable, but needed for graph coherency.
-    } else {
-      // Range check.
-      Node* diff = IntPtrSub(tag, IntPtrConstant(tag_range.first));
-      GotoIf(Uint64LessThanOrEqual(
-                 diff, IntPtrConstant(tag_range.last - tag_range.first)),
-             &ok, BranchHint::kTrue);
-      RuntimeAbort(AbortReason::kTrustedPointerTagMismatch);
-      Goto(&ok);  // Unreachable, but needed for graph coherency.
-    }
-
-    Bind(&ok);
-    decoded_ptr =
-        WordAnd(decoded_ptr, IntPtrConstant(kTrustedPointerTablePayloadMask));
-  }
-
-  // We have to change the type of the result value to Tagged, so if the value
-  // gets spilled on the stack, it will get processed by the GC.
-  decoded_ptr = BitcastWordToTagged(decoded_ptr);
-  return decoded_ptr;
-#else
-  UNREACHABLE();
-#endif  // V8_ENABLE_SANDBOX
-}
-
 Node* WasmGraphAssembler::BuildLoadExternalPointerFromObject(
     Node* object, int field_offset, ExternalPointerTagRange tag_range,
     Node* isolate_root) {
@@ -408,41 +359,17 @@
   return LoadFromObject(type, byte_array, offset);
 }
 
-Node* WasmGraphAssembler::LoadImmutableTrustedPointerFromObject(
-    Node* object, int field_offset, TrustedPointerTagRange tag) {
-  Node* offset = IntPtrConstant(field_offset);
-#ifdef V8_ENABLE_SANDBOX
-  Node* handle = LoadImmutableFromObject(MachineType::Uint32(), object, offset);
-  return BuildDecodeTrustedPointer(handle, tag);
-#else
-  return LoadImmutableFromObject(MachineType::TaggedPointer(), object, offset);
-#endif
-}
-
 Node* WasmGraphAssembler::LoadTrustedPointerFromObject(
     Node* object, int field_offset, TrustedPointerTagRange tag) {
-  Node* offset = IntPtrConstant(field_offset);
 #ifdef V8_ENABLE_SANDBOX
-  Node* handle = LoadFromObject(MachineType::Uint32(), object, offset);
-  return BuildDecodeTrustedPointer(handle, tag);
+  return AddNode(graph()->NewNode(common_.LoadTrustedPointer(field_offset, tag),
+                                  object, effect(), control()));
 #else
+  Node* offset = IntPtrConstant(field_offset);
   return LoadFromObject(MachineType::TaggedPointer(), object, offset);
 #endif
 }
 
-std::pair<Node*, Node*>
-WasmGraphAssembler::LoadTrustedPointerFromObjectTrapOnNull(
-    Node* object, int field_offset, TrustedPointerTagRange tag) {
-  Node* offset = IntPtrConstant(field_offset);
-#ifdef V8_ENABLE_SANDBOX
-  Node* handle = LoadTrapOnNull(MachineType::Uint32(), object, offset);
-  return {handle, BuildDecodeTrustedPointer(handle, tag)};
-#else
-  Node* value = LoadTrapOnNull(MachineType::TaggedPointer(), object, offset);
-  return {value, value};
-#endif
-}
-
 Node* WasmGraphAssembler::StoreFixedArrayElement(Node* array, int index,
                                                  Node* value,
                                                  ObjectAccess access) {
@@ -465,7 +392,7 @@
 
 Node* WasmGraphAssembler::LoadFunctionDataFromJSFunction(Node* js_function) {
   Node* shared = LoadSharedFunctionInfo(js_function);
-  return LoadImmutableTrustedPointerFromObject(
+  return LoadTrustedPointerFromObject(
       shared,
       offsetof(SharedFunctionInfo, trusted_function_data_) - kHeapObjectTag,
       kWasmExportedFunctionDataTrustedPointerTag);
@@ -623,7 +550,7 @@
 
 Node* WasmGraphAssembler::LoadTrustedDataFromInstanceObject(
     Node* instance_object) {
-  return LoadImmutableTrustedPointerFromObject(
+  return LoadTrustedPointerFromObject(
       instance_object,
       offsetof(WasmInstanceObject, trusted_data_) - kHeapObjectTag,
       kWasmTrustedInstanceDataTrustedPointerTag);
diff --git a/src/compiler/wasm-graph-assembler.h b/src/compiler/wasm-graph-assembler.h
index 2f5b4db..00ec30a 100644
--- a/src/compiler/wasm-graph-assembler.h
+++ b/src/compiler/wasm-graph-assembler.h
@@ -28,7 +28,8 @@
  public:
   WasmGraphAssembler(MachineGraph* mcgraph, Zone* zone)
       : GraphAssembler(mcgraph, zone, BranchSemantics::kMachine),
-        simplified_(zone) {}
+        simplified_(zone),
+        common_(zone) {}
 
   // While CallBuiltin() translates to a direct call to the address of the
   // builtin, CallBuiltinThroughJumptable instead jumps to a slot in a jump
@@ -177,15 +178,8 @@
                                            ExternalPointerTagRange tag_range,
                                            Node* isolate_root);
 
-  Node* LoadImmutableTrustedPointerFromObject(Node* object, int offset,
-                                              TrustedPointerTagRange tag);
   Node* LoadTrustedPointerFromObject(Node* object, int offset,
                                      TrustedPointerTagRange tag);
-  // Returns the load node (where the source position for the trap needs to be
-  // set by the caller) and the result.
-  std::pair<Node*, Node*> LoadTrustedPointerFromObjectTrapOnNull(
-      Node* object, int offset, TrustedPointerTagRange tag);
-  Node* BuildDecodeTrustedPointer(Node* handle, TrustedPointerTagRange tag);
 
   Node* IsSmi(Node* object);
 
@@ -343,6 +337,7 @@
   }
 
   SimplifiedOperatorBuilder simplified_;
+  CommonOperatorBuilder common_;
 };
 
 }  // namespace compiler
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.