CVE-2024-9400
Overview
Background
- `IonAnalysis.cpp`
- The SpiderMonkey source file implementing MIR-graph analysis and optimization passes used by the IonMonkey JIT compiler.
- Implicit use flagging
- The process by which helpers like
FlagAllOperandsAsImplicitlyUsedandFlagOperandsAsImplicitlyUsedAftermark the operands of instructions in blocks that are being removed so that later phases treat them as still live. - `[[nodiscard]]`
- A C++ attribute that makes the compiler warn when a function’s return value is ignored, used here to force callers to check the success/failure status.
- OOM (out-of-memory)
- A condition where an allocation fails during JIT compilation, causing helper functions to return
falseto signal that the operation could not complete.
Root Cause Analysis
Inside jit::PruneUnusedBranches and jit::RemoveUnmarkedBlocks, the code invoked FlagAllOperandsAsImplicitlyUsed and FlagOperandsAsImplicitlyUsedAfter to mark operands of instructions in blocks slated for removal, but it discarded the boolean return values of those calls. These helpers can fail on OOM (for example when growing the MPhiUseIteratorStack worklist inside DepthFirstSearchUse), yet the callers proceeded as if flagging had succeeded, violating the invariant that all operands of removed instructions are marked as implicitly used before the graph is mutated. When flagging is skipped, later optimization phases may free or reuse MIR nodes that are still referenced, producing use-after-free style memory corruption.
The fix propagates the failures by wrapping each call in if (!...) { return false; } and adds the [[nodiscard]] attribute to the flagging helpers so any future caller that ignores the result is caught at compile time. Because the failure now aborts compilation cleanly rather than continuing on a corrupt graph, the corruption window is eliminated.
[[nodiscard]].Attack Path
- Trigger JIT compilation
The attacker supplies JavaScript hot enough to be compiled by IonMonkey, driving execution into
jit::PruneUnusedBranchesorjit::RemoveUnmarkedBlocks. - Reach block removal
The MIR graph is shaped so that blocks are marked for removal, invoking
FlagAllOperandsAsImplicitlyUsedorFlagOperandsAsImplicitlyUsedAfter. - Induce OOM at the right moment
The attacker forces an allocation failure precisely during operand flagging so the helper (via
DepthFirstSearchUse) returnsfalse. - Skip invariant on silent failure Because the caller discards the return value, flagging is silently abandoned while compilation continues on a graph whose removed-instruction operands were never marked implicitly used.
- Corrupt memory via stale references A later optimization phase frees or reuses MIR nodes that should have remained live, yielding memory corruption that can be leveraged for exploitation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
whilejs/src/jit/IonAnalysis.cpp |
modified |
Files Changed
js/src/jit/IonAnalysis.cpp
Audit Directions
- Discarded fallible return valuesSearch JIT passes for calls to functions returning
boolfor success/failure whose results are ignored, especially operand/graph-mutation helpers, and confirm they are[[nodiscard]]. - OOM invariant violationsReview code that mutates the MIR graph after helper calls that can fail on allocation, verifying that failure aborts the pass before the graph is left in an inconsistent state.
- Missing `[[nodiscard]]` on OOM-fallible helpersAudit SpiderMonkey static helpers that can fail on OOM but lack
[[nodiscard]], since those are the ones most prone to silently continuing past a failure.
Patch
diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
index 8c37105b238..affbdafd7fc 100644
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -29,8 +29,9 @@ using MPhiUseIteratorStack =
// Look for Phi uses with a depth-first search. If any uses are found the stack
// of MPhi instructions is returned in the |worklist| argument.
-static bool DepthFirstSearchUse(MIRGenerator* mir,
- MPhiUseIteratorStack& worklist, MPhi* phi) {
+[[nodiscard]] static bool DepthFirstSearchUse(MIRGenerator* mir,
+ MPhiUseIteratorStack& worklist,
+ MPhi* phi) {
// Push a Phi and the next use to iterate over in the worklist.
auto push = [&worklist](MPhi* phi, MUseIterator use) -> bool {
phi->setInWorklist();
@@ -131,9 +132,9 @@ static bool DepthFirstSearchUse(MIRGenerator* mir,
return true;
}
-static bool FlagPhiInputsAsImplicitlyUsed(MIRGenerator* mir, MBasicBlock* block,
- MBasicBlock* succ,
- MPhiUseIteratorStack& worklist) {
+[[nodiscard]] static bool FlagPhiInputsAsImplicitlyUsed(
+ MIRGenerator* mir, MBasicBlock* block, MBasicBlock* succ,
+ MPhiUseIteratorStack& worklist) {
// When removing an edge between 2 blocks, we might remove the ability of
// later phases to figure out that the uses of a Phi should be considered as
// a use of all its inputs. Thus we need to mark the Phi inputs as being
@@ -265,7 +266,7 @@ static MInstructionIterator FindFirstInstructionAfterBail(MBasicBlock* block) {
// Given an iterator pointing to the first removed instruction, mark
// the operands of each removed instruction as having implicit uses.
-static bool FlagOperandsAsImplicitlyUsedAfter(
+[[nodiscard]] static bool FlagOperandsAsImplicitlyUsedAfter(
MIRGenerator* mir, MBasicBlock* block, MInstructionIterator firstRemoved) {
MOZ_ASSERT(firstRemoved->block() == block);
@@ -312,8 +313,8 @@ static bool FlagOperandsAsImplicitlyUsedAfter(
return true;
}
-static bool FlagEntryResumePointOperands(MIRGenerator* mir,
- MBasicBlock* block) {
+[[nodiscard]] static bool FlagEntryResumePointOperands(MIRGenerator* mir,
+ MBasicBlock* block) {
// Flag observable operands of the entry resume point as having implicit uses.
MResumePoint* rp = block->entryResumePoint();
while (rp) {
@@ -334,8 +335,8 @@ static bool FlagEntryResumePointOperands(MIRGenerator* mir,
return true;
}
-static bool FlagAllOperandsAsImplicitlyUsed(MIRGenerator* mir,
- MBasicBlock* block) {
+[[nodiscard]] static bool FlagAllOperandsAsImplicitlyUsed(MIRGenerator* mir,
+ MBasicBlock* block) {
return FlagEntryResumePointOperands(mir, block) &&
FlagOperandsAsImplicitlyUsedAfter(mir, block, block->begin());
}
@@ -420,12 +421,16 @@ bool jit::PruneUnusedBranches(MIRGenerator* mir, MIRGraph& graph) {
if (!block->isMarked()) {
// If we are removing the block entirely, mark the operands of every
// instruction as being implicitly used.
- FlagAllOperandsAsImplicitlyUsed(mir, block);
+ if (!FlagAllOperandsAsImplicitlyUsed(mir, block)) {
+ return false;
+ }
} else if (block->alwaysBails()) {
// If we are only trimming instructions after a bail, only mark operands
// of removed instructions.
MInstructionIterator firstRemoved = FindFirstInstructionAfterBail(block);
- FlagOperandsAsImplicitlyUsedAfter(mir, block, firstRemoved);
+ if (!FlagOperandsAsImplicitlyUsedAfter(mir, block, firstRemoved)) {
+ return false;
+ }
}
}
@@ -503,7 +508,8 @@ bool jit::PruneUnusedBranches(MIRGenerator* mir, MIRGraph& graph) {
return true;
}
-static bool SplitCriticalEdgesForBlock(MIRGraph& graph, MBasicBlock* block) {
+[[nodiscard]] static bool SplitCriticalEdgesForBlock(MIRGraph& graph,
+ MBasicBlock* block) {
if (block->numSuccessors() < 2) {
return true;
}
@@ -767,8 +773,8 @@ static bool IsDiamondPattern(MBasicBlock* initialBlock) {
return true;
}
-static bool MaybeFoldDiamondConditionBlock(MIRGraph& graph,
- MBasicBlock* initialBlock) {
+[[nodiscard]] static bool MaybeFoldDiamondConditionBlock(
+ MIRGraph& graph, MBasicBlock* initialBlock) {
MOZ_ASSERT(IsDiamondPattern(initialBlock));
// Optimize the MIR graph to improve the code generated for conditional
@@ -936,8 +942,8 @@ static bool IsTrianglePattern(MBasicBlock* initialBlock) {
return false;
}
-static bool MaybeFoldTriangleConditionBlock(MIRGraph& graph,
- MBasicBlock* initialBlock) {
+[[nodiscard]] static bool MaybeFoldTriangleConditionBlock(
+ MIRGraph& graph, MBasicBlock* initialBlock) {
MOZ_ASSERT(IsTrianglePattern(initialBlock));
// Optimize the MIR graph to improve the code generated for boolean
@@ -1089,8 +1095,8 @@ static bool MaybeFoldTriangleConditionBlock(MIRGraph& graph,
return true;
}
-static bool MaybeFoldConditionBlock(MIRGraph& graph,
- MBasicBlock* initialBlock) {
+[[nodiscard]] static bool MaybeFoldConditionBlock(MIRGraph& graph,
+ MBasicBlock* initialBlock) {
if (IsDiamondPattern(initialBlock)) {
return MaybeFoldDiamondConditionBlock(graph, initialBlock);
}
@@ -1100,7 +1106,8 @@ static bool MaybeFoldConditionBlock(MIRGraph& graph,
return true;
}
-static bool MaybeFoldTestBlock(MIRGraph& graph, MBasicBlock* initialBlock) {
+[[nodiscard]] static bool MaybeFoldTestBlock(MIRGraph& graph,
+ MBasicBlock* initialBlock) {
// Handle test expressions on more than two inputs. For example
// |if ((x > 10) && (y > 20) && (z > 30)) { ... }|, which results in the below
// pattern.
@@ -2984,7 +2991,9 @@ bool jit::RemoveUnmarkedBlocks(MIRGenerator* mir, MIRGraph& graph,
continue;
}
- FlagAllOperandsAsImplicitlyUsed(mir, block);
+ if (!FlagAllOperandsAsImplicitlyUsed(mir, block)) {
+ return false;
+ }
}
// Find unmarked blocks and remove them.
@@ -4235,8 +4244,8 @@ bool jit::EliminateRedundantShapeGuards(MIRGraph& graph) {
return true;
}
-static bool TryEliminateGCBarriersForAllocation(TempAllocator& alloc,
- MInstruction* allocation) {
+[[nodiscard]] static bool TryEliminateGCBarriersForAllocation(
+ TempAllocator& alloc, MInstruction* allocation) {
MOZ_ASSERT(allocation->type() == MIRType::Object);
JitSpew(JitSpew_RedundantGCBarriers, "Analyzing allocation %s",