fix(lower): guard deeply-nested expression chains against stack overflow (#5259)#5264
Conversation
…low (#5259) Expression lowering (`lower_expr`) recurses once per node, so a left-nested `1+1+…+1`, `o.a.a.…a`, or `a||a||…` chain — shapes bundlers/minifiers emit — recurses thousands of frames deep and overflows the native stack, aborting the process with `fatal runtime error: stack overflow` (SIGABRT, exit 134) and no diagnostic. Add a recursion-depth guard at the `lower_expr` entry point: a per-module counter on `LoweringContext`, incremented on entry and decremented on every exit path. Past `MAX_EXPR_LOWER_DEPTH` (2000) lowering bails via `lower_bail!` with a span-tagged "expression nested too deeply" diagnostic instead of recursing further. The compiler lowers on a 128 MB stack and the heaviest shape (member chains) uses ~16 KB/level, so 2000 caps worst-case lowering at ~32 MB — well under the limit and far above anything real code emits, so only the degenerate inputs that would otherwise crash are rejected. Tests cover all three shapes plus an under-limit chain that must still lower; they run on a 128 MB-stack thread (mirroring `perry-main`) because the default ~2 MB cargo-test stack can't hold the multi-thousand-node parse/lower walk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6a97aba to
4b4b591
Compare
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a recursion-depth guard to expression lowering in the perry HIR compiler. A new ChangesExpression Lowering Depth Guard
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Fixes #5259.
Problem
Expression lowering (
lower_expr) is recursive — one frame per node. A left-nested chain likevar x = 1+1+…+1,var x = o.a.a.…a, orvar x = a||a||…(shapes bundlers/minifiers routinely emit) recurses thousands of frames deep and overflows the native stack, aborting the whole process:No diagnostic is emitted — the process just dies. The crash is in the
check-lowerstage (parsing completes fine).Fix
Add a recursion-depth guard at the
lower_exprentry point, exactly as the issue suggests:expr_lower_depthcounter onLoweringContext, incremented on entry and decremented on every exit path (so a recoverable lowering error elsewhere never leaves it inflated).MAX_EXPR_LOWER_DEPTH(2000), lowering bails vialower_bail!with a span-tagged diagnostic:The compiler runs its collect/lower walk on a 128 MB stack (
perry-main), and the heaviest shape (member chains) uses ~16 KB of stack per level, so a ceiling of 2000 caps worst-case lowering depth at ~32 MB — comfortably under the limit and far above anything real code or a reasonable build emits. The only inputs it now rejects are the degenerate ones that previously crashed.Validation
Verified against all three repro shapes from the issue with the built binary:
1+1+…×20000U006diagnostico.a.a.…a×8000U006diagnostica‖a‖…×20000U006diagnosticNew unit tests (
crates/perry-hir/src/lower/tests.rs) cover all three rejecting shapes plus an under-limit accepting case. They run on a 128 MB-stack thread mirroringperry-main, since the default ~2 MB cargo-test stack can't hold the multi-thousand-node parse/lower walk. Fullperry-hirlib suite: 158 passed.Per maintainer convention, this PR intentionally omits the version bump and CHANGELOG entry.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests