Overview

High
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker457351015
Fix commit7ef5ae531a9e (v8/v8) +25/-4
CISA KEVNot listed
Credited303f06e3
Disclosed2025-11-11

Changed Functions

FunctionChangeNotes
if
src/maglev/maglev-code-generator.cc
modified
if
src/maglev/maglev-regalloc.cc
modified

Files Changed

  • src/maglev/maglev-code-generator.cc
  • src/maglev/maglev-regalloc.cc
  • src/maglev/maglev-regalloc.h
From 7ef5ae531a9e79a084b5f0bebd5496d5d481e0ea Mon Sep 17 00:00:00 2001
From: Olivier FlΓΌckiger <[email protected]>
Date: Wed, 05 Nov 2025 14:11:51 +0100
Subject: [PATCH] [maglev] Fix left over register allocations from regalloc

The regalloc should clear the node allocations when it is done.
Failing to do so can cause the codegen to use stale register state.
In this concrete example the exception handler trampolines would not
load from the spill slot due to the left over allocation.

Bug: 457351015
Change-Id: Ia113c0b3373b5b11250e11d66d63b6a67b63b13f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7119379
Auto-Submit: Olivier FlΓΌckiger <[email protected]>
Commit-Queue: Leszek Swirski <[email protected]>
Reviewed-by: Leszek Swirski <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103535}
---

diff --git a/src/maglev/maglev-code-generator.cc b/src/maglev/maglev-code-generator.cc
index d5600f1..45a6f77 100644
--- a/src/maglev/maglev-code-generator.cc
+++ b/src/maglev/maglev-code-generator.cc
@@ -811,6 +811,12 @@
 
   template <typename NodeT>
   ProcessResult Process(NodeT* node, const ProcessingState& state) {
+#ifdef DEBUG
+    if constexpr (std::is_base_of_v<ValueNode, NodeT>) {
+      // Regalloc must clear its temp allocations.
+      DCHECK(!node->regalloc_info()->has_register());
+    }
+#endif
     if (v8_flags.code_comments) {
       std::stringstream ss;
       ss << "--   " << graph_labeller()->NodeId(node) << ": "
diff --git a/src/maglev/maglev-regalloc.cc b/src/maglev/maglev-regalloc.cc
index 55731b5..2a566b9 100644
--- a/src/maglev/maglev-regalloc.cc
+++ b/src/maglev/maglev-regalloc.cc
@@ -635,6 +635,9 @@
     AllocateControlNode(block->control_node(), block);
     ApplyPatches(block);
   }
+
+  // Clean up remaining register allocations at the end
+  ClearRegisters();
 }
 
 void StraightForwardRegisterAllocator::FreeRegistersUsedBy(ValueNode* node) {
@@ -1622,8 +1625,8 @@
   double_registers_.ForEachUsedRegister(spill);
 }
 
-template <typename RegisterT>
-void StraightForwardRegisterAllocator::SpillAndClearRegisters(
+template <typename RegisterT, bool spill>
+void StraightForwardRegisterAllocator::ClearRegisters(
     RegisterFrameState<RegisterT>& registers) {
   while (registers.used() != registers.empty()) {
     RegisterT reg = registers.used().first();
@@ -1632,7 +1635,9 @@
       printing_visitor_->os()
           << "  clearing registers with " << PrintNodeLabel(node) << "\n";
     }
-    Spill(node);
+    if (spill) {
+      Spill(node);
+    }
     registers.FreeRegistersUsedBy(node);
     DCHECK(!registers.used().has(reg));
   }
@@ -1643,6 +1648,11 @@
   SpillAndClearRegisters(double_registers_);
 }
 
