CVE-2025-3031
Overview
Background
- SpiderMonkey IonMonkey register allocator
BacktrackingAllocatorassigns 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
CanMergeTypesInBundleallows 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.
LStackSlot and asserting it matches the value type at snapshot-encode time.Attack Path
- Script Attacker-controlled JavaScript runs long/hot enough to be Ion-compiled, so values get spilled to stack slots.
- 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).
- Trigger bailout
The attacker forces a deoptimization/bailout so the interpreter reconstructs the value from its
RValueAllocation. - Over-read Recovery reads the full (wider) value from the narrow slot, pulling in adjacent, previously-written stack bytes.
- 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
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
| Function | Change | Notes |
|---|---|---|
ifjs/src/jit/BacktrackingAllocator.cpp |
modified | |
LStackSlotjs/src/jit/LIR.h |
modified | |
switchjs/src/jit/LIR.h |
modified | |
ifjs/src/jit/Safepoints.cpp |
modified | |
switchjs/src/jit/StackSlotAllocator.h |
modified |
Files Changed
js/src/jit/BacktrackingAllocator.cppjs/src/jit/BacktrackingAllocator.hjs/src/jit/LIR.cppjs/src/jit/LIR.hjs/src/jit/Safepoints.cppjs/src/jit/StackSlotAllocator.hjs/src/jit/shared/CodeGenerator-shared.cppjs/src/jit/shared/LIR-shared.h
Audit Directions
- Other `LAllocation`/slot constructorsAudit remaining
LStackSlotconstruction sites and any code that re-derives slot width from type for consistency with the storedwidth(). - Snapshot/bailout width handlingReview
RValueAllocationdecode andSnapshotReader/RInstructionrecovery for other places that assume a slot’s width, now that width is authoritative in the allocation. - Register-allocator merging rulesRe-examine bundle/slot merging and reuse (
tryMergeReusedRegister, spill-set coalescing) for assumptions that two differently-typed values sharing a slot are width-compatible.
Patch
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);
}
};