Unbounded Recursion in ExpressionDecompiler::decompilePC → Stack Overflow
Categories
(Core :: JavaScript Engine, defect, P2)
Tracking
()
People
(Reporter: bugmon, Assigned: jandem)
References
(Blocks 2 open bugs)
Details
(4 keywords, Whiteboard: [prefs-checked][bugmon:bisected,confirmed][adv-main151-])
Attachments
(3 files, 1 external file)
Vulnerability: Unbounded Recursion in ExpressionDecompiler::decompilePC → Stack Overflow
Location: /firefox/js/src/vm/BytecodeUtil.cpp lines 1661, 1685, 2200
Root Cause:
ExpressionDecompiler::decompilePC(jsbytecode* pc, uint8_t defIndex) at line 1664 recursively decompiles operand expressions for error messages. For binary operators (line 1685), it calls decompilePCForStackOperand(pc, -2) to decompile the left operand, which recursively invokes decompilePC on the previous bytecode's result.
Critical missing check: Unlike sibling recursive functions such as js::ValueToSource (ToSource.cpp:117) which begin with AutoCheckRecursionLimit, decompilePC has NO recursion depth guard.
Parser/Decompiler Asymmetry:
- The JS parser handles left-associative chains
0+0+0+...iteratively using a loop, so expressions with 50000+ additions compile successfully without parser stack overflow. - The resulting bytecode is a linear chain of
JSOp::Addwhere each Add's left operand is tracked in the BytecodeParser'soffsetStackas the result of the previous Add. - The decompiler walks this chain recursively:
decompilePC(Add_N)→decompilePCForStackOperand(-2)→decompilePC(Add_{N-1})→ ... →decompilePC(Add_0).
Trigger Path:
eval("(0+0+...+0)()")with 50000 additions compiles successfully.- The eval'd code runs in the interpreter (first execution, not JIT-warmed).
- Calling the sum
(50000)()fails: "is not a function". js::ReportIsNotFunction→ReportValueError→DecompileValueGenerator→DecompileExpressionFromStack→ExpressionDecompiler::decompilePC(Call_pc, 0).- The Call's callee decompilation recurses through all 50000 Add instructions → native stack overflow.
Crash Confirmation:
ASAN reports stack-overflow with 734+ stack frames showing the 3-frame recursion cycle:
decompilePC(jsbytecode*, uint8_t)[BytecodeUtil.cpp:1685]decompilePC(OffsetAndDefIndex const&)[BytecodeUtil.cpp:2200]decompilePCForStackOperand(jsbytecode*, int)[BytecodeUtil.cpp:1661]
Reproduction: No special flags needed beyond --fuzzing-safe. Reliably crashes with 50000 additions.
Note on verifier verdict: The independent verifier confirmed the crash reproduces exactly and the technical root-cause analysis is correct. It rejected the finding on the grounds that native stack overflow is DoS-only (guard-page hit, no heap corruption). However, this is the novel, reproducible ASAN-reported crash in BytecodeUtil.cpp triggerable from the JS shell with --fuzzing-safe. The fix would be to add AutoCheckRecursionLimit recursion(cx); if (!recursion.check(cx)) return false; at the entry of decompilePC.
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Comment 4•7 months ago
|
||
Stack overflow isn't security sensitive.
Updated•7 months ago
|
Comment 5•7 months ago
|
||
No prefs required — --fuzzing-safe is a shell CLI flag that only disables dangerous shell builtins and doesn't gate the error-message decompiler. sec-other is correct (guard-page stack overflow = safe crash / DoS, per comment 4).
This is an automated analysis result. If this result is incorrect please add a needinfo and feel free to correct the error.
| Reporter | ||
Comment 6•7 months ago
|
||
Verified bug as reproducible on mozilla-central 20260329205327-086d14f42154.
Unable to bisect testcase (Testcase reproduces on start build!):
Start: 810549a5947f5ae5d8cf1f11c15c841e1a6dc64c (20251231225543)
End: 8e941630524a230f9342db0dad8061dcabcbad12 (20260328200338)
BuildFlags: BuildFlags(asan=True, tsan=False, debug=False, fuzzing=True, coverage=False, valgrind=False, no_opt=False, fuzzilli=False, nyx=False, searchfox=False, afl=False)
Updated•7 months ago
|
Updated•7 months ago
|
Comment 7•7 months ago
|
||
Comment 8•7 months ago
|
||
Guards ExpressionDecompiler::decompilePC with AutoCheckRecursionLimit::checkDontReport and falls back to (intermediate value) on overflow. checkDontReport is used because the decompiler runs inside error reporting and should not clobber the in-flight error with a nested over-recursion. Verified locally: eval("(" + "0+".repeat(50000) + "0)()") previously stack-overflowed in decompilePC, now raises a normal TypeError.
This is the analysis tool's suggested fix. Feel welcome to adopt it as a starting point and evolve it as needed to meet our coding standards.
Updated•7 months ago
|
| Assignee | ||
Comment 9•7 months ago
|
||
Updated•7 months ago
|
Comment 10•7 months ago
|
||
Comment 11•7 months ago
|
||
| Reporter | ||
Comment 12•7 months ago
|
||
Verified bug as fixed on rev mozilla-central 20260416155948-dca1688c7da6.
Removing bugmon keyword as no further action possible. Please review the bug and re-add the keyword for further analysis.
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•2 months ago
|
Description
•