+void StraightForwardRegisterAllocator::ClearRegisters() {
+  ClearRegisters(general_registers_);
+  ClearRegisters(double_registers_);
+}
+
 void StraightForwardRegisterAllocator::SaveRegisterSnapshot(NodeBase* node) {
   RegisterSnapshot snapshot;
   general_registers_.ForEachUsedRegister([&](Register reg, ValueNode* node) {
diff --git a/src/maglev/maglev-regalloc.h b/src/maglev/maglev-regalloc.h
index ff9ae3a..ba00530 100644
--- a/src/maglev/maglev-regalloc.h
+++ b/src/maglev/maglev-regalloc.h
@@ -226,8 +226,13 @@
   void Spill(ValueNode* node);
   void SpillRegisters();
 
+  template <typename RegisterT, bool spill = false>
+  void ClearRegisters(RegisterFrameState<RegisterT>& registers);
   template <typename RegisterT>
-  void SpillAndClearRegisters(RegisterFrameState<RegisterT>& registers);
+  void SpillAndClearRegisters(RegisterFrameState<RegisterT>& registers) {
+    ClearRegisters<RegisterT, true>(registers);
+  }
+  void ClearRegisters();
   void SpillAndClearRegisters();
 
   void SaveRegisterSnapshot(NodeBase* node);
Loading diff…

Original Bug Report

reported by [email protected]

DCHECK Fail when Maglev Generates Exception Handler Trampoline Instructions

VULNERABILITY DETAILS

I am still analyzing this PoC and have not yet identified the root cause. I can only explain the reason for the crash.

This crash is related to the Maglev register allocation process. The relevant Maglev graph is as follows:

After register allocation
Graph
    ....
│╭───►Block b2 (effects: ua)
β”‚β”‚      32/35: Ο†α΅€ r1 (n33, n85) (compressed) β†’ [rax|R|t] (spilled: [stack:15|t]), live range: [32-66]
β”‚β”‚      33/36: Ο†α΅€ r5 (n32, n89) (decompressed) β†’ [rdx|R|t] (spilled: [stack:7|t]), live range: [33-60]
β”‚β”‚         98: GapMove([rdx|R|t] β†’ [rbx|R|t])
β”‚β”‚     79 : TestLessThan r5, [14]
β”‚β”‚             ↱ eager @79 (11 live vars)
β”‚β”‚      34/38: CheckedSmiUntag [v33/n36:[rdx|R|t]] β†’ [rdx|R|w32] (spilled: [stack:1|w32]), live range: [34-60]
β”‚β”‚         99: ConstantGapMove(n39 β†’ [rsi|R|w32])
β”‚β”‚     82 : JumpIfFalse [81]
││╭─────35/41: BranchIfInt32Compare(LessThan) [v34/n38:[rdx|R|w32], v13/n39:[rsi|R|w32]] b3 b9
β”‚β”‚β”‚         ↓
β”‚β”‚β”‚   Block b3    /*=== k[9] = k; ===*/
|||        ...
β”‚β”‚β”‚  0x09b1008384b1 <SharedFunctionInfo> (0x09b100848915 <String[8]: "./poc.js">:5:21)
β”‚β”‚β”‚    93 : SetKeyedProperty r5, r17, [15]
β”‚β”‚β”‚     36/44: 🐒 CallBuiltin(KeyedStoreIC_Megamorphic) [v33/n36:[rdx|R|t], v9/n43:[rcx|R|t], v33/n36:[rax|R|t], v16/n2:[rsi|R|t]] β†’ [rax|R|t]
β”‚β”‚β”‚            ↳ lazy @93 (11 live vars)
β”‚β”‚β”‚            ↳ throw @126 (b7) : {<this>:n1, <context>:n2, r1:n35, r4:n26, r5:n36, r7:n11, r8:n12, r10:n14, r13:n17, r14:n18}

....        /*=== for (let m = 0; m<10; m++) { } ===*/

β”‚β”‚β”‚ β•°β–ΊBlock b6    /*=== undefined(); ===*/
β”‚β”‚β”‚       108: ConstantGapMove(n33 β†’ [rdi|R|t])
β”‚β”‚β”‚       109: GapMove([stack:-3|t] β†’ [rsi|R|t])
β”‚β”‚β”‚  0x09b1008384b1 <SharedFunctionInfo> (0x09b100848915 <String[8]: "./poc.js">:8:16)
β”‚β”‚β”‚   120 : CallUndefinedReceiver0 r16, [20]
β”‚β”‚β”‚     44/62: 🐒 Call(NULL_OR_UNDEFINED, Any) [v4/n33:[rdi|R|t], v16/n2:[rsi|R|t], v4/n33:[rdi|R|t]] β†’ [rax|R|t], live range: [44-45]
β”‚β”‚β”‚            ↳ lazy @120 (11 live vars)
β”‚β”‚β”‚            ↳ throw @126 (b7) : {<this>:n1, <context>:n2, r1:n33, r4:n26, r5:n36, r7:n11, r8:n12, r10:n14, r13:n17, r14:n18}
β”‚β”‚β”‚   124 : Jump [30]
│││╭────45/63: Jump b8
β”‚β”‚β”‚β”‚           with gap moves:
β”‚β”‚β”‚β”‚             - v44/n62:[rax|R|t] β†’ 85: Ο†α΅€ r1 [rax|R|t]
β”‚β”‚β”‚β”‚           with register merges:
β”‚β”‚β”‚β”‚ 
β”‚β”‚β”‚β”‚  Block b7 (exception handler)    /*=== catch(exception) { ... } ===*/
β”‚β”‚β”‚β”‚    46/64: Ο†α΅€β‚‘ <accumulator> (compressed) β†’ [rax|R|t], live range: [46-54]
β”‚β”‚β”‚β”‚    47/65: Ο†α΅€β‚‘ r1 (compressed) β†’ [rcx|R|t] (spilled: [stack:14|t]), live range: [47-57]
....

In the PoC, there are two nodes that may throw exceptions, and the exceptions thrown by both will be handled by Block b7 (exception handler).

  1. 36/44: CallBuiltin(...). If an exception is thrown from here, the value of r1 will be n35.
  2. 44/62: Call(...). If an exception is thrown from here, the value of r1 will be n33.

Since the value of r1 differs in the two paths, an exception phi node 47/65: Ο†α΅€β‚‘ r1 is additionally added in Block b7.

The crash occurs during the Maglev assembly process. According to the comments in the ExceptionHandlerTrampolineBuilder::EmitTrampolineFor() method, when handling exception phis, the NewHeapNumber builtin may be called to create a HeapNumber object for float64, which may overwrite registers. Therefore, it is required that the source of every exception phi must be spilled to the stack during processing.

However, when ExceptionHandlerTrampolineBuilder processes the 36/44: CallBuiltin node, n35 is the source of the exception phi node, but n35 is not spilled to the stack. Instead, it is simultaneously located in [rax|R|t] and (spilled: [stack:15|t]), causing the DCHECK to fail.

In the release version, ExceptionHandlerTrampolineBuilder directly loads the value from rax, which is unsafe because the NewHeapNumber builtin may overwrite the value in rax at any time.

As I continued to study the Maglev register allocation process, I found:

  • When StraightForwardRegisterAllocator::AllocateNode() processes the 36/44: CallBuiltin(...) node, it correctly calls SpillAndClearRegisters() to spill all registers to the stack. Therefore, the location information of the 32/35: Ο†α΅€ r1 (n33, n85) node is (spilled: [stack:15|t]).
  • However, when processing Block b9, InitializeRegisterValues() is called to reload the 32/35: Ο†α΅€ r1 (n33, n85) node into rax. This results in the location information (i.e., regalloc_info) of the node being [rax|R|t] (spilled: [stack:15|t]).
β”‚β”‚β”‚   Block b3
β”‚β”‚β”‚   live regs: rax=v32, rcx=v1, rdx=v34, rbx=v33, rsi=v13
β”‚β”‚β”‚   [holes: 45-59 63β†°]
.....
β”‚β”‚β”‚   Using n2...
β”‚β”‚β”‚     clearing registers with n36      /* <=== Spill and Clean */
β”‚β”‚β”‚     spill: [stack:7|t] ← n36  
β”‚β”‚β”‚     clearing registers with n43
β”‚β”‚β”‚     clearing registers with n2
β”‚β”‚β”‚     clearing registers with n38
β”‚β”‚β”‚     spill: [stack:1|w32] ← n38
β”‚β”‚β”‚     clearing registers with n35
β”‚β”‚β”‚     spill: [stack:15|t] ← n35
β”‚β”‚β”‚   Allocating result...
β”‚β”‚β”‚     forcing rax to n44...
β”‚β”‚β”‚   Allocating lazy deopt inputs...
β”‚β”‚β”‚   Using n3...
β”‚β”‚β”‚   Using n1...
β”‚β”‚β”‚   Using n2...
β”‚β”‚β”‚   Using n35...
β”‚β”‚β”‚   Using n26...
β”‚β”‚β”‚   Using n36...
β”‚β”‚β”‚   Using n11...
β”‚β”‚β”‚   Using n12...
β”‚β”‚β”‚   Using n14...
β”‚β”‚β”‚   Using n17...
β”‚β”‚β”‚   Using n18...
β”‚β”‚β”‚   Using n2...
β”‚β”‚β”‚  0x09b1008384b1 <SharedFunctionInfo> (0x09b100848915 <String[8]: "./poc.js">:5:21)
β”‚β”‚β”‚    93 : SetKeyedProperty r5, r17, [15]
β”‚β”‚β”‚     44: 🐒 CallBuiltin(KeyedStoreIC_Megamorphic) [n36, n43, n36, n2], 0 uses, but required
β”‚β”‚β”‚         ↳ lazy @93 (11 live vars)
β”‚β”‚β”‚         ↳ throw @126 (b7) : {<this>:n1, <context>:n2, r1:n35, r4:n26, r5:n36, r7:n11, r8:n12, r10:n14, r13:n17, r14:n18}
....
β”‚ ╰──►Block b9
β”‚     live regs: rax=v32, rcx=v1, rsi=v13    /* Force Load */

RecordMoves() retrieves the location information of the node from source->regalloc_info()->allocation(). If a node’s value is simultaneously in a register and on the stack, allocation() will choose to load the value from the register.

class RegallocValueNodeInfo : public RegallocNodeInfo {
  ...
  compiler::InstructionOperand allocation() const {
    if (has_register()) {
      return compiler::AllocatedOperand(compiler::LocationOperand::REGISTER,
                                        representation_, FirstRegisterCode());
    }
    CHECK(is_loadable());
    return loadable_slot();
  }
  ...
}

That is all I know so far. I cannot determine the root cause of this crash. I believe you have a better understanding of Maglev than I do, so I report it to you.

REPRODUCTION CASE

poc.js:

let arr = [1, 2, 3];
for (const i of arr) {
    for (let j = 0; j < 50, true; j++) {
        for (let k = 0; k < 5; k++) {
            try {
                k[9] = k;
                for (let m = 0; m<10; m++) {
                }
                undefined();   
            } catch(exception) {
                print(exception);
            }
        }
    }
}

V8 must be built with a debug configuration, Execute v8 as follows:

./d8 \
    --predictable \
    --jit-fuzzing \
    ./poc.js

This will result in the following crash:

#
# Fatal error in ../../src/maglev/maglev-code-generator.cc, line 623
# Debug check failed: !source->regalloc_info()->allocation().IsRegister().
#

CREDIT INFORMATION

Reporter credit: [303f06e3]

View on issue tracker