CVE-2024-12693
Overview
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
Patch
From e4ecfc909687511aeb20b88ce6ae2a7a1a80afe5 Mon Sep 17 00:00:00 2001 From: Darius Mercadier <[email protected]> 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 <[email protected]> Auto-Submit: Darius Mercadier <[email protected]> Reviewed-by: Olivier FlΓΌckiger <[email protected]> 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