Low CVSS 8.8 firefox Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionA potential memory corruption vulnerability could be triggered if an attacker had the ability to trigger an OOM at a specific moment during JIT compilation.
ComponentSpiderMonkey
Bug ClassMemory Corruption
Tracker1915249
Fix commitbbc38e6bfd7c (firefox) +32/-23
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGary Kwong
Disclosed2024-10-01

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 FlagAllOperandsAsImplicitlyUsed and FlagOperandsAsImplicitlyUsedAfter mark 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 false to 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.

Key insight
The core mistake was ignoring the fallible return value of the operand-flagging helpers, so an OOM during flagging left the MIR graph in an inconsistent state that downstream passes then corrupted; the fix checks and propagates the failure and enforces that check permanently with [[nodiscard]].

Attack Path

  1. Trigger JIT compilation The attacker supplies JavaScript hot enough to be compiled by IonMonkey, driving execution into jit::PruneUnusedBranches or jit::RemoveUnmarkedBlocks.
  2. Reach block removal The MIR graph is shaped so that blocks are marked for removal, invoking FlagAllOperandsAsImplicitlyUsed or FlagOperandsAsImplicitlyUsedAfter.
  3. Induce OOM at the right moment The attacker forces an allocation failure precisely during operand flagging so the helper (via DepthFirstSearchUse) returns false.
  4. 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.
  5. 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

An attacker who can trigger an OOM at a specific moment during Ion JIT compilation gains a memory-corruption primitive within the content process running SpiderMonkey. Exploitation requires precise control over allocation-failure timing during a specific graph-pruning pass, which makes it difficult to weaponize reliably, consistent with the low severity rating. Success would corrupt the MIR graph’s live-node bookkeeping, potentially enabling use-after-free of JIT compiler data structures.

Changed Functions

FunctionChangeNotes
while
js/src/jit/IonAnalysis.cpp
modified

Files Changed

  • js/src/jit/IonAnalysis.cpp

Audit Directions

  • Discarded fallible return values
    Search JIT passes for calls to functions returning bool for success/failure whose results are ignored, especially operand/graph-mutation helpers, and confirm they are [[nodiscard]].
  • OOM invariant violations
    Review 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 helpers
    Audit SpiderMonkey static helpers that can fail on OOM but lack [[nodiscard]], since those are the ones most prone to silently continuing past a failure.
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",
Loading diff…