Skip to content

Cap recursion depth when expanding macros recursively#22730

Open
momomuchu wants to merge 1 commit into
rust-lang:masterfrom
momomuchu:fix/expand-macro-recursion-guard-21824
Open

Cap recursion depth when expanding macros recursively#22730
momomuchu wants to merge 1 commit into
rust-lang:masterfrom
momomuchu:fix/expand-macro-recursion-guard-21824

Conversation

@momomuchu

@momomuchu momomuchu commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #21824 (the crash half).

Opening a file that invokes higher-kinded-types' hkt::ForLt! macro with an elided lifetime, then running "Expand macro recursively" on that call site, can abort rust-analyzer with a native stack overflow (thread 'WorkerN' has overflowed its stack). Reproduced locally against the exact repro in the issue.

Root cause: expand/expand_macro_recur in crates/ide/src/expand_macro.rs (the code behind "Expand macro recursively") re-expand every nested macro-call descendant of the already-expanded AST with no recursion-depth guard at all. It is a plausible design for ordinary nested macro_rules! expansions, which bottom out in a few levels, but a tt-muncher macro like ForLt! can expand into a chain deep enough to overflow the native call stack before the AST stops containing macro calls.

The fix threads a depth: u32 counter through the expand/expand_macro_recur mutual recursion and stops at 128, which is rustc's own default recursion_limit. That means a macro the compiler itself accepts is never truncated, only expansions the compiler would already reject on recursion grounds get a truncated result with a note instead of a crash. Added a deterministic regression test (macro_expand_recursion_limit_is_bounded) using a local macro_rules! fixture nested well past the limit, so the guard is covered without depending on the external crate or on Salsa scheduling. Full cargo test -p ide --lib expand_macro suite passes (27/27), including all pre-existing nesting tests unmodified.

One thing worth flagging while I have your attention: digging into this issue, I found it is actually two distinct bugs sharing the same repro, not one. There is this crash (unbounded recursion in the display walk above), and separately a name-resolution/Salsa deadlock where collect_defs's prelude seeding calls back into the same tracked query it is currently running inside of, which parks a worker thread forever instead of returning. That hang is a different subsystem (hir-def's collector, not ide::expand_macro) and needs someone who knows the Salsa scheduler better than a first pass gives; this PR only fixes the crash. Happy to comment on the issue with the hang details separately if useful, or take a swing at it in a follow-up.

Also open to a small refinement if you would prefer it: 128 matches the default recursion_limit, but a crate that raises its own recursion_limit could still hit the truncation on an expansion it legitimately allows. I can switch the guard to read the crate's actual recursion_limit (via DefMap) instead of the hardcoded constant, if you would rather have that than a fixed default. Left it as a constant for this first pass since it is simpler and covers the reported case exactly.

Small disclosure per the AI policy: I used AI tooling to help investigate the crash and draft the patch and this description. I reviewed the change, understand why it works, and I am the one submitting and standing behind it.

The "Expand macro recursively" walk in expand_macro re-expands every
nested macro-call descendant with no depth guard, so a deep enough
expansion overflows the native stack and aborts the process. Thread a
depth counter through expand/expand_macro_recur and stop at 128 (the
default recursion_limit), returning a truncated expansion with a note
instead of recursing without bound.

Fixes the crash half of rust-lang#21824 (the "Expand macro recursively" abort on
the higher-kinded-types ForLt! call site). The separate name-resolution
hang in the same report is not addressed here.

Investigation and drafting were assisted by AI tooling; I reviewed,
understand, and own the change.
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 7, 2026
@rustbot

rustbot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Warning ⚠️

  • There are uncanonicalized issue links (such as #123) in the commit messages of the following commits.
    Please add the organization and repository before the issue number (like so rust-lang/rust#123) to avoid issues with subtree.

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to fetch the recursion limit from the def map, yes.

View changes since this review

// truncates with a note instead of overflowing the stack.
#[test]
fn macro_expand_recursion_limit_is_bounded() {
let depth = (super::MACRO_EXPANSION_RECURSION_LIMIT + 12) as usize;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need more than a macro that calls itself with no base condition.

@ChayimFriedman2

Copy link
Copy Markdown
Contributor

I'm interested to hear details about the hang issue. If it is indeed inside name resolution, I find it unlikely that Salsa is involved here, since def map is one query.

@momomuchu

Copy link
Copy Markdown
Author

I didn't attach a debugger to the hang, so the collect_defs prelude-seeding theory in the PR description is a code-reading guess, not a confirmed trace. I never captured a stack dump proving it's stuck inside Salsa, so you may well be right that it isn't. What I actually verified is the stack-overflow crash from "Expand macro recursively" on the ForLt! call site in #21824, which is all this PR fixes. I can try to capture a proper trace of the hang and post it on #21824 if that helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rust-analyzer thread infinite loops when opening file (most of the time)

3 participants