Assertion failure: fallibleScope_ ([OOM] Cannot allocate a new chunk in an infallible scope.) in js::jit::WarpBuilder::build with many unbuilt generator inner-loop resume blocks
Categories
(Core :: JavaScript Engine: JIT, defect, P3)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr153 | --- | unaffected |
| firefox158 | --- | wontfix |
| firefox159 | --- | wontfix |
| firefox160 | --- | fixed |
People
(Reporter: bugmon, Assigned: iain)
References
(Blocks 1 open bug)
Details
(Keywords: assertion, testcase)
Attachments
(3 files, 1 external file)
|
741 bytes,
patch
|
Details | Diff | Splinter Review | |
|
5.11 KB,
text/plain
|
Details | |
|
254 bytes,
text/plain
|
Details |
WarpBuilder::build() finishes every block left in pendingInnerLoopResumes_ by allocating an MBail and an MUnreachable with the infallible TempAllocator, but the loop never calls alloc().ensureBallast(). The number of entries is controlled by the script (one per yield-containing loop that Warp did not build, for example loops inside a catch block), so with enough entries the infallible allocations use up the 16 KiB ballast and LifoAlloc::newChunkWithCapacity is entered outside a fallible scope, tripping the MOZ_ASSERT(fallibleScope_) ballast invariant.
A script author can abort a debug build of SpiderMonkey on an Ion helper thread by running a hot generator function whose catch block contains several hundred loops that each yield. The testcase uses only standard ECMAScript and needs no shell flags. The assertion is compiled only under DEBUG or JS_OOM_BREAKPOINT; the same testcase ran to completion with no sanitizer report in the non-debug ASan shell, where the infallible path allocates a new chunk and, if that allocation fails, takes a controlled OOM crash. No memory corruption was demonstrated.
Build Info
- Branch: main
- Revision: bf9757d91e2e53b8ab836f2a08bf4c0275fdcd23
- Timestamp: 2026-10-02T21:18:24+00:00
Affected Code
File: js/src/jit/WarpBuilder.cpp, line 316-326
// Finish inner-loop blocks build_LoopHead never filled in, because the loop
// is unreachable or only reachable through code Warp doesn't compile, e.g.
// a catch-block. The latter is reachable, so bail out to resume in Baseline
// instead. If this happens frequently, we disable Ion for the script and
// the generator will run in Baseline instead.
for (auto iter = pendingInnerLoopResumes_.iter(); !iter.done(); iter.next()) {
MBasicBlock* block = iter.get().value();
MOZ_ASSERT(!block->hasLastIns());
block->add(MBail::New(alloc(), BailoutKind::UncompiledGeneratorResume));
block->end(MUnreachable::New(alloc()));
}
File: js/src/jit/WarpBuilder.cpp, line 508-521
} else {
// We have a loop that contains an AfterYield. Create a new block to jump
// to the loop header. See buildLoopResumeMerge.
if (!startNewBlock(from, loc)) {
return false;
}
from->lastIns()->initSuccessor(successorIndex, current);
PendingInnerLoopResumesMap::AddPtr p =
pendingInnerLoopResumes_.lookupForAdd(entry.innerLoopHeadOffset());
MOZ_ASSERT(!p, "each loop is targeted by a single dispatch entry");
if (!pendingInnerLoopResumes_.add(p, entry.innerLoopHeadOffset(),
current)) {
return false;
}
}
File: js/src/jit/JitAllocPolicy.h, line 37-43, 69-72
explicit TempAllocator(LifoAlloc* lifoAlloc) : lifoScope_(lifoAlloc) {
lifoAlloc->setAsInfallibleByDefault();
}
void* allocateInfallible(size_t bytes) {
return lifoScope_.alloc().allocInfallible(bytes);
}
...
[[nodiscard]] bool ensureBallast() {
JS_OOM_POSSIBLY_FAIL_BOOL();
return lifoScope_.alloc().ensureUnusedApproximate(BallastSize);
}
File: js/src/ds/LifoAlloc.cpp, line 161-164
LifoAlloc::UniqueBumpChunk LifoAlloc::newChunkWithCapacity(size_t n,
bool oversize) {
MOZ_ASSERT(fallibleScope_,
"[OOM] Cannot allocate a new chunk in an infallible scope.");
File: js/src/ds/LifoAlloc.h, line 889-897
MOZ_ALWAYS_INLINE
void* allocInfallible(size_t n) {
AutoEnterOOMUnsafeRegion oomUnsafe;
if (void* result = allocImpl(n)) {
return result;
}
oomUnsafe.crash("LifoAlloc::allocInfallible");
return nullptr;
}
File: js/src/jit/Ion.cpp, line 2829-2830
/* static */ const size_t TempAllocator::BallastSize = 16 * 1024;
/* static */ const size_t TempAllocator::PreferredLifoChunkSize = 32 * 1024;
The cleanup loop in WarpBuilder::build() performs two infallible TempAllocator allocations per map entry without replenishing ballast. Other script-sized loops in the same file (for example WarpBuilder.cpp:697-700 and 2768-2771) call alloc().ensureBallast() on each iteration. Entries are added in linkDispatchEntry, one per inner loop that contains an AfterYield, and are removed only when build_LoopHead reaches the loop via buildLoopResumeMerge/takePendingInnerLoopResume; loops in code Warp does not compile, such as a catch block, stay in the map until the cleanup loop. TempObject::operator new routes to LifoAlloc::allocInfallible, and the LifoAlloc is marked infallible-by-default by the TempAllocator constructor, so needing a new chunk there fails the debug assertion.
Exploit Chain
- Script defines a generator function whose catch block contains 900
for (;;) { yield i; break; }loops, so each loop head gets a resume dispatch entry and a block in pendingInnerLoopResumes_ (WarpBuilder::linkDispatchEntry). - The generator is iterated to completion 300000 times without ever throwing, making it hot enough for an off-thread Ion/Warp compile.
- Warp does not build the catch block, so build_LoopHead never takes those blocks out of pendingInnerLoopResumes_.
- The cleanup loop in WarpBuilder::build() allocates an MBail and an MUnreachable infallibly for each remaining entry with no ensureBallast() call.
- The current LifoAlloc chunk and ballast are used up, allocInfallible falls into allocImplColdPath -> getOrCreateChunk -> newChunkWithCapacity while fallibleScope_ is false, and MOZ_ASSERT(fallibleScope_) fails on the helper thread, aborting the debug shell.
Steps to Reproduce
- Build the SpiderMonkey debug shell from an unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug (objdir /firefox/obj-js-debug).
- Run testcase.js with /firefox/obj-js-debug/dist/bin/js --fuzzing-safe testcase.js (js_shell_evaluator; no additional flags, prefs or patches).
- Observe
Assertion failure: fallibleScope_ ([OOM] Cannot allocate a new chunk in an infallible scope.), at js/src/ds/LifoAlloc.cpp:164with js::jit::WarpBuilder::build() (WarpBuilder.cpp:325) on the stack of an Ion helper thread. - Control runs performed during verification: the same testcase with 20 loops instead of 900 exits 0 in the debug shell; the unmodified testcase exits 0 with no sanitizer report in the non-debug ASan shell (/firefox/obj-firefox-asan/dist/bin/js).
Security Impact
- Severity: Low
- Attacker capability: Trigger a debug-only assertion failure (process abort) in the Ion compilation helper thread from plain JavaScript. In the non-debug ASan shell the same testcase completed without any report; on that path allocInfallible obtains a new chunk and, if that fails, takes a controlled OOM crash (js/src/ds/LifoAlloc.h:890-897). No out-of-bounds access, use-after-free or other memory corruption was demonstrated.
- Preconditions: Assertion is only compiled in DEBUG or JS_OOM_BREAKPOINT builds. The attacker must be able to run a script that defines and repeatedly runs a generator with several hundred yield-containing loops in code Warp does not compile (a catch block), so that it reaches Ion compilation.
ASAN Report
[1433] Assertion failure: fallibleScope_ ([OOM] Cannot allocate a new chunk in an infallible scope.), at js/src/ds/LifoAlloc.cpp:164
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==1433==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5594134396d9 bp 0x7ed184bfd520 sp 0x7ed184bfd500 T1435)
==1433==The signal is caused by a READ memory access.
==1433==Hint: address points to the zero page.
#0 0x5594134396d9 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x5594134396d9 in js::LifoAlloc::newChunkWithCapacity(unsigned long, bool) /firefox/obj-js-debug/js/src/ds/./../../../../js/src/ds/LifoAlloc.cpp:163:3
#2 0x55941343981e in js::LifoAlloc::getOrCreateChunk(unsigned long) /firefox/obj-js-debug/js/src/ds/./../../../../js/src/ds/LifoAlloc.cpp:219:30
#3 0x559413439a1a in js::LifoAlloc::allocImplColdPath(unsigned long) /firefox/obj-js-debug/js/src/ds/./../../../../js/src/ds/LifoAlloc.cpp:229:30
#4 0x559413b4f197 in js::LifoAlloc::allocImpl(unsigned long) /firefox/js/src/ds/LifoAlloc.h:793:12
#5 0x559413b4f197 in js::LifoAlloc::allocInfallible(unsigned long) /firefox/js/src/ds/LifoAlloc.h:892:24
#6 0x559413d6222e in js::jit::TempAllocator::allocateInfallible(unsigned long) /firefox/js/src/jit/JitAllocPolicy.h:42:31
#7 0x559413d6222e in js::jit::TempObject::operator new(unsigned long, js::jit::TempAllocator&) /firefox/js/src/jit/JitAllocPolicy.h:133:18
#8 0x559413d6222e in js::jit::MInstruction::operator new(unsigned long, js::jit::TempAllocator&) /firefox/js/src/jit/MIR.h:1071:12
#9 0x559413d6222e in js::jit::MUnreachable* js::jit::MUnreachable::New<>(js::jit::TempAllocator&) /firefox/js/src/jit/MIR.h:2809:3
#10 0x559413d6222e in js::jit::WarpBuilder::build() /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/WarpBuilder.cpp:325:16
#11 0x5594143044f3 in js::jit::CompileBackEnd(js::jit::MIRGenerator*, js::jit::WarpSnapshot*) /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/Ion.cpp:1760:18
#12 0x559414330e97 in js::jit::IonCompileTask::runTask() /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/IonCompileTask.cpp:50:24
#13 0x559414330e97 in js::jit::IonCompileTask::runHelperThreadTask(js::AutoLockHelperThreadState&) /firefox/obj-js-debug/js/src/jit/./../../../../js/src/jit/IonCompileTask.cpp:28:5
#14 0x55941483c8aa in js::GlobalHelperThreadState::runTaskLocked(JS::HelperThreadTask*, js::AutoLockHelperThreadState&) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/HelperThreads.cpp:666:11
#15 0x5594148593d7 in js::GlobalHelperThreadState::runOneTask(JS::HelperThreadTask*, js::AutoLockHelperThreadState&) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/HelperThreads.cpp:622:3
#16 0x5594148593d7 in js::HelperThread::threadLoop(js::InternalThreadPool*) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/InternalThreadPool.cpp:324:25
#17 0x55941485913b in js::HelperThread::ThreadMain(js::InternalThreadPool*, js::HelperThread*) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/InternalThreadPool.cpp:251:11
#18 0x559414866696 in void js::detail::ThreadTrampoline<void (&)(js::InternalThreadPool*, js::HelperThread*), js::InternalThreadPool*&, js::HelperThread*>::callMain<0ul, 1ul>(std::integer_sequence<unsigned long, 0ul, 1ul>) /firefox/js/src/threading/Thread.h:223:5
#19 0x559414866696 in js::detail::ThreadTrampoline<void (&)(js::InternalThreadPool*, js::HelperThread*), js::InternalThreadPool*&, js::HelperThread*>::Start(void*) /firefox/js/src/threading/Thread.h:212:11
#20 0x7ed1857fcb83 (/lib/x86_64-linux-gnu/libc.so.6+0x9cb83) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#21 0x7ed185889ecb (/lib/x86_64-linux-gnu/libc.so.6+0x129ecb) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==1433==ABORTING
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
| Reporter | ||
Comment 4•1 month ago
|
||
This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.
Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.
Updated•1 month ago
|
Updated•1 month ago
|
| Assignee | ||
Comment 5•1 month ago
|
||
Updated•1 month ago
|
Updated•1 month ago
|
Description
•