CVE-2025-13042
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/maglev/maglev-code-generator.cc |
modified | |
ifsrc/maglev/maglev-regalloc.cc |
modified |
Files Changed
src/maglev/maglev-code-generator.ccsrc/maglev/maglev-regalloc.ccsrc/maglev/maglev-regalloc.h
Patch
From 7ef5ae531a9e79a084b5f0bebd5496d5d481e0ea Mon Sep 17 00:00:00 2001
From: Olivier FlΓΌckiger <olivf@chromium.org>
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 <olivf@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
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);
Original Bug Report
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).
36/44: CallBuiltin(...). If an exception is thrown from here, the value ofr1will ben35.44/62: Call(...). If an exception is thrown from here, the value ofr1will ben33.
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 the36/44: CallBuiltin(...)node, it correctly callsSpillAndClearRegisters()to spill all registers to the stack. Therefore, the location information of the32/35: Οα΅ r1 (n33, n85)node is(spilled: [stack:15|t]). - However, when processing
Block b9,InitializeRegisterValues()is called to reload the32/35: Οα΅ r1 (n33, n85)node intorax. 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]