CVE-2026-16369
Overview
Background
- `StackSlotAllocator`
- the JIT helper that hands out stack-frame offsets for spilled virtual registers, tracking the frame
height_as auint32_t. - `LStackSlot`
- an
LAllocationsubclass that packs a stack-slot offset plus width into a bitfield limited toLAllocation::DATA_BITSbits. - `BacktrackingAllocator` / `SimpleAllocator`
- the two Ion/Wasm register allocators that call into
StackSlotAllocatorto 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.
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
- 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.
- Force a huge frame height
During register allocation, repeated
allocateSlot/allocateStackAreacalls driveheight_past the value encodable inLAllocation::DATA_BITS. - 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. - Alias two virtual registers Two logically distinct values are emitted to the same frame location, so writes to one corrupt the other.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
forjs/src/jit/SimpleAllocator.cpp |
modified | |
ifjs/src/jit/StackSlotAllocator.h |
modified | |
switchjs/src/jit/StackSlotAllocator.h |
modified |
Files Changed
js/src/jit/BacktrackingAllocator.cppjs/src/jit/BacktrackingAllocator.hjs/src/jit/LIR.hjs/src/jit/SimpleAllocator.cppjs/src/jit/SimpleAllocator.hjs/src/jit/StackSlotAllocator.h
Audit Directions
- Infallible allocation returning a bitfield-packed valueaudit 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_taccumulators that grow with attacker-influenced input and are later used as offsets or shifted into encodings, ensuring each increment has an overflow guard likeincrementHeight. - `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.
Patch
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");