Medium CVSS 6.5 firefox Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionAn attacker could read 32 bits of values spilled onto the stack in a JIT compiled function.
ComponentSpiderMonkey
Bug ClassInfo Disclosure
Tracker1947141
Fix commitc7c404f78c99 (firefox) +127/-55
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
CISA KEVNot listed
Creditedanbu
Disclosed2025-04-01

Background

SpiderMonkey IonMonkey register allocator
BacktrackingAllocator assigns virtual registers to physical registers and spill (stack) slots during JIT compilation.
`StackSlotAllocator::width(LDefinition::Type)`
Mapped a value type to a byte width (4/8/16) for allocating slots; the width was used to pick the right free list but not persisted into the LStackSlot.
`SpillSet` merging
CanMergeTypesInBundle allows two values to share a spill slot only if their widths match.

Root Cause Analysis

A stack slot’s width was computed transiently (StackSlotAllocator::width(type)) but never stored in the LStackSlot allocation itself. Consequently, nothing tied a given physical spill slot to the width of the value placed in it, and later consumers that needed the width had to re-derive it from the value’s type — which could differ from the width the slot was actually sized/allocated for. When bailout metadata is produced in CodeGeneratorShared::encodeAllocation, a payload living in a stack slot is encoded (e.g. RValueAllocation::Typed(valueType, ToStackIndex(payload))); if the slot was allocated as a narrow (4-byte) word but the value is reconstructed as a wider quantity, the recovery reads bytes beyond the 32 bits that were actually written, disclosing adjacent, uninitialized spilled stack contents. The advisory states precisely: “read 32 bits of values spilled onto the stack in a JIT compiled function.” The advisory’s generic “LogicError” label is really an information-disclosure (CWE-200) caused by the allocator losing track of spill-slot widths.

The patch threads width through the slot representation. LStackSlot now encodes a Width enum (Word/DoubleWord/QuadWord) in the two least-significant bits (slots are 4-byte aligned, so those bits are free), with slot()/width() accessors and ByteWidth(). The type→width mapping moves from StackSlotAllocator::width to LStackSlot::width(LDefinition::Type), and StackSlotAllocator::allocateSlot now takes an LStackSlot::Width. SpillSlot stores the width and constructs its LStackSlot alloc with it; pickStackSlot chooses the free list by LStackSlot::width(type) and builds the slot with the matching width. Crucially, CodeGeneratorShared::encodeAllocation adds MOZ_ASSERT_IF(payload->isStackSlot(), payload->toStackSlot()->width() == LStackSlot::width(LDefinition::TypeFrom(type))) (and analogous asserts for GENERAL/int64/intptr cases), enforcing that the width recorded in the slot matches the width implied by the value’s type at encode time — catching exactly the mismatch that leaked bytes. Safepoint decoding (PartFromStream) and wasm stack-result iterators are updated to construct LStackSlots with explicit widths.

Key insight
Spill stack slots carried their offset but not their width, so a value could be stored in a narrow (32-bit) slot yet recovered as a wider value during bailout, reading beyond the written bytes and leaking adjacent spilled stack data — fixed by storing the width in the LStackSlot and asserting it matches the value type at snapshot-encode time.

Attack Path

  1. Script Attacker-controlled JavaScript runs long/hot enough to be Ion-compiled, so values get spilled to stack slots.
  2. Width mismatch The function is shaped so a value is allocated a narrow spill slot but the snapshot/recovery machinery treats it as a wider type (inferred from the asserts the patch adds; the diff proves the width was previously untracked).
  3. Trigger bailout The attacker forces a deoptimization/bailout so the interpreter reconstructs the value from its RValueAllocation.
  4. Over-read Recovery reads the full (wider) value from the narrow slot, pulling in adjacent, previously-written stack bytes.
  5. Primitive Disclosure of 32 bits of otherwise-inaccessible spilled stack contents per leak, repeatable to read sensitive in-process data (e.g. defeating ASLR / leaking pointers).

Impact Assessment

The flaw is in the renderer/content process’s JIT (SpiderMonkey Ion), reachable by any web page that gets JIT-compiled JavaScript — no special privilege. The primitive is an information leak: the attacker reads 32 bits of spilled stack values that should not be observable, which is a disclosure (not memory corruption or RCE). Its strength is in leaking in-process secrets such as pointers to aid other exploits; on its own it is a confidentiality bug (advisory “moderate”, CVSS C:L/I:L). The exact source-shape needed to induce the width mismatch is (inferred) from the added assertions rather than spelled out by the diff.

Changed Functions

FunctionChangeNotes
if
js/src/jit/BacktrackingAllocator.cpp
modified
LStackSlot
js/src/jit/LIR.h
modified
switch
js/src/jit/LIR.h
modified
if
js/src/jit/Safepoints.cpp
modified
switch
js/src/jit/StackSlotAllocator.h
modified

Files Changed

  • js/src/jit/BacktrackingAllocator.cpp
  • js/src/jit/BacktrackingAllocator.h
  • js/src/jit/LIR.cpp
  • js/src/jit/LIR.h
  • js/src/jit/Safepoints.cpp
  • js/src/jit/StackSlotAllocator.h
  • js/src/jit/shared/CodeGenerator-shared.cpp
  • js/src/jit/shared/LIR-shared.h

Audit Directions

  • Other `LAllocation`/slot constructors
    Audit remaining LStackSlot construction sites and any code that re-derives slot width from type for consistency with the stored width().
  • Snapshot/bailout width handling
    Review RValueAllocation decode and SnapshotReader/RInstruction recovery for other places that assume a slot’s width, now that width is authoritative in the allocation.
  • Register-allocator merging rules
    Re-examine bundle/slot merging and reuse (tryMergeReusedRegister, spill-set coalescing) for assumptions that two differently-typed values sharing a slot are width-compatible.
diff --git a/js/src/jit/BacktrackingAllocator.cpp b/js/src/jit/BacktrackingAllocator.cpp
index ba8551469b0..d97f187c191 100644
--- a/js/src/jit/BacktrackingAllocator.cpp
+++ b/js/src/jit/BacktrackingAllocator.cpp
@@ -2095,7 +2095,7 @@ static bool CanMergeTypesInBundle(LDefinition::Type a, LDefinition::Type b) {
 
   // Only merge if the sizes match, so that we don't get confused about the
   // width of spill slots.
-  return StackSlotAllocator::width(a) == StackSlotAllocator::width(b);
+  return LStackSlot::width(a) == LStackSlot::width(b);
 }
 
 // Helper for ::tryMergeReusedRegister
@@ -3892,14 +3892,14 @@ bool BacktrackingAllocator::pickStackSlot(SpillSet* spillSet) {
       spillSet->spilledBundle(0)->firstRange()->vreg().type();
 
   SpillSlotList* slotList;
-  switch (StackSlotAllocator::width(type)) {
-    case 4:
+  switch (LStackSlot::width(type)) {
+    case LStackSlot::Word:
       slotList = &normalSlots;
       break;
-    case 8:
+    case LStackSlot::DoubleWord:
       slotList = &doubleSlots;
       break;
-    case 16:
+    case LStackSlot::QuadWord:
       slotList = &quadSlots;
       break;
     default:
@@ -3963,10 +3963,11 @@ bool BacktrackingAllocator::pickStackSlot(SpillSet* spillSet) {
   }
 
   // We need a new physical stack slot.
-  uint32_t stackSlot = stackSlotAllocator.allocateSlot(type);
+  LStackSlot::Width width = LStackSlot::width(type);
+  uint32_t stackSlot = stackSlotAllocator.allocateSlot(width);
 
   SpillSlot* spillSlot =
-      new (alloc().fallible()) SpillSlot(stackSlot, alloc().lifoAlloc());
+      new (alloc().fallible()) SpillSlot(stackSlot, width, alloc().lifoAlloc());
   if (!spillSlot) {
     return false;
   }
diff --git a/js/src/jit/BacktrackingAllocator.h b/js/src/jit/BacktrackingAllocator.h
index e3c815992f8..f65f0cee59b 100644
--- a/js/src/jit/BacktrackingAllocator.h
+++ b/js/src/jit/BacktrackingAllocator.h
@@ -726,8 +726,8 @@ class BacktrackingAllocator : protected RegisterAllocator {
     LStackSlot alloc;
     LiveRangePlusSet allocated;
 
-    SpillSlot(uint32_t slot, LifoAlloc* alloc)
-        : alloc(slot), allocated(alloc) {}
+    SpillSlot(uint32_t slot, LStackSlot::Width width, LifoAlloc* alloc)
+        : alloc(slot, width), allocated(alloc) {}
   };
   using SpillSlotList = InlineForwardList<SpillSlot>;
 
diff --git a/js/src/jit/LIR.cpp b/js/src/jit/LIR.cpp
index 7e374144e0c..cecdc666f62 100644
--- a/js/src/jit/LIR.cpp
+++ b/js/src/jit/LIR.cpp
@@ -494,7 +494,8 @@ UniqueChars LAllocation::toString() const {
         buf = JS_smprintf("%s", toFloatReg()->reg().name());
         break;
       case LAllocation::STACK_SLOT:
-        buf = JS_smprintf("stack:%u", toStackSlot()->slot());
+        buf = JS_smprintf("stack:%u(%u)", toStackSlot()->slot(),
+                          LStackSlot::ByteWidth(toStackSlot()->width()));
         break;
       case LAllocation::ARGUMENT_SLOT:
         buf = JS_smprintf("arg:%u", toArgument()->index());
diff --git a/js/src/jit/LIR.h b/js/src/jit/LIR.h
index 15ddc3ee821..fbce20245b8 100644
--- a/js/src/jit/LIR.h
+++ b/js/src/jit/LIR.h
@@ -392,10 +392,51 @@ class LConstantIndex : public LAllocation {
 
 // Stack slots are indices into the stack. The indices are byte indices.
 class LStackSlot : public LAllocation {
+  // Stack slots are aligned to 32-bit word boundaries.
+  static constexpr uint32_t SLOT_ALIGNMENT = 4;
+
+  // Stack slot width is stored in the two least significant bits.
+  static constexpr uint32_t WIDTH_MASK = SLOT_ALIGNMENT - 1;
+
+  // Remaining bits hold the stack slot offset.
+  static constexpr uint32_t SLOT_MASK = ~WIDTH_MASK;
+
  public:
-  explicit LStackSlot(uint32_t slot) : LAllocation(STACK_SLOT, slot) {}
+  enum Width {
+    Word,
+    DoubleWord,
+    QuadWord,
+  };
+
+  LStackSlot(uint32_t slot, Width width)
+      : LAllocation(STACK_SLOT, slotAndWidth(slot, width)) {}
+
+  uint32_t slot() const { return data() & SLOT_MASK; }
+
+  Width width() const { return Width(data() & WIDTH_MASK); }
+
+  // |Type| is LDefinition::Type, but can't forward declare a nested definition.
+  template <typename Type>
+  static Width width(Type type);
+
+  static uint32_t ByteWidth(Width width) {
+    switch (width) {
+      case Width::Word:
+        return 4;
+      case Width::DoubleWord:
+        return 8;
+      case Width::QuadWord:
+        return 16;
+    }
+    MOZ_CRASH("invalid width");
+  }
 
-  uint32_t slot() const { return data(); }
+ private:
+  static uint32_t slotAndWidth(uint32_t slot, Width width) {
+    MOZ_ASSERT(slot % SLOT_ALIGNMENT == 0);
+    MOZ_ASSERT(uint32_t(width) < SLOT_ALIGNMENT);
+    return slot | uint32_t(width);
+  }
 };
 
 // Stack area indicates a contiguous stack allocation meant to receive call
@@ -689,6 +730,41 @@ class LInt64Definition : public LInt64Value<LDefinition> {
   }
 };
 
+template <>
+inline LStackSlot::Width LStackSlot::width(LDefinition::Type type) {
+  switch (type) {
+#if JS_BITS_PER_WORD == 32
+    case LDefinition::GENERAL:
+    case LDefinition::OBJECT:
+    case LDefinition::SLOTS:
+    case LDefinition::WASM_ANYREF:
+#endif
+#ifdef JS_NUNBOX32
+    case LDefinition::TYPE:
+    case LDefinition::PAYLOAD:
+#endif
+    case LDefinition::INT32:
+    case LDefinition::FLOAT32:
+      return LStackSlot::Word;
+#if JS_BITS_PER_WORD == 64
+    case LDefinition::GENERAL:
+    case LDefinition::OBJECT:
+    case LDefinition::SLOTS:
+    case LDefinition::WASM_ANYREF:
+#endif
+#ifdef JS_PUNBOX64
+    case LDefinition::BOX:
+#endif
+    case LDefinition::DOUBLE:
+      return LStackSlot::DoubleWord;
+    case LDefinition::SIMD128:
+      return LStackSlot::QuadWord;
+    case LDefinition::STACKRESULTS:
+      MOZ_CRASH("Stack results area must be allocated manually");
+  }
+  MOZ_CRASH("Unknown slot type");
+}
+
 // Forward declarations of LIR types.
 #define LIROP(op) class L##op;
 LIR_OPCODE_LIST(LIROP)
diff --git a/js/src/jit/Safepoints.cpp b/js/src/jit/Safepoints.cpp
index db1c05b70ce..77245b1cc27 100644
--- a/js/src/jit/Safepoints.cpp
+++ b/js/src/jit/Safepoints.cpp
@@ -541,7 +541,7 @@ static inline LAllocation PartFromStream(CompactBufferReader& stream,
   }
 
   if (kind == Part_Stack) {
-    return LStackSlot(info);
+    return LStackSlot(info, LStackSlot::Word);
   }
 
   MOZ_ASSERT(kind == Part_Arg);
diff --git a/js/src/jit/StackSlotAllocator.h b/js/src/jit/StackSlotAllocator.h
index 1f11b0e785b..c8f8893c563 100644
--- a/js/src/jit/StackSlotAllocator.h
+++ b/js/src/jit/StackSlotAllocator.h
@@ -7,6 +7,7 @@
 #ifndef jit_StackSlotAllocator_h
 #define jit_StackSlotAllocator_h
 
+#include "jit/LIR.h"
 #include "jit/Registers.h"
 
 namespace js {
@@ -80,47 +81,13 @@ class StackSlotAllocator {
     alloc->setBase(height_);
   }
 
-  static uint32_t width(LDefinition::Type type) {
-    switch (type) {
-#if JS_BITS_PER_WORD == 32
-      case LDefinition::GENERAL:
-      case LDefinition::OBJECT:
-      case LDefinition::SLOTS:
-      case LDefinition::WASM_ANYREF:
-#endif
-#ifdef JS_NUNBOX32
-      case LDefinition::TYPE:
-      case LDefinition::PAYLOAD:
-#endif
-      case LDefinition::INT32:
-      case LDefinition::FLOAT32:
-        return 4;
-#if JS_BITS_PER_WORD == 64
-      case LDefinition::GENERAL:
-      case LDefinition::OBJECT:
-      case LDefinition::SLOTS:
-      case LDefinition::WASM_ANYREF:
-#endif
-#ifdef JS_PUNBOX64
-      case LDefinition::BOX:
-#endif
-      case LDefinition::DOUBLE:
-        return 8;
-      case LDefinition::SIMD128:
-        return 16;
-      case LDefinition::STACKRESULTS:
-        MOZ_CRASH("Stack results area must be allocated manually");
-    }
-    MOZ_CRASH("Unknown slot type");
-  }
-
-  uint32_t allocateSlot(LDefinition::Type type) {
-    switch (width(type)) {
-      case 4:
+  uint32_t allocateSlot(LStackSlot::Width width) {
+    switch (width) {
+      case LStackSlot::Word:
         return allocateSlot();
-      case 8:
+      case LStackSlot::DoubleWord:
         return allocateDoubleSlot();
-      case 16:
+      case LStackSlot::QuadWord:
         return allocateQuadSlot();
     }
     MOZ_CRASH("Unknown slot width");
diff --git a/js/src/jit/shared/CodeGenerator-shared.cpp b/js/src/jit/shared/CodeGenerator-shared.cpp
index 7ed36389bae..ac3c68282c6 100644
--- a/js/src/jit/shared/CodeGenerator-shared.cpp
+++ b/js/src/jit/shared/CodeGenerator-shared.cpp
@@ -438,6 +438,9 @@ void CodeGeneratorShared::encodeAllocation(LSnapshot* snapshot,
 
       MOZ_DIAGNOSTIC_ASSERT(payload->isMemory() || payload->isRegister());
       if (payload->isMemory()) {
+        MOZ_ASSERT_IF(payload->isStackSlot(),
+                      payload->toStackSlot()->width() ==
+                          LStackSlot::width(LDefinition::TypeFrom(type)));
         alloc = RValueAllocation::Typed(valueType, ToStackIndex(payload));
       } else if (payload->isGeneralReg()) {
         alloc = RValueAllocation::Typed(valueType, ToRegister(payload));
@@ -464,6 +467,9 @@ void CodeGeneratorShared::encodeAllocation(LSnapshot* snapshot,
       if (payload->isFloatReg()) {
         alloc = RValueAllocation::AnyFloat(ToFloatRegister(payload));
       } else {
+        MOZ_ASSERT_IF(payload->isStackSlot(),
+                      payload->toStackSlot()->width() ==
+                          LStackSlot::width(LDefinition::TypeFrom(type)));
         alloc = RValueAllocation::AnyFloat(ToStackIndex(payload));
       }
       break;
@@ -496,6 +502,9 @@ void CodeGeneratorShared::encodeAllocation(LSnapshot* snapshot,
       if (payload->isGeneralReg()) {
         alloc = RValueAllocation::IntPtr(ToRegister(payload));
       } else {
+        MOZ_ASSERT_IF(payload->isStackSlot(),
+                      payload->toStackSlot()->width() ==
+                          LStackSlot::width(LDefinition::GENERAL));
         alloc = RValueAllocation::IntPtr(ToStackIndex(payload));
       }
       break;
@@ -546,6 +555,13 @@ void CodeGeneratorShared::encodeAllocation(LSnapshot* snapshot,
       LAllocation* type = snapshot->typeOfSlot(*allocIndex);
       MOZ_ASSERT(type->isMemory() || type->isRegister());
 
+      MOZ_ASSERT_IF(payload->isStackSlot(),
+                    payload->toStackSlot()->width() ==
+                        LStackSlot::width(LDefinition::GENERAL));
+      MOZ_ASSERT_IF(type->isStackSlot(),
+                    type->toStackSlot()->width() ==
+                        LStackSlot::width(LDefinition::GENERAL));
+
       if (payload->isRegister()) {
         if (type->isRegister()) {
           alloc =
@@ -567,6 +583,9 @@ void CodeGeneratorShared::encodeAllocation(LSnapshot* snapshot,
       if (payload->isRegister()) {
         alloc = RValueAllocation::Int64(ToRegister(payload));
       } else {
+        MOZ_ASSERT_IF(payload->isStackSlot(),
+                      payload->toStackSlot()->width() ==
+                          LStackSlot::width(LDefinition::GENERAL));
         alloc = RValueAllocation::Int64(ToStackIndex(payload));
       }
 #endif
diff --git a/js/src/jit/shared/LIR-shared.h b/js/src/jit/shared/LIR-shared.h
index d7235c0813f..8bbce714d4c 100644
--- a/js/src/jit/shared/LIR-shared.h
+++ b/js/src/jit/shared/LIR-shared.h
@@ -937,7 +937,14 @@ inline void LStackArea::ResultIterator::next() {
 inline LAllocation LStackArea::ResultIterator::alloc() const {
   MOZ_ASSERT(!done());
   MWasmStackResultArea* area = alloc_.ins()->toWasmStackResultArea()->mir();
-  return LStackSlot(area->base() - area->result(idx_).offset());
+  const auto& stackResult = area->result(idx_);
+  MIRType type = stackResult.type();
+  auto width =
+#ifndef JS_PUNBOX64
+      type == MIRType::Int64 ? LStackSlot::DoubleWord :
+#endif
+                             LStackSlot::width(LDefinition::TypeFrom(type));
+  return LStackSlot(area->base() - stackResult.offset(), width);
 }
 inline bool LStackArea::ResultIterator::isWasmAnyRef() const {
   MOZ_ASSERT(!done());
@@ -961,7 +968,8 @@ class LWasmStackResult : public LInstructionHelper<1, 1, 0> {
 
   MWasmStackResult* mir() const { return mir_->toWasmStackResult(); }
   LStackSlot result(uint32_t base) const {
-    return LStackSlot(base - mir()->result().offset());
+    auto width = LStackSlot::width(LDefinition::TypeFrom(mir()->type()));
+    return LStackSlot(base - mir()->result().offset(), width);
   }
 };
 
@@ -984,7 +992,7 @@ class LWasmStackResult64 : public LInstructionHelper<INT64_PIECES, 1, 0> {
 #else
     MOZ_ASSERT(def == getDef(0));
 #endif
-    return LStackSlot(offset);
+    return LStackSlot(offset, LStackSlot::DoubleWord);
   }
 };
 
Loading diff…