High firefox Integer Overflow 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInteger overflow in the JavaScript: WebAssembly component
ComponentSpiderMonkey
Bug ClassInteger Overflow
Tracker2051854
Fix commit612b38c7eac4 (firefox) +159/-60
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-07-21

Background

`StackSlotAllocator`
the JIT helper that hands out stack-frame offsets for spilled virtual registers, tracking the frame height_ as a uint32_t.
`LStackSlot`
an LAllocation subclass that packs a stack-slot offset plus width into a bitfield limited to LAllocation::DATA_BITS bits.
`BacktrackingAllocator` / `SimpleAllocator`
the two Ion/Wasm register allocators that call into StackSlotAllocator to place spills and stack results.
Integer overflow
an arithmetic or bitfield-truncation error where a value exceeds its type’s representable range and silently wraps to a smaller, aliasing value.

Root Cause Analysis

The vulnerable path ran through StackSlotAllocator::allocateSlot and allocateStackArea, which grew height_ and returned a raw stack offset without ever checking that the offset still fit inside the DATA_BITS field that LStackSlot uses to encode it. Callers such as BacktrackingAllocator::pickStackSlot, BacktrackingAllocator::allocateStackDefinition, and SimpleAllocator::ensureStackLocation returned void/LAllocation and had no way to signal or detect failure, so an over-large offset was silently truncated when packed into LStackSlot. The violated invariant is that every allocated stack offset must be <= MAX_SLOT (i.e. representable in DATA_BITS) and height_ must not overflow uint32_t. When a function demanded enough stack space, the truncated offset aliased a different, valid slot, so two distinct virtual registers shared frame memory.

The fix makes allocateSlot, allocateStackArea, and the new incrementHeight helper fallible ([[nodiscard]] bool), adds the explicit LStackSlot::MAX_SLOT bound, and threads a failure return through every caller so compilation aborts cleanly instead of emitting code with an aliased slot.

Key insight
The single core mistake was treating stack-slot allocation as infallible and packing an unbounded offset into a fixed-width bitfield, so an offset past DATA_BITS truncated into an aliasing slot; the fix bounds the offset with MAX_SLOT, makes the allocation return bool, and propagates that failure to abort compilation.

Attack Path

  1. Craft an oversized function An attacker supplies a WebAssembly module (or JavaScript) whose function needs an enormous number of spilled virtual registers or stack-result areas.
  2. Force a huge frame height During register allocation, repeated allocateSlot/allocateStackArea calls drive height_ past the value encodable in LAllocation::DATA_BITS.
  3. Trigger silent truncation The out-of-range offset is packed into an LStackSlot, truncating to a smaller offset that collides with an already-assigned slot.
  4. Alias two virtual registers Two logically distinct values are emitted to the same frame location, so writes to one corrupt the other.
  5. Exploit the corruption The aliased slot yields type confusion or out-of-bounds effects in the JIT-compiled code, giving the attacker a memory-safety primitive inside the content process.

Impact Assessment

An attacker who can run scripted content gains, in the JIT/JavaScript-engine context of the content process, a memory-corruption primitive from a mis-allocated (aliased) stack slot in generated code. The precondition is authoring a module or function large enough to overflow the stack-slot offset field, which is reachable from any page that can execute WebAssembly or JavaScript. This is a high-severity engine bug typically usable as a step toward sandbox-constrained code execution rather than a benign crash.

Changed Functions

FunctionChangeNotes
for
js/src/jit/SimpleAllocator.cpp
modified
if
js/src/jit/StackSlotAllocator.h
modified
switch
js/src/jit/StackSlotAllocator.h
modified

Files Changed

  • js/src/jit/BacktrackingAllocator.cpp
  • js/src/jit/BacktrackingAllocator.h
  • js/src/jit/LIR.h
  • js/src/jit/SimpleAllocator.cpp
  • js/src/jit/SimpleAllocator.h
  • js/src/jit/StackSlotAllocator.h

Audit Directions

  • Infallible allocation returning a bitfield-packed value
    audit any allocator whose result is stored into a narrow bitfield (LAllocation, tagged pointers, packed IR nodes) to confirm the value is range-checked against the field width before packing.
  • Unchecked monotonic counters (`height_`)
    flag uint32_t accumulators that grow with attacker-influenced input and are later used as offsets or shifted into encodings, ensuring each increment has an overflow guard like incrementHeight.
  • `void` helpers changed to `[[nodiscard]] bool`
    review sibling callers of the newly fallible functions across both allocators to verify every call site actually propagates the failure and does not swallow it.
diff --git a/js/src/jit/BacktrackingAllocator.cpp b/js/src/jit/BacktrackingAllocator.cpp
index 59d7c44564e..64e4d434d22 100644
--- a/js/src/jit/BacktrackingAllocator.cpp
+++ b/js/src/jit/BacktrackingAllocator.cpp
@@ -2239,11 +2239,13 @@ void BacktrackingAllocator::tryMergeBundles(LiveBundle* bundle0,
 }
 
 // Helper for ::mergeAndQueueRegisters
-void BacktrackingAllocator::allocateStackDefinition(VirtualRegister& reg) {
+bool BacktrackingAllocator::allocateStackDefinition(VirtualRegister& reg) {
   LInstruction* ins = reg.ins()->toInstruction();
   if (reg.def()->type() == LDefinition::STACKRESULTS) {
     LStackArea alloc(ins->toInstruction());
-    stackSlotAllocator.allocateStackArea(&alloc);
+    if (!stackSlotAllocator.allocateStackArea(&alloc)) {
+      return false;
+    }
     reg.def()->setOutput(alloc);
   } else {
     // Because the definitions are visited in order, the area has been allocated
@@ -2253,6 +2255,7 @@ void BacktrackingAllocator::allocateStackDefinition(VirtualRegister& reg) {
     const LStackArea* areaAlloc = area.def()->output()->toStackArea();
     reg.def()->setOutput(areaAlloc->resultAlloc(ins, reg.def()));
   }
+  return true;
 }
 
 // Helper for ::mergeAndQueueRegisters
@@ -2481,8 +2484,9 @@ bool BacktrackingAllocator::mergeAndQueueRegisters() {
     VirtualRegister& reg = vregs[i];
 
     // Eagerly allocate stack result areas and their component stack results.
-    if (reg.def() && reg.def()->policy() == LDefinition::STACK) {
-      allocateStackDefinition(reg);
+    if (reg.def() && reg.def()->policy() == LDefinition::STACK &&
+        !allocateStackDefinition(reg)) {
+      return false;
     }
 
     for (VirtualRegister::RangeIterator iter(reg); iter; iter++) {
@@ -3970,7 +3974,10 @@ bool BacktrackingAllocator::pickStackSlot(SpillSet* spillSet) {
 
   // We need a new physical stack slot.
   LStackSlot::Width width = LStackSlot::width(type);
-  uint32_t stackSlot = stackSlotAllocator.allocateSlot(width);
+  uint32_t stackSlot;
+  if (!stackSlotAllocator.allocateSlot(width, &stackSlot)) {
+    return false;
+  }
 
   SpillSlot* spillSlot =
       new (alloc().fallible()) SpillSlot(stackSlot, width, alloc().lifoAlloc());
diff --git a/js/src/jit/BacktrackingAllocator.h b/js/src/jit/BacktrackingAllocator.h
index 82b1955aa50..89d8e905185 100644
--- a/js/src/jit/BacktrackingAllocator.h
+++ b/js/src/jit/BacktrackingAllocator.h
@@ -867,7 +867,7 @@ class MOZ_STACK_CLASS BacktrackingAllocator : protected RegisterAllocator {
 
   // Merging and queueing of LiveRange groups
   void tryMergeBundles(LiveBundle* bundle0, LiveBundle* bundle1);
-  void allocateStackDefinition(VirtualRegister& reg);
+  [[nodiscard]] bool allocateStackDefinition(VirtualRegister& reg);
   [[nodiscard]] bool tryMergeReusedRegister(VirtualRegister& def,
                                             VirtualRegister& input);
   [[nodiscard]] bool mergeAndQueueRegisters();
diff --git a/js/src/jit/LIR.h b/js/src/jit/LIR.h
index 3f4efeda795..cac3baca33e 100644
--- a/js/src/jit/LIR.h
+++ b/js/src/jit/LIR.h
@@ -433,6 +433,9 @@ class LStackSlot : public LAllocation {
     Width width() const { return Width(data_ & WIDTH_MASK); }
   };
 
+  static constexpr uint32_t MAX_SLOT =
+      (uint64_t(1) << LAllocation::DATA_BITS) - 1;
+
   explicit LStackSlot(SlotAndWidth slotAndWidth)
       : LAllocation(STACK_SLOT, slotAndWidth.data()) {}
 
diff --git a/js/src/jit/SimpleAllocator.cpp b/js/src/jit/SimpleAllocator.cpp
index 1b668fec2f0..8f6b0d61827 100644
--- a/js/src/jit/SimpleAllocator.cpp
+++ b/js/src/jit/SimpleAllocator.cpp
@@ -383,16 +383,22 @@ void SimpleAllocator::removeAllocatedRegisterAtIndex(size_t index) {
   }
 }
 
-LAllocation SimpleAllocator::ensureStackLocation(uint32_t vregId) {
+bool SimpleAllocator::ensureStackLocation(uint32_t vregId, LAllocation* alloc) {
   // Allocate a stack slot for this virtual register if needed.
   VirtualRegister& vreg = vregs_[vregId];
   if (vreg.hasStackLocation()) {
-    return vreg.stackLocation();
+    *alloc = vreg.stackLocation();
+    return true;
   }
   LStackSlot::Width width = LStackSlot::width(vreg.def()->type());
-  LStackSlot::SlotAndWidth slot(stackSlotAllocator_.allocateSlot(width), width);
+  uint32_t slotOffset;
+  if (!stackSlotAllocator_.allocateSlot(width, &slotOffset)) {
+    return false;
+  }
+  LStackSlot::SlotAndWidth slot(slotOffset, width);
   vreg.setAllocatedStackSlot(slot);
-  return LStackSlot(slot);
+  *alloc = LStackSlot(slot);
+  return true;
 }
 
 LAllocation SimpleAllocator::registerOrStackLocation(LInstruction* ins,
@@ -418,7 +424,10 @@ bool SimpleAllocator::spillRegister(LInstruction* ins,
   }
   // Allocate a new stack slot and insert a register => stack move.
   LMoveGroup* input = getInputMoveGroup(ins);
-  LAllocation dest = ensureStackLocation(allocated.vregId());
+  LAllocation dest;
+  if (!ensureStackLocation(allocated.vregId(), &dest)) {
+    return false;
+  }
   return input->addAfter(LAllocation(allocated.reg()), dest,
                          vreg.def()->type());
 }
@@ -469,7 +478,10 @@ bool SimpleAllocator::allocateForBlockEnd(LBlock* block, LInstruction* ins) {
 
     LAllocation source =
         registerOrStackLocation(ins, sourceVreg, /* trackRegUse = */ true);
-    LAllocation dest = ensureStackLocation(destVreg);
+    LAllocation dest;
+    if (!ensureStackLocation(destVreg, &dest)) {
+      return false;
+    }
     if (!group->add(source, dest, phi->getDef(0)->type())) {
       return false;
     }
@@ -795,7 +807,9 @@ bool SimpleAllocator::allocateForDefinition(uint32_t blockLastId,
       MOZ_ASSERT(!isTemp);
       if (def->type() == LDefinition::STACKRESULTS) {
         LStackArea alloc(ins->toInstruction());
-        stackSlotAllocator_.allocateStackArea(&alloc);
+        if (!stackSlotAllocator_.allocateStackArea(&alloc)) {
+          return false;
+        }
         def->setOutput(alloc);
       } else {
         // Because the definitions are visited in order, the area has been
@@ -844,7 +858,10 @@ bool SimpleAllocator::allocateForInstruction(VirtualRegBitSet& liveGC,
     LMoveGroup* moves = getInputMoveGroup(ins);
     for (LDefinition* def : eagerSpillOutputs_) {
       MOZ_ASSERT(!vregs_[def->virtualRegister()].hasStackLocation());
-      LAllocation dest = ensureStackLocation(def->virtualRegister());
+      LAllocation dest;
+      if (!ensureStackLocation(def->virtualRegister(), &dest)) {
+        return false;
+      }
       if (!moves->add(*def->output(), dest, def->type())) {
         return false;
       }
@@ -1182,7 +1199,11 @@ bool SimpleAllocator::allocateRegisters() {
       LDefinition* def = phi->getDef(0);
       uint32_t vregId = def->virtualRegister();
       bool isGCType = vregs_[vregId].isGCType();
-      def->setOutput(ensureStackLocation(vregId));
+      LAllocation defAlloc;
+      if (!ensureStackLocation(vregId, &defAlloc)) {
+        return false;
+      }
+      def->setOutput(defAlloc);
       if (isGCType && !liveGC.insert(vregId)) {
         return false;
       }
diff --git a/js/src/jit/SimpleAllocator.h b/js/src/jit/SimpleAllocator.h
index dc282fd51e0..3aca8073908 100644
--- a/js/src/jit/SimpleAllocator.h
+++ b/js/src/jit/SimpleAllocator.h
@@ -323,7 +323,7 @@ class MOZ_STACK_CLASS SimpleAllocator : protected RegisterAllocator {
 
   [[nodiscard]] bool allocateForBlockEnd(LBlock* block, LInstruction* ins);
 
-  LAllocation ensureStackLocation(uint32_t vregId);
+  bool ensureStackLocation(uint32_t vregId, LAllocation* allocation);
   LAllocation registerOrStackLocation(LInstruction* ins, uint32_t vregId,
                                       bool trackRegUse);
 
diff --git a/js/src/jit/StackSlotAllocator.h b/js/src/jit/StackSlotAllocator.h
index ab666d9e53f..051c3d14e79 100644
--- a/js/src/jit/StackSlotAllocator.h
+++ b/js/src/jit/StackSlotAllocator.h
@@ -17,97 +17,165 @@ class StackSlotAllocator {
   js::Vector<uint32_t, 4, SystemAllocPolicy> quadSlots;
   uint32_t height_;
 
-  void addAvailableSlot(uint32_t index) {
-    // Ignoring OOM here (and below) is fine; it just means the stack slot
-    // will be unused.
-    (void)normalSlots.append(index);
-  }
-  void addAvailableDoubleSlot(uint32_t index) {
-    (void)doubleSlots.append(index);
+  [[nodiscard]] bool incrementHeight(uint32_t amount) {
+    // See MaxBytes for why we don't need to check for overflow here.
+    if (amount > MaxBytes || height_ + amount > MaxBytes) {
+      return false;
+    }
+    height_ += amount;
+    return true;
   }
-  void addAvailableQuadSlot(uint32_t index) { (void)quadSlots.append(index); }
 
-  uint32_t allocateQuadSlot() {
-    // This relies on the fact that any architecture specific
-    // alignment of the stack pointer is done a priori.
-    if (!quadSlots.empty()) {
-      return quadSlots.popCopy();
+  void freeSlot(uint32_t offset) { (void)normalSlots.append(offset); }
+  void freeDoubleSlot(uint32_t offset) { (void)doubleSlots.append(offset); }
+  void freeQuadSlot(uint32_t offset) { (void)quadSlots.append(offset); }
+
+  [[nodiscard]] bool allocateSlot(uint32_t* slotOffset) {
+    // Re-use a normal slot if we have one.
+    if (!normalSlots.empty()) {
+      *slotOffset = normalSlots.popCopy();
+      return true;
     }
-    if (height_ % 8 != 0) {
-      addAvailableSlot(height_ += 4);
+
+    // Otherwise split a double slot in half if we can.
+    if (!doubleSlots.empty()) {
+      uint32_t doubleSlotOffset = doubleSlots.popCopy();
+      freeSlot(doubleSlotOffset - 4);
+      *slotOffset = doubleSlotOffset;
+      return true;
     }
-    if (height_ % 16 != 0) {
-      addAvailableDoubleSlot(height_ += 8);
+
+    // Or else grow the stack by a word.
+    if (!incrementHeight(4)) {
+      return false;
     }
-    return height_ += 16;
+    *slotOffset = height_;
+    return true;
   }
-  uint32_t allocateDoubleSlot() {
+
+  [[nodiscard]] bool allocateDoubleSlot(uint32_t* slotOffset) {
     if (!doubleSlots.empty()) {
-      return doubleSlots.popCopy();
+      *slotOffset = doubleSlots.popCopy();
+      return true;
     }
+
+    // Grow the stack, freeing the padding word for reuse if we need to align.
     if (height_ % 8 != 0) {
-      addAvailableSlot(height_ += 4);
+      if (!incrementHeight(4)) {
+        return false;
+      }
+      freeSlot(height_);
+    }
+    if (!incrementHeight(8)) {
+      return false;
     }
-    return height_ += 8;
+    *slotOffset = height_;
+    return true;
   }
-  uint32_t allocateSlot() {
-    if (!normalSlots.empty()) {
-      return normalSlots.popCopy();
+
+  [[nodiscard]] bool allocateQuadSlot(uint32_t* slotOffset) {
+    if (!quadSlots.empty()) {
+      *slotOffset = quadSlots.popCopy();
+      return true;
     }
-    if (!doubleSlots.empty()) {
-      uint32_t index = doubleSlots.popCopy();
-      addAvailableSlot(index - 4);
-      return index;
+
+    // Grow the stack, freeing any padding for reuse if we need to align. This
+    // relies on the fact that any architecture specific alignment of the stack
+    // pointer is done a priori.
+    if (height_ % 8 != 0) {
+      if (!incrementHeight(4)) {
+        return false;
+      }
+      freeSlot(height_);
+    }
+    if (height_ % 16 != 0) {
+      if (!incrementHeight(8)) {
+        return false;
+      }
+      freeDoubleSlot(height_);
     }
-    return height_ += 4;
+    if (!incrementHeight(16)) {
+      return false;
+    }
+    *slotOffset = height_;
+    return true;
   }
 
  public:
   StackSlotAllocator() : height_(0) {}
 
-  void allocateStackArea(LStackArea* alloc) {
+  // The maximum size we allow. This is a conservative value, even values this
+  // low are very likely to hit stack overflows.
+  static constexpr size_t MaxBytes = 2 * 1024 * 1024;
+  // Check that as long as we keep height_ within MaxBytes and only add by up
+  // to MaxBytes, that we cannot overflow. This is used in several places.
+  static_assert(uint64_t(MaxBytes) + uint64_t(MaxBytes) <= UINT32_MAX);
+  // Any offset we provide can be stored in LStackSlot.
+  static_assert(MaxBytes <= LStackSlot::MAX_SLOT);
+
+  [[nodiscard]] bool allocateStackArea(LStackArea* alloc) {
     uint32_t size = alloc->size();
 
+    // Check that size is within MaxBytes so that we don't need to check for
+    // overflow below when adding to height_.
+    if (size > MaxBytes) {
+      return false;
+    }
+
     MOZ_ASSERT(size % 4 == 0);
     switch (alloc->alignment()) {
-      case 8:
+      case 8: {
+        // Grow the stack, freeing the padding word for reuse if we need to
+        // align.
         if ((height_ + size) % 8 != 0) {
-          addAvailableSlot(height_ += 4);
+          if (!incrementHeight(4)) {
+            return false;
+          }
+          freeSlot(height_);
         }
         break;
+      }
       default:
         MOZ_CRASH("unexpected stack results area alignment");
     }
-    MOZ_ASSERT((height_ + size) % alloc->alignment() == 0);
 
-    height_ += size;
-    alloc->setBase(height_);
+    // See MaxBytes for why we don't need to check for overflow here.
+    uint32_t areaSlotOffset = height_ + size;
+    if (areaSlotOffset > MaxBytes) {
+      return false;
+    }
+    MOZ_ASSERT(areaSlotOffset % alloc->alignment() == 0);
+
+    alloc->setBase(areaSlotOffset);
+    height_ = areaSlotOffset;
+    return true;
   }
 
-  uint32_t allocateSlot(LStackSlot::Width width) {
+  [[nodiscard]] bool allocateSlot(LStackSlot::Width width,
+                                  uint32_t* slotOffset) {
     switch (width) {
       case LStackSlot::Word:
-        return allocateSlot();
+        return allocateSlot(slotOffset);
       case LStackSlot::DoubleWord:
-        return allocateDoubleSlot();
+        return allocateDoubleSlot(slotOffset);
       case LStackSlot::QuadWord:
-        return allocateQuadSlot();
+        return allocateQuadSlot(slotOffset);
     }
     MOZ_CRASH("Unknown slot width");
   }
 
   // This method is used by the Simple allocator to free stack slots so that
   // they can be reused. The Backtracking allocator doesn't call this.
-  void freeSlot(LStackSlot::Width width, uint32_t slot) {
+  void freeSlot(LStackSlot::Width width, uint32_t slotOffset) {
     switch (width) {
       case LStackSlot::Word:
-        addAvailableSlot(slot);
+        freeSlot(slotOffset);
         return;
       case LStackSlot::DoubleWord:
-        addAvailableDoubleSlot(slot);
+        freeDoubleSlot(slotOffset);
         return;
       case LStackSlot::QuadWord:
-        addAvailableQuadSlot(slot);
+        freeQuadSlot(slotOffset);
         return;
     }
     MOZ_CRASH("Unknown slot width");
Loading diff…