Chrome · V8
CVE-2026-85045
Race in V8
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/maglev/maglev-code-generator.cc |
modified | |
fortest/mjsunit/maglev/regress-547819997.js |
modified |
Files Changed
src/maglev/maglev-code-generator.ccsrc/maglev/maglev-ir.htest/mjsunit/maglev/regress-547819997.js
Patch
From 7d5eb1df2a721c8fe3aa03b999464c4b86242080 Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Wed, 19 Aug 2026 14:06:55 +0200
Subject: [PATCH] [maglev] Materialize a fresh HeapNumber per deoptimization
This matches what Turbolev graph builder already does to Maglev.
Fixed: 547819997
Change-Id: Iafc1185ef96db8816db36b295a3a4b7e528f9863
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8255554
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109347}
---
diff --git a/src/maglev/maglev-code-generator.cc b/src/maglev/maglev-code-generator.cc
index 6ebc3c7..32899ba 100644
--- a/src/maglev/maglev-code-generator.cc
+++ b/src/maglev/maglev-code-generator.cc
@@ -1557,17 +1557,9 @@
return kNotDuplicated;
}
- void BuildHeapNumber(const VirtualObject* vobject) {
- DCHECK_EQ(vobject->object_type(), vobj::ObjectType::kHeapNumber);
- ValueNode* value_node = vobject->get(offsetof(HeapNumber, value_));
- return BuildHeapNumber(value_node->Cast<Float64Constant>()->value());
- }
-
- void BuildHeapNumber(Float64 number) {
- DirectHandle<Object> value =
- local_isolate_->factory()->NewHeapNumberFromBits<AllocationType::kOld>(
- number.get_bits());
- translation_array_builder_->StoreLiteral(GetDeoptLiteral(*value));
+ int CreateUnduplicatableId() {
+ object_ids_.push_back(kNotDuplicated);
+ return kNotDuplicated;
}
void BuildNestedValue(const ValueNode* value,
@@ -1622,13 +1614,16 @@
const InputLocation*& input_location,
const VirtualObjectList& virtual_objects) {
vobj::ObjectType object_type = object->object_type();
- if (object_type == vobj::ObjectType::kHeapNumber) {
- // TODO(jgruber): Could we use the standard path below instead?
- return BuildHeapNumber(object);
- }
DCHECK_NOT_NULL(object->allocation());
+ // HeapNumbers may be mutable object fields; each materialization must
+ // create a fresh box, so they are never deduplicated.
+ // TODO(victorgomes): Constrain which objects may contain mutable
+ // HeapNumbers. Immutable HeapNumbers can be stored as a literal object
+ // instead of a captured object.
int dup_id =
- GetDuplicatedId(reinterpret_cast<intptr_t>(object->allocation()));
+ object_type == vobj::ObjectType::kHeapNumber
+ ? CreateUnduplicatableId()
+ : GetDuplicatedId(reinterpret_cast<intptr_t>(object->allocation()));
if (dup_id != kNotDuplicated) {
translation_array_builder_->DuplicateObject(dup_id);
object->ForEachNestedRuntimeInput(
@@ -1796,7 +1791,7 @@
ZoneVector<IndirectHandle<TrustedObject>>* protected_deopt_literals_vector_;
ZoneVector<IndirectHandle<Object>>* deopt_literals_vector_;
- static const int kNotDuplicated = -1;
+ static constexpr int kNotDuplicated = -1;
std::vector<intptr_t> object_ids_;
};
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index fc76ae5..c9f1bd8 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -5874,11 +5874,9 @@
vobj::Field snd = FieldForOffset(offsetof(ConsString, second_));
return callback(slots_[snd.slot_index], snd);
}
- if (object_type() == vobj::ObjectType::kHeapNumber) {
- // HeapNumber materialization creates a literal object instead of
- // slot traversal.
- return true;
- }
+ // TODO(victorgomes): Constrain which objects may contain mutable
+ // HeapNumbers. Immutable HeapNumbers can be stored as a literal object
+ // instead of traversing their slots.
}
for (int i = 0; i < slot_count(); i++) {
vobj::Field field = FieldForSlot(i);
@@ -6243,8 +6241,8 @@
struct VirtualHeapNumberShape : VirtualPrimitiveHeapObjectShape {
using T = HeapNumber;
- // Special handling needed; deopt materialization uses a special path.
- // TODO(jgruber): .. but could it take the standard path instead?
+ // Special handling needed; instances may be mutable object fields and thus
+ // must never be deduplicated in deopt frames.
static constexpr vobj::ObjectType kObjectType = vobj::ObjectType::kHeapNumber;
#define FIELD_LIST(V) V(value, offsetof(T, value_), vobj::FieldType::kFloat64)
DEF_SHAPE(VirtualPrimitiveHeapObjectShape, FIELD_LIST);
diff --git a/test/mjsunit/maglev/regress-547819997.js b/test/mjsunit/maglev/regress-547819997.js
new file mode 100644
index 0000000..43a8e39
--- /dev/null
+++ b/test/mjsunit/maglev/regress-547819997.js
@@ -0,0 +1,38 @@
+// 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 --maglev
+
+const boxes = [];
+
+// Never called during warm-up, so this call site has no feedback and Maglev
+// emits an unconditional deopt for it. The literal below is therefore only
+// used by deopt frames and gets elided by escape analysis.
+function sink(o) {
+ boxes.push(o);
+}
+
+function foo(depth, take) {
+ const o = {x: 1.5};
+ if (depth > 0) foo(depth - 1, take);
+ if (take) sink(o);
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(3, false);
+foo(3, false);
+%OptimizeMaglevOnNextCall(foo);
+foo(3, false);
+
+// Deopts the innermost activation eagerly and the outer ones lazily, so every
+// activation materializes its own object.
+foo(3, true);
+
+assertEquals(4, boxes.length);
+for (let i = 0; i < boxes.length; i++) {
+ boxes[i].x = i;
+}
+for (let i = 0; i < boxes.length; i++) {
+ assertEquals(i, boxes[i].x);
+}
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/maglev/regress-547819997.js b/test/mjsunit/maglev/regress-547819997.js
new file mode 100644
index 0000000..43a8e39
--- /dev/null
+++ b/test/mjsunit/maglev/regress-547819997.js
@@ -0,0 +1,38 @@
+// 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 --maglev
+
+const boxes = [];
+
+// Never called during warm-up, so this call site has no feedback and Maglev
+// emits an unconditional deopt for it. The literal below is therefore only
+// used by deopt frames and gets elided by escape analysis.
+function sink(o) {
+ boxes.push(o);
+}
+
+function foo(depth, take) {
+ const o = {x: 1.5};
+ if (depth > 0) foo(depth - 1, take);
+ if (take) sink(o);
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(3, false);
+foo(3, false);
+%OptimizeMaglevOnNextCall(foo);
+foo(3, false);
+
+// Deopts the innermost activation eagerly and the outer ones lazily, so every
+// activation materializes its own object.
+foo(3, true);
+
+assertEquals(4, boxes.length);
+for (let i = 0; i < boxes.length; i++) {
+ boxes[i].x = i;
+}
+for (let i = 0; i < boxes.length; i++) {
+ assertEquals(i, boxes[i].x);
+}
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.
References
On This Page