Skip to content

A long elif chain exhausts the C stack in the parser before the compiler's depth limit can reject it #926

Description

@Nitjsefnie

Describe the bug

parse_statement recurses once per elif arm, so C-stack usage grows linearly with the length of an if/elif chain. Past roughly 12,800 arms the process segfaults while parsing.

I want to be straightforward about the severity, because the interesting part isn't the crash: below that threshold the behaviour is already correct. COMPILE_MAX_DEPTH (src/compiler.c:237, 128) rejects a long chain cleanly with expression nesting too deep to compile (limit 128). So no valid program is affected, and a chain long enough to crash is one the compiler would have refused anyway — it just never gets the chance, because the parser exhausts the stack first. What seems worth reporting is the two invariants underneath:

  • PARSE_MAX_DEPTH does not cover this path. Its comment at src/parser.c:636-640 scopes it to nested expressions and nested blocks, and an elif chain is syntactically flat, so nothing on this path ever charges g_parse_depth. The guard isn't defeated — it has nothing to compare.
  • The comment at src/parser.c:1390-1391 doesn't hold here. It reads Nesting depth is preserved by parse_block's own guard, but the elif desugaring manufactures nesting by recursing at src/parser.c:1616 (else_body[0] = parse_statement(p);) after that guard has already unwound. The recursion is non-tail — the result is stored and more work follows — so it cannot be flattened away.

Measured cost is ~656 bytes of C stack per arm at -O2. The exact threshold is therefore ulimit -s-dependent: ulimit -s 65536 turns every crash below back into normal behaviour.

Surfaced while working on #870.

To reproduce

The chain has to be generated. The shape is an ordinary top-level chain, one statement per arm:

x is 0
if x == 0:
    print of 0
elif x == 1:
    print of 1
else:
    print of -1

Generate a long one and run it:

python3 -c '
n = 14000
print("x is 0")
print("if x == 0:")
print("    print of 0")
for i in range(1, n):
    print(f"elif x == {i}:")
    print(f"    print of {i}")
print("else:")
print("    print of -1")
' > deep_elif.eigs

./src/eigenscript deep_elif.eigs ; echo "rc=$?"
./src/eigenscript --lint deep_elif.eigs ; echo "rc=$?"

Expected behavior

A chain this long is refused, not fatal. Either the existing compile-depth error, or a parse-time depth error in the same style as the other bounds in the tree — the point is a diagnostic and a non-zero exit rather than a signal.

Actual behavior

At n = 14000, both invocations die with rc=139 (128 + 11, SIGSEGV) during parsing, with no diagnostic.

The two modes differ below the crash threshold, which is worth knowing when reproducing:

  • ./src/eigenscript deep_elif.eigs at n = 200 and at n = 12000rc=1, expression nesting too deep to compile (limit 128). Correct behaviour.
  • ./src/eigenscript --lint deep_elif.eigs at n = 12000rc=0. Linting doesn't run the compiler, so the depth limit that saves the plain path isn't in play at all.

So if you reproduce with a few hundred arms you'll get the clean error and see nothing wrong — the crash needs roughly 12,800+ at the default stack size.

Environment

  • OS: Debian GNU/Linux 13 (trixie)
  • GCC version: 14.2.0 (Debian 14.2.0-19)
  • EigenScript version: 0.39.0 (built from main at 4e398aa1e47eeb0dc7182478ab4bf3240a7f8476)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions