Large function with a trailing empty switch crashes debug SpiderMonkey
Categories
(Core :: JavaScript Engine, defect, P3)
Tracking
()
People
(Reporter: bugmon, Assigned: iain)
References
(Blocks 1 open bug)
Details
(Keywords: assertion, testcase)
Attachments
(3 files, 1 external file)
|
1.30 KB,
patch
|
Details | Diff | Splinter Review | |
|
6.40 KB,
text/plain
|
Details | |
|
469 bytes,
text/plain
|
Details |
BytecodeEmitter::allocateTableSwitchResumeIndexRange assigns firstResumeIndex from numCaseOffsets(), but checks the resume-index limit only while appending cases (js/src/frontend/BytecodeEmitter.cpp:2177). After 16,777,216 entries, an empty switch receives 0x1000000, and SwitchEmitter::emitEnd passes it to SET_RESUMEINDEX, violating the 24-bit SET_UINT24 assertion (js/src/frontend/SwitchEmitter.cpp:386; js/src/vm/BytecodeUtil.h:89).
A JavaScript author can crash a debug SpiderMonkey shell by compiling a sufficiently large function ending in an empty switch. The unchanged --fuzzing-safe debug shell reproduces the assertion, while the optimized ASAN shell completes the same testcase. Release-build memory corruption is not demonstrated.
This finding was discovered during variant analysis seeded by bug 1827359.
Build Info
- Branch: main
- Revision: c32abda0190531351e22da36334f18f9e994d474
- Timestamp: 2026-09-26T04:22:48+00:00
Affected Code
File: js/src/frontend/BytecodeEmitter.cpp, line 2177
*firstResumeIndex = list.numCaseOffsets();
if (!list.appendTableSwitch(switchOffset)) {
return false;
}
for (BytecodeOffset offset : caseOffsets) {
if (!checkResumeIndexLimit()) {
return false;
}
if (!list.appendCaseOffset(offset)) {
return false;
}
}
File: js/src/frontend/SwitchEmitter.cpp, line 382
if (!bce_->allocateTableSwitchResumeIndexRange(offsets, top_,
&firstResumeIndex)) {
return false;
}
SET_RESUMEINDEX(pc, firstResumeIndex);
File: js/src/vm/BytecodeUtil.h, line 88
static MOZ_ALWAYS_INLINE void SET_UINT24(jsbytecode* pc, uint32_t i) {
MOZ_ASSERT(i < (1 << 24));
MaxResumeIndex is the 24-bit mask (js/src/frontend/BytecodeEmitter.cpp:2138). The limit check runs before insertion, allowing a table to contain 2^24 entries (js/src/frontend/BytecodeEmitter.cpp:2150). An empty caseOffsets span bypasses that check and returns the one-past-end count as an index. Removing the final empty switch or retaining it with one fewer preceding entry makes the debug testcase succeed.
Exploit Chain
- Generate 512 switches spanning integer cases 0 through 32766, followed by a switch spanning 0 through 511; their jump tables require 16,777,216 resume entries.
- Append an empty switch and pass the generated body to the ordinary Function constructor.
- Compilation passes the unrepresentable resume index to SET_UINT24 and aborts the debug engine.
Steps to Reproduce
- Use unmodified Firefox revision c32abda0190531351e22da36334f18f9e994d474. Run build_firefox with firefox_dir=/firefox and mozconfig_path=/app/mozconfigs/mozconfig.js.linux.debug.
- Run js_shell_evaluator with the contents of /app/output/verification-tmpfru_7e7y/resume-index-overflow.js, js_binary=/firefox/obj-js-debug/dist/bin/js, flags=[], and timeout=240.
- Observe crashed=true, timed_out=false, and exit_code=1. The complete independent stack is saved at /app/output/verification-tmpfru_7e7y/debug-crash.stderr.txt:1. Build and control results are recorded in /app/output/verification-tmpfru_7e7y/verification.json:1.
Security Impact
- Severity: Low
- Attacker capability: Abort an assertions-enabled SpiderMonkey process during JavaScript compilation. Release-build memory corruption or code execution was not demonstrated.
- Preconditions: Ability to supply JavaScript to the Function constructor and sufficient resources to compile 16,777,216 table-switch resume entries. The demonstrated abort requires the debug build.
ASAN Report
[13572] Assertion failure: i < (1 << 24), at /firefox/js/src/vm/BytecodeUtil.h:89
#0 0x5605929ea3ce in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x5605929ea3ce in SET_UINT24(unsigned char*, unsigned int) /firefox/js/src/vm/BytecodeUtil.h:89:3
#2 0x5605929ea3ce in SET_RESUMEINDEX(unsigned char*, unsigned int) /firefox/js/src/vm/BytecodeUtil.h:195:3
#3 0x5605929ea3ce in js::frontend::SwitchEmitter::emitEnd() /firefox/obj-js-debug/js/src/frontend/./../../../../js/src/frontend/SwitchEmitter.cpp:386:5
#4 0x5605928c810f in js::frontend::BytecodeEmitter::emitSwitch(js::frontend::SwitchStatement*) /firefox/obj-js-debug/js/src/frontend/./../../../../js/src/frontend/BytecodeEmitter.cpp:2131:11
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
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
|
Updated•1 month ago
|
| Assignee | ||
Comment 6•1 month ago
|
||
Updated•1 month ago
|
Comment 7•1 month ago
|
||
:iain, please review the release status flags for beta, release, and ESR and update each one as appropriate.
For each version, indicate whether it is affected, unaffected, disabled, or wontfix if it is affected but does not need to be fixed on that branch.
| Assignee | ||
Comment 8•1 month ago
|
||
As mentioned in this comment on the dupe, I'm pretty sure this doesn't affect non-release builds at all. Opening up.
Updated•1 month ago
|
Comment 10•1 month ago
|
||
| bugherder | ||
Updated•1 month ago
|
Description
•