Closed Bug 2027357 Opened 7 months ago Closed 7 months ago

Unbounded Recursion in ExpressionDecompiler::decompilePC → Stack Overflow

Categories

(Core :: JavaScript Engine, defect, P2)

defect

Tracking

()

VERIFIED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox149 --- wontfix
firefox150 --- wontfix
firefox151 + fixed

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::Add where each Add's left operand is tracked in the BytecodeParser's offsetStack as 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:

  1. eval("(0+0+...+0)()") with 50000 additions compiles successfully.
  2. The eval'd code runs in the interpreter (first execution, not JIT-warmed).
  3. Calling the sum (50000)() fails: "is not a function".
  4. js::ReportIsNotFunction → ReportValueError → DecompileValueGenerator → DecompileExpressionFromStack → ExpressionDecompiler::decompilePC(Call_pc, 0).
  5. 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.

Attached file poc.js —
Attached file crash_stack.txt —
Group: core-security → javascript-core-security

Stack overflow isn't security sensitive.

Keywords: sec-high → sec-other
Whiteboard: [prefs-checked]

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.

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)

Whiteboard: [prefs-checked] → [prefs-checked][bugmon:bisected,confirmed]
Summary: **Vulnerability: Unbounded Recursion in ExpressionDecompiler::decompilePC → Stack Overflow** → Unbounded Recursion in ExpressionDecompiler::decompilePC → Stack Overflow
Severity: -- → S4
Priority: -- → P2

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.

Attached file (secure) —
Assignee: nobody → jdemooij
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

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.

Status: RESOLVED → VERIFIED
Keywords: bugmon
QA Whiteboard: [sec][qa-triage-done-c152/b151]
Whiteboard: [prefs-checked][bugmon:bisected,confirmed] → [prefs-checked][bugmon:bisected,confirmed][adv-main151-][adv-esr115.36-][adv-esr140.11-]
Whiteboard: [prefs-checked][bugmon:bisected,confirmed][adv-main151-][adv-esr115.36-][adv-esr140.11-] → [prefs-checked][bugmon:bisected,confirmed][adv-main151-]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: