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 <[email protected]> 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 <[email protected]> Reviewed-by: Darius Mercadier <[email protected]> Auto-Submit: Leszek Swirski <[email protected]> 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)