CVE-2026-102328
Overview
Background
- Deoptimization materialization
- When optimized code deopts, V8 rebuilds (“materializes”) the objects the optimizer had kept virtual, reading each captured field value from the frame translation and writing it into a real heap object.
- Virtual object / `FixedDoubleArray` vs `FixedArray`
- The optimizer represents captured arrays as virtual objects; a
FixedDoubleArraystores rawFloat64elements, whereas aFixedArraystores tagged pointers, so the two must not receive each other’s representation. - Hole NaN / undefined NaN
- Special
Float64bit patterns (kHoleNanInt64,kUndefinedNanInt64) that encode “hole”/“undefined” inside a double slot; they must stay raw doubles in double storage but were being “laundered” intothe_hole_value/undefined_valueOddballs. - Frame translation opcodes
- Encodings describing how to reconstruct each value during deopt; this patch adds
DOUBLE_LITERALandHOLEY_DOUBLE_LITERAL(2-operand) to carry rawFloat64bits.
Root Cause Analysis
Before the patch, Float64/HoleyFloat64 constants captured in virtual objects were emitted into frame translations as tagged deopt literals: the codegen paths (in code-generator.cc, maglev-code-generator.cc, and turbolev-graph-builder.cc) inspected the containing virtual object’s map, and when a Float64 constant carried the hole-NaN (or undefined-NaN) bit pattern they substituted the the_hole_value (or undefined_value) Oddball, or a HeapNumber, instead of the raw double. Per the commit message, when such a hole-NaN float64 constant landed in a virtual FixedArray slot rather than a FixedDoubleArray or HeapNumber, deoptimization materialized an actual Oddball hole into the array — placing a hole sentinel where the program semantics expected a numeric double, i.e. a representation/type confusion between a raw double and a tagged Oddball.
The fix stops the map-dependent laundering: constants are now encoded directly as raw bits via StoreDoubleLiteral/StoreHoleyDoubleLiteral, decoded back into kDouble/kHoleyDouble TranslatedValues, and TranslatedState::MaterializeFixedDoubleArray writes them with FixedDoubleArray::set_raw using the new TranslatedValue::GetDoubleValue, while MaterializeHeapNumber boxes the raw Float64 (normalizing any NaN to a quiet NaN). This preserves the double representation end-to-end so a hole-NaN double never becomes an Oddball in a materialized array.
the_hole/undefined/HeapNumber Oddball) from the containing virtual object’s map, so a hole-NaN Float64 constant in a FixedArray slot was reified as an actual Oddball hole; the fix carries the raw Float64 bits through dedicated translation opcodes and materializes them directly as doubles.Attack Path
- Craft a captured double constant with the hole-NaN pattern
Attacker JS shapes optimized code so a virtual object captures a
Float64/HoleyFloat64constant whose bits equal the hole-NaN pattern in a slot that materializes into aFixedArray(tagged) rather than aFixedDoubleArray(inferred). - Force deoptimization
Trigger a deopt so the virtual object is materialized, exercising the pre-patch translation path that laundered the hole-NaN double through
the_hole_value. - Oddball hole written where a double is expected
Materialization stores the
the_hole_valueOddball into the array element, producing a value whose type disagrees with how surrounding code reads that slot (double vs. tagged pointer) — a type confusion. - Leverage the confusion
Reading the hole Oddball as a double (or a double as a tagged pointer) is the basis for a memory-corruption primitive
(inferred); the diff does not itself demonstrate a specific out-of-bounds sink.
Impact Assessment
(inferred). The precondition is the ability to run script and drive optimization and deoptimization of the affected pattern.Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/deoptimizer/translated-state.cc |
modified | |
ifsrc/maglev/maglev-code-generator.cc |
modified |
Files Changed
src/compiler/turboshaft/turbolev-graph-builder.ccsrc/maglev/maglev-code-generator.ccsrc/maglev/maglev-ir.ccsrc/deoptimizer/frame-translation-builder.hsrc/objects/fixed-primitive-array-inl.hsrc/deoptimizer/translated-state.ccsrc/objects/deoptimization-data.ccsrc/deoptimizer/translation-opcode.hsrc/deoptimizer/frame-translation-builder.ccsrc/objects/deoptimization-data.hsrc/compiler/backend/code-generator.ccsrc/deoptimizer/translated-state.hsrc/objects/fixed-primitive-array.h
Audit Directions
- Representation chosen from container typeLook for deopt/materialization or serialization code that decides a value’s representation (raw double vs. tagged Oddball/HeapNumber) from the containing object’s map rather than from the value’s own kind.
- Hole/undefined NaN launderingAudit paths that convert
is_hole_nan()/is_undefined_nan()doubles intothe_hole_value/undefined_valueOddballs, checking that the destination storage actually expects tagged Oddballs and not raw doubles. - `set` vs `set_raw` on primitive arraysReview
FixedDoubleArray/primitive-array writes during materialization to confirm raw-double slots are filled withset_raw/Float64values and never with taggedObjects derived from classification.
Patch
From 81235f143e1e7b3cc1904f81d7ded64c92315e6b Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Fri, 25 Sep 2026 16:03:54 +0200
Subject: [PATCH] [deopt] Materialize FixedDoubleArray elements directly as Float64
Previously, float64 constants in virtual objects were reified as
HeapObjects or laundered through the_hole_value when they had the hole
NaN bit pattern. When a float64 constant with the hole NaN bit pattern
ended up in a virtual FixedArray slot rather than a FixedDoubleArray or
HeapNumber, deoptimization materialized an actual Oddball hole into the
array.
Add DOUBLE_LITERAL and HOLEY_DOUBLE_LITERAL translation opcodes to
encode raw Float64 and HoleyFloat64 constants directly in frame
translations, and update TranslatedState::MaterializeFixedDoubleArray to
write Float64 values directly via FixedDoubleArray::set_raw.
TAG=agy
CONV=82679d1d-b3b1-48b8-8997-bb272f212b20
Bug: 560536732
Change-Id: Ibe8f178eaeb121e76d7e9838f1e615f1dc7eace0
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8455209
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Darius Mercadier <dmercadier@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110185}
---
diff --git a/src/compiler/backend/code-generator.cc b/src/compiler/backend/code-generator.cc
index f4b25af..edf804f 100644
--- a/src/compiler/backend/code-generator.cc
+++ b/src/compiler/backend/code-generator.cc
@@ -1672,19 +1672,12 @@
case Constant::kFloat64:
DCHECK(type.representation() == MachineRepresentation::kFloat64 ||
type.representation() == MachineRepresentation::kTagged);
- if (type == MachineType::HoleyFloat64() &&
- constant.ToFloat64().AsUint64() == kHoleNanInt64) {
- literal = DeoptimizationLiteral::HoleNaN();
-#ifdef V8_ENABLE_UNDEFINED_DOUBLE
- } else if (type == MachineType::HoleyFloat64() &&
- constant.ToFloat64().AsUint64() == kUndefinedNanInt64) {
- literal =
- DeoptimizationLiteral(isolate()->factory()->undefined_value());
-#endif // V8_ENABLE_UNDEFINED_DOUBLE
+ if (type == MachineType::HoleyFloat64()) {
+ translations_.StoreHoleyDoubleLiteral(Float64(constant.ToFloat64()));
} else {
- literal = DeoptimizationLiteral(constant.ToFloat64().value());
+ translations_.StoreDoubleLiteral(Float64(constant.ToFloat64()));
}
- break;
+ return;
case Constant::kHeapObject:
DCHECK_EQ(MachineRepresentation::kTagged, type.representation());
literal = DeoptimizationLiteral(constant.ToHeapObject());
diff --git a/src/compiler/turboshaft/turbolev-graph-builder.cc b/src/compiler/turboshaft/turbolev-graph-builder.cc
index a6e00c8..bfb0dce 100644
--- a/src/compiler/turboshaft/turbolev-graph-builder.cc
+++ b/src/compiler/turboshaft/turbolev-graph-builder.cc
@@ -6374,8 +6374,7 @@
case maglev::vobj::FieldType::kTrustedPointer:
case maglev::vobj::FieldType::kFloat64:
case maglev::vobj::FieldType::kInt32:
- AddVirtualObjectNestedValue(builder, virtual_objects, vobj,
- value_node);
+ AddVirtualObjectNestedValue(builder, virtual_objects, value_node);
break;
case maglev::vobj::FieldType::kNone:
UNREACHABLE();
@@ -6387,7 +6386,7 @@
void AddVirtualObjectNestedValue(
FrameStateData::Builder& builder,
const maglev::VirtualObjectList& virtual_objects,
- const maglev::VirtualObject* vobj, const maglev::ValueNode* value) {
+ const maglev::ValueNode* value) {
if (maglev::IsConstantNode(value->opcode())) {
switch (value->opcode()) {
case maglev::Opcode::kHeapConstant:
@@ -6398,29 +6397,17 @@
break;
case maglev::Opcode::kFloat64Constant:
- case maglev::Opcode::kHoleyFloat64Constant: {
- i::Float64 value_as_float =
- value->opcode() == maglev::Opcode::kFloat64Constant
- ? value->Cast<maglev::Float64Constant>()->value()
- : value->Cast<maglev::HoleyFloat64Constant>()->value();
- DCHECK(vobj->has_static_map());
- const bool is_fixed_double_array =
- vobj->map()->IsFixedDoubleArrayMap();
- if (is_fixed_double_array && value_as_float.is_hole_nan()) {
- builder.AddInput(
- MachineType::AnyTagged(),
- __ HeapConstantHole(local_factory_->the_hole_value()));
-#ifdef V8_ENABLE_UNDEFINED_DOUBLE
- } else if (is_fixed_double_array &&
- value_as_float.is_undefined_nan()) {
- builder.AddInput(MachineType::AnyTagged(), undefined_value_);
-#endif // V8_ENABLE_UNDEFINED_DOUBLE
- } else {
- builder.AddInput(MachineType::AnyTagged(),
- __ NumberConstant(value_as_float));
- }
+ builder.AddInput(
+ MachineType::Float64(),
+ __ Float64Constant(
+ value->Cast<maglev::Float64Constant>()->value()));
break;
- }
+ case maglev::Opcode::kHoleyFloat64Constant:
+ builder.AddInput(
+ MachineType::HoleyFloat64(),
+ __ Float64Constant(
+ value->Cast<maglev::HoleyFloat64Constant>()->value()));
+ break;
case maglev::Opcode::kInt32Constant:
builder.AddInput(
MachineType::AnyTagged(),
diff --git a/src/deoptimizer/frame-translation-builder.cc b/src/deoptimizer/frame-translation-builder.cc
index dc337a5..acc2218 100644
--- a/src/deoptimizer/frame-translation-builder.cc
+++ b/src/deoptimizer/frame-translation-builder.cc
@@ -484,6 +484,20 @@
Add(opcode, SignedOperand(literal_id));
}
+void FrameTranslationBuilder::StoreDoubleLiteral(Float64 value) {
+ auto opcode = TranslationOpcode::DOUBLE_LITERAL;
+ uint64_t bits = value.get_bits();
+ Add(opcode, UnsignedOperand(static_cast<uint32_t>(bits)),
+ UnsignedOperand(static_cast<uint32_t>(bits >> 32)));
+}
+
+void FrameTranslationBuilder::StoreHoleyDoubleLiteral(Float64 value) {
+ auto opcode = TranslationOpcode::HOLEY_DOUBLE_LITERAL;
+ uint64_t bits = value.get_bits();
+ Add(opcode, UnsignedOperand(static_cast<uint32_t>(bits)),
+ UnsignedOperand(static_cast<uint32_t>(bits >> 32)));
+}
+
void FrameTranslationBuilder::StoreOptimizedOut() {
auto opcode = TranslationOpcode::OPTIMIZED_OUT;
Add(opcode);
diff --git a/src/deoptimizer/frame-translation-builder.h b/src/deoptimizer/frame-translation-builder.h
index cdabcb9..9b2566d 100644
--- a/src/deoptimizer/frame-translation-builder.h
+++ b/src/deoptimizer/frame-translation-builder.h
@@ -88,6 +88,8 @@
void StoreSimd128StackSlot(int index);
void StoreHoleyDoubleStackSlot(int index);
void StoreLiteral(int literal_id);
+ void StoreDoubleLiteral(Float64 value);
+ void StoreHoleyDoubleLiteral(Float64 value);
void StoreOptimizedOut();
void StoreJSFrameFunction();
diff --git a/src/deoptimizer/translated-state.cc b/src/deoptimizer/translated-state.cc
index 73aae60..6587235 100644
--- a/src/deoptimizer/translated-state.cc
+++ b/src/deoptimizer/translated-state.cc
@@ -333,6 +333,21 @@
break;
}
+ case TranslationOpcode::DOUBLE_LITERAL:
+ case TranslationOpcode::HOLEY_DOUBLE_LITERAL: {
+ DCHECK_EQ(TranslationOpcodeOperandCount(opcode), 2);
+ uint32_t low = iterator.NextOperandUnsigned();
+ uint32_t high = iterator.NextOperandUnsigned();
+ Float64 value =
+ Float64::FromBits((static_cast<uint64_t>(high) << 32) | low);
+ os << "{double_literal=" << value.get_scalar() << " (0x" << std::hex
+ << value.get_bits() << std::dec << ")"
+ << (opcode == TranslationOpcode::HOLEY_DOUBLE_LITERAL ? " (holey)"
+ : "")
+ << "}";
+ break;
+ }
+
case TranslationOpcode::DUPLICATED_OBJECT: {
DCHECK_EQ(TranslationOpcodeOperandCount(opcode), 1);
int object_index = iterator.NextOperand();
@@ -738,7 +753,11 @@
// We shouldn't have hole values by now, so treat holey double as normal
// doubles.
case TranslatedValue::kHoleyDouble:
- number = double_value().get_scalar();
+ if (double_value().is_nan()) {
+ number = std::numeric_limits<double>::quiet_NaN();
+ } else {
+ number = double_value().get_scalar();
+ }
heap_object = isolate()->factory()->NewHeapNumber(number);
break;
default:
@@ -750,6 +769,35 @@
return storage_;
}
+Float64 TranslatedValue::GetDoubleValue() {
+ switch (kind()) {
+ case TranslatedValue::kDouble:
+ case TranslatedValue::kHoleyDouble:
+ return double_value();
+ case TranslatedValue::kFloat:
+ return Float64::FromMaybeNaN(float_value().get_scalar());
+ case TranslatedValue::kInt32:
+ return Float64(static_cast<double>(int32_value()));
+ case TranslatedValue::kUint32:
+ return Float64(static_cast<double>(uint32_value()));
+ default: {
+ DCHECK(!v8_flags.turbolev);
+ DirectHandle<Object> value = GetValue();
+ if (IsNumber(*value)) {
+ return Float64::FromMaybeNaN(Object::NumberValue(*value));
+#ifdef V8_ENABLE_UNDEFINED_DOUBLE
+ } else if (value.is_identical_to(
+ isolate()->factory()->undefined_value())) {
+ return Float64::undefined_nan();
+#endif // V8_ENABLE_UNDEFINED_DOUBLE
+ } else {
+ CHECK(value.is_identical_to(isolate()->factory()->the_hole_value()));
+ return Float64::hole_nan();
+ }
+ }
+ }
+}
+
bool TranslatedValue::IsMaterializedObject() const {
switch (kind()) {
case kCapturedObject:
@@ -1211,6 +1259,8 @@
case TranslationOpcode::SIMD128_STACK_SLOT:
case TranslationOpcode::HOLEY_DOUBLE_STACK_SLOT:
case TranslationOpcode::LITERAL:
+ case TranslationOpcode::DOUBLE_LITERAL:
+ case TranslationOpcode::HOLEY_DOUBLE_LITERAL:
case TranslationOpcode::OPTIMIZED_OUT:
case TranslationOpcode::MATCH_PREVIOUS_TRANSLATION:
break;
@@ -1775,6 +1825,50 @@
return translated_value.GetChildrenCount();
}
+ case TranslationOpcode::DOUBLE_LITERAL: {
+ uint32_t low = iterator->NextOperandUnsigned();
+ uint32_t high = iterator->NextOperandUnsigned();
+ Float64 value =
+ Float64::FromBits((static_cast<uint64_t>(high) << 32) | low);
+ if constexpr (IsTracing) {
+ if (value.is_nan()) {
+ PrintF(trace_file, "(double literal %e 0x%" PRIx64 ")",
+ value.get_scalar(), value.get_bits());
+ } else {
+ PrintF(trace_file, "(double literal %e)", value.get_scalar());
+ }
+ }
+ TranslatedValue translated_value =
+ TranslatedValue::NewDouble(this, value);
+ frame.Add(translated_value);
+ return translated_value.GetChildrenCount();
+ }
+
+ case TranslationOpcode::HOLEY_DOUBLE_LITERAL: {
+ uint32_t low = iterator->NextOperandUnsigned();
+ uint32_t high = iterator->NextOperandUnsigned();
+ Float64 value =
+ Float64::FromBits((static_cast<uint64_t>(high) << 32) | low);
+ if constexpr (IsTracing) {
+ if (value.is_hole_nan()) {
+ PrintF(trace_file, "(holey double literal hole nan)");
+#ifdef V8_ENABLE_UNDEFINED_DOUBLE
+ } else if (value.is_undefined_nan()) {
+ PrintF(trace_file, "(holey double literal undefined nan)");
+#endif // V8_ENABLE_UNDEFINED_DOUBLE
+ } else if (value.is_nan()) {
+ PrintF(trace_file, "(holey double literal %e 0x%" PRIx64 ")",
+ value.get_scalar(), value.get_bits());
+ } else {
+ PrintF(trace_file, "(holey double literal %e)", value.get_scalar());
+ }
+ }
+ TranslatedValue translated_value =
+ TranslatedValue::NewHoleyDouble(this, value);
+ frame.Add(translated_value);
+ return translated_value.GetChildrenCount();
+ }
+
case TranslationOpcode::LITERAL: {
int literal_index = iterator->NextOperand();
TranslatedValue translated_value = literal_array.Get(this, literal_index);
@@ -2202,17 +2296,8 @@
for (uint32_t i = 0; i < length; i++) {
CHECK_NE(TranslatedValue::kCapturedObject,
frame->values_[*value_index].kind());
- DirectHandle<Object> value = frame->values_[*value_index].GetValue();
- if (IsNumber(*value)) {
- array->set(i, Object::NumberValue(*value));
-#ifdef V8_ENABLE_UNDEFINED_DOUBLE
- } else if (value.is_identical_to(isolate()->factory()->undefined_value())) {
- array->set_undefined(i);
-#endif // V8_ENABLE_UNDEFINED_DOUBLE
- } else {
- CHECK(value.is_identical_to(isolate()->factory()->the_hole_value()));
- array->set_the_hole(isolate(), i);
- }
+ Float64 value = frame->values_[*value_index].GetDoubleValue();
+ array->set_raw(i, value);
(*value_index)++;
}
slot->set_storage(array);
@@ -2223,17 +2308,12 @@
TranslatedValue* slot) {
CHECK_NE(TranslatedValue::kCapturedObject,
frame->values_[*value_index].kind());
- DirectHandle<Object> value = frame->values_[*value_index].GetValue();
- Handle<HeapNumber> box;
- if (value.is_identical_to(isolate()->factory()->the_hole_value())) {
- // See is_hole_nan conversions in maglev-code-generator.cc and
- // turbolev-graph-builder.cc.
- box = isolate()->factory()->NewHeapNumber(
- std::numeric_limits<double>::quiet_NaN());
- } else {
- CHECK(IsNumber(*value));
- box = isolate()->factory()->NewHeapNumber(Object::NumberValue(*value));
+ Float64 value = frame->values_[*value_index].GetDoubleValue();
+ if (value.is_nan()) {
+ value = Float64::quiet_nan();
}
+ Handle<HeapNumber> box =
+ isolate()->factory()->NewHeapNumber(value.get_scalar());
(*value_index)++;
slot->set_storage(box);
}
diff --git a/src/deoptimizer/translated-state.h b/src/deoptimizer/translated-state.h
index 738cc71..ee52cf3 100644
--- a/src/deoptimizer/translated-state.h
+++ b/src/deoptimizer/translated-state.h
@@ -67,6 +67,8 @@
// possible.
Handle<Object> GetValue();
+ Float64 GetDoubleValue();
+
bool IsMaterializedObject() const;
bool IsMaterializableByDebugger() const;
diff --git a/src/deoptimizer/translation-opcode.h b/src/deoptimizer/translation-opcode.h
index 0bc7a1c..5ee4254 100644
--- a/src/deoptimizer/translation-opcode.h
+++ b/src/deoptimizer/translation-opcode.h
@@ -38,9 +38,11 @@
V(BOOL_STACK_SLOT, 1) \
V(CAPTURED_OBJECT, 1) \
V(STRING_CONCAT, 0) \
+ V(DOUBLE_LITERAL, 2) \
V(DOUBLE_REGISTER, 1) \
V(DOUBLE_STACK_SLOT, 1) \
V(SIMD128_STACK_SLOT, 1) \
+ V(HOLEY_DOUBLE_LITERAL, 2) \
V(HOLEY_DOUBLE_REGISTER, 1) \
V(HOLEY_DOUBLE_STACK_SLOT, 1) \
V(SIMD128_REGISTER, 1) \
diff --git a/src/maglev/maglev-code-generator.cc b/src/maglev/maglev-code-generator.cc
index c2ae9e4..f4013c3 100644
--- a/src/maglev/maglev-code-generator.cc
+++ b/src/maglev/maglev-code-generator.cc
@@ -1564,7 +1564,7 @@
return kNotDuplicated;
}
- void BuildNestedValue(const VirtualObject* object, const ValueNode* value,
+ void BuildNestedValue(const ValueNode* value,
const InputLocation*& input_location,
const VirtualObjectList& virtual_objects) {
const Opcode opcode = value->opcode();
@@ -1573,22 +1573,14 @@
DCHECK_NE(opcode, Opcode::kIdentity);
if (IsConstantNode(opcode)) {
if (opcode == Opcode::kFloat64Constant) {
- DCHECK(object->has_static_map());
- if (object->map()->IsFixedDoubleArrayMap()) {
- Float64 value_as_float = value->Cast<Float64Constant>()->value();
- if (value_as_float.is_hole_nan()) {
- translation_array_builder_->StoreLiteral(GetDeoptLiteral(
- ReadOnlyRoots{local_isolate_}.the_hole_value()));
- return;
- }
-#ifdef V8_ENABLE_UNDEFINED_DOUBLE
- if (value_as_float.is_undefined_nan()) {
- translation_array_builder_->StoreLiteral(GetDeoptLiteral(
- ReadOnlyRoots{local_isolate_}.undefined_value()));
- return;
- }
-#endif // V8_ENABLE_UNDEFINED_DOUBLE
- }
+ translation_array_builder_->StoreDoubleLiteral(
+ value->Cast<Float64Constant>()->value());
+ return;
... (file diff truncated)