From 7cf54e66a8833e103d1141c2b1c4459b0620666f Mon Sep 17 00:00:00 2001 From: albab-hasan Date: Wed, 8 Jul 2026 15:38:24 +0600 Subject: [PATCH] fix: avoid unbounded recursion in the parser deeply nested input overflows the parser stack and takes the whole process down with it (the 100k nested parens reproducer from https://github.com/rust-lang/rust-analyzer/pull/9358 . track recursion depth on `Parser` and bail out with an `ERROR` node past 128 levels. each guard wraps the deepest function of its grammar family and delegates to an `_inner` fn so no return path can skip `leave_recursion`. counter instead of unwinding since unwinding past an armed `Marker` trips its `DropBomb`. the limit also bounds the syntax tree depth that everything downstream recurses over. fixes https://github.com/rust-lang/rust-analyzer/issues/9824 --- crates/parser/src/grammar/attributes.rs | 18 ++++++++ crates/parser/src/grammar/expressions.rs | 18 ++++++++ crates/parser/src/grammar/expressions/atom.rs | 8 ++++ crates/parser/src/grammar/generic_args.rs | 14 +++++++ crates/parser/src/grammar/items.rs | 37 +++++++++++++++++ crates/parser/src/grammar/items/use_item.rs | 11 +++++ crates/parser/src/grammar/patterns.rs | 9 ++++ crates/parser/src/grammar/types.rs | 9 ++++ crates/parser/src/parser.rs | 41 +++++++++++++++++++ crates/parser/src/tests.rs | 41 +++++++++++++++++++ 10 files changed, 206 insertions(+) diff --git a/crates/parser/src/grammar/attributes.rs b/crates/parser/src/grammar/attributes.rs index 2eeaa25257db..699971b10f63 100644 --- a/crates/parser/src/grammar/attributes.rs +++ b/crates/parser/src/grammar/attributes.rs @@ -60,6 +60,15 @@ fn cfg_attr_meta(p: &mut Parser<'_>, m: Marker) { const CFG_PREDICATE_FIRST_SET: TokenSet = TokenSet::new(&[T![true], T![false], T![ident]]); fn cfg_predicate(p: &mut Parser<'_>) { + if p.enter_recursion() { + p.bail_recursion(); + } else { + cfg_predicate_inner(p); + } + p.leave_recursion(); +} + +fn cfg_predicate_inner(p: &mut Parser<'_>) { let m = p.start(); if p.eat(T![true]) || p.eat(T![false]) { // test cfg_true_false_pred @@ -142,6 +151,15 @@ fn cfg_meta(p: &mut Parser<'_>, m: Marker) { // #![unsafe(simple::path::tt[a b c])] // #![unsafe(simple::path::tt{a b c})] pub(super) fn meta(p: &mut Parser<'_>) { + if p.enter_recursion() { + p.bail_recursion(); + } else { + meta_inner(p); + } + p.leave_recursion(); +} + +fn meta_inner(p: &mut Parser<'_>) { let m = p.start(); if p.eat(T![unsafe]) { p.expect(T!['(']); diff --git a/crates/parser/src/grammar/expressions.rs b/crates/parser/src/grammar/expressions.rs index 3f341c2ab846..4aaadfaf75e8 100644 --- a/crates/parser/src/grammar/expressions.rs +++ b/crates/parser/src/grammar/expressions.rs @@ -246,6 +246,24 @@ fn expr_bp( m: Option, r: Restrictions, bp: u8, +) -> Option<(CompletedMarker, BlockLike)> { + let res = if p.enter_recursion() { + if let Some(m) = m { + m.abandon(p); + } + Some((p.bail_recursion(), BlockLike::NotBlock)) + } else { + expr_bp_inner(p, m, r, bp) + }; + p.leave_recursion(); + res +} + +fn expr_bp_inner( + p: &mut Parser<'_>, + m: Option, + r: Restrictions, + bp: u8, ) -> Option<(CompletedMarker, BlockLike)> { let m = m.unwrap_or_else(|| { let m = p.start(); diff --git a/crates/parser/src/grammar/expressions/atom.rs b/crates/parser/src/grammar/expressions/atom.rs index 7ce1fc2c4895..7c366a8514a5 100644 --- a/crates/parser/src/grammar/expressions/atom.rs +++ b/crates/parser/src/grammar/expressions/atom.rs @@ -643,6 +643,14 @@ fn closure_expr(p: &mut Parser<'_>) -> CompletedMarker { // if { true } { } else { }; // } fn if_expr(p: &mut Parser<'_>) -> CompletedMarker { + // Guarded separately because an `else if` chain recurses through + // `if_expr` alone, bypassing the guard on `expr_bp`. + let cm = if p.enter_recursion() { p.bail_recursion() } else { if_expr_inner(p) }; + p.leave_recursion(); + cm +} + +fn if_expr_inner(p: &mut Parser<'_>) -> CompletedMarker { assert!(p.at(T![if])); let m = p.start(); p.bump(T![if]); diff --git a/crates/parser/src/grammar/generic_args.rs b/crates/parser/src/grammar/generic_args.rs index 8f74acd1ba2a..0c612bec49e7 100644 --- a/crates/parser/src/grammar/generic_args.rs +++ b/crates/parser/src/grammar/generic_args.rs @@ -47,6 +47,20 @@ const GENERIC_ARG_RECOVERY_SET: TokenSet = TokenSet::new(&[T![>], T![,]]); // test generic_arg // type T = S; pub(crate) fn generic_arg(p: &mut Parser<'_>) -> bool { + // Guarded separately because path arguments recurse through + // `opt_path_type_args` back into `generic_arg`, bypassing the guard on + // `type_with_bounds_cond`. + let res = if p.enter_recursion() { + p.bail_recursion(); + true + } else { + generic_arg_inner(p) + }; + p.leave_recursion(); + res +} + +fn generic_arg_inner(p: &mut Parser<'_>) -> bool { match p.current() { LIFETIME_IDENT if !p.nth_at(1, T![+]) => lifetime_arg(p), T!['{'] | T![true] | T![false] | T![-] => const_arg(p), diff --git a/crates/parser/src/grammar/items.rs b/crates/parser/src/grammar/items.rs index 88a43972329b..75ce05eb2a0d 100644 --- a/crates/parser/src/grammar/items.rs +++ b/crates/parser/src/grammar/items.rs @@ -107,6 +107,20 @@ pub(super) fn item_or_macro(p: &mut Parser<'_>, stop_on_r_curly: bool, is_in_ext /// Try to parse an item, completing `m` in case of success. pub(super) fn opt_item(p: &mut Parser<'_>, m: Marker, is_in_extern: bool) -> Result<(), Marker> { + let res = if p.enter_recursion() { + // The caller already started `m`, so complete it as the `ERROR` + // node instead of letting `bail_recursion` start its own. + p.bump_any(); + m.complete(p, ERROR); + Ok(()) + } else { + opt_item_inner(p, m, is_in_extern) + }; + p.leave_recursion(); + res +} + +fn opt_item_inner(p: &mut Parser<'_>, m: Marker, is_in_extern: bool) -> Result<(), Marker> { // test_err pub_expr // fn foo() { pub 92; } let has_visibility = opt_visibility(p, false); @@ -503,6 +517,29 @@ pub(super) fn macro_call_after_excl(p: &mut Parser<'_>) -> BlockLike { } pub(crate) fn token_tree(p: &mut Parser<'_>) { + if p.enter_recursion() { + // Skip to the matching close bracket iteratively: bailing one token + // at a time would leave one `ERROR` node per excess bracket, and + // unlike other productions a token tree has an unambiguous end. + let m = p.start(); + let mut depth = 1u32; + p.bump_any(); + while !p.at(EOF) && depth > 0 { + match p.current() { + T!['{'] | T!['('] | T!['['] => depth += 1, + T!['}'] | T![')'] | T![']'] => depth -= 1, + _ => {} + } + p.bump_any(); + } + m.complete(p, ERROR); + } else { + token_tree_inner(p); + } + p.leave_recursion(); +} + +fn token_tree_inner(p: &mut Parser<'_>) { let closing_paren_kind = match p.current() { T!['{'] => T!['}'], T!['('] => T![')'], diff --git a/crates/parser/src/grammar/items/use_item.rs b/crates/parser/src/grammar/items/use_item.rs index 675a1fd4650f..bafaef6c6440 100644 --- a/crates/parser/src/grammar/items/use_item.rs +++ b/crates/parser/src/grammar/items/use_item.rs @@ -12,6 +12,17 @@ pub(super) fn use_(p: &mut Parser<'_>, m: Marker) { // test use_tree // use outer::tree::{inner::tree}; fn use_tree(p: &mut Parser<'_>, top_level: bool) -> bool { + let res = if p.enter_recursion() { + p.bail_recursion(); + true + } else { + use_tree_inner(p, top_level) + }; + p.leave_recursion(); + res +} + +fn use_tree_inner(p: &mut Parser<'_>, top_level: bool) -> bool { let m = p.start(); match p.current() { // test use_tree_star diff --git a/crates/parser/src/grammar/patterns.rs b/crates/parser/src/grammar/patterns.rs index 29fa11720aca..80d071a20b91 100644 --- a/crates/parser/src/grammar/patterns.rs +++ b/crates/parser/src/grammar/patterns.rs @@ -64,6 +64,15 @@ fn pattern_r(p: &mut Parser<'_>, recovery_set: TokenSet) { } fn pattern_single_r(p: &mut Parser<'_>, recovery_set: TokenSet) { + if p.enter_recursion() { + p.bail_recursion(); + } else { + pattern_single_r_inner(p, recovery_set); + } + p.leave_recursion(); +} + +fn pattern_single_r_inner(p: &mut Parser<'_>, recovery_set: TokenSet) { // test range_pat // fn main() { // match 92 { diff --git a/crates/parser/src/grammar/types.rs b/crates/parser/src/grammar/types.rs index db0185331cdb..75ed8c4660d4 100644 --- a/crates/parser/src/grammar/types.rs +++ b/crates/parser/src/grammar/types.rs @@ -43,6 +43,15 @@ pub(super) fn type_no_bounds(p: &mut Parser<'_>) { } fn type_with_bounds_cond(p: &mut Parser<'_>, allow_bounds: bool) { + if p.enter_recursion() { + p.bail_recursion(); + } else { + type_with_bounds_cond_inner(p, allow_bounds); + } + p.leave_recursion(); +} + +fn type_with_bounds_cond_inner(p: &mut Parser<'_>, allow_bounds: bool) { match p.current() { T!['('] => paren_or_tuple_type(p), T![!] => never_type(p), diff --git a/crates/parser/src/parser.rs b/crates/parser/src/parser.rs index 57c501eda224..e05ff12c22d3 100644 --- a/crates/parser/src/parser.rs +++ b/crates/parser/src/parser.rs @@ -37,10 +37,19 @@ pub(crate) struct Parser<'t> { /// into this vec, keeping `Event` itself a flat 8-byte enum. errors: Vec, steps: Cell, + depth: Cell, } const PARSER_STEP_LIMIT: usize = if cfg!(debug_assertions) { 150_000 } else { 15_000_000 }; +// Pathologically nested input must not overflow the stack (#9824). The limit +// also bounds the depth of the produced syntax tree, which everything +// downstream recurses over, so bailing out here shields the rest of the +// pipeline as well. A counter is preferred over unwinding (floated in the +// issue) because unwinding past an armed `Marker` trips its `DropBomb`, and +// over growing the stack because a deterministic limit is directly testable. +const PARSER_RECURSION_LIMIT: u32 = 128; + impl<'t> Parser<'t> { pub(super) fn new(inp: &'t Input) -> Parser<'t> { Parser { @@ -49,7 +58,39 @@ impl<'t> Parser<'t> { events: Vec::with_capacity(2 * inp.len()), errors: Vec::new(), steps: Cell::new(0), + depth: Cell::new(0), + } + } + + /// Enters one level of grammar recursion, returning `true` if the depth + /// budget is exhausted — the caller must then emit an `ERROR` node + /// (typically via [`Parser::bail_recursion`]) instead of recursing. + /// + /// Guard only the deepest function of each mutually recursive grammar + /// family — guarding every production would count one level of input + /// nesting several times, shrinking the effective budget. Guard from a + /// thin wrapper delegating to an `_inner` function, so that no return + /// path can skip the paired [`Parser::leave_recursion`]. + pub(crate) fn enter_recursion(&mut self) -> bool { + let depth = self.depth.get() + 1; + self.depth.set(depth); + let over = depth > PARSER_RECURSION_LIMIT; + if over { + self.error("parser recursion limit reached"); } + over + } + + pub(crate) fn leave_recursion(&self) { + self.depth.set(self.depth.get() - 1); + } + + /// Consume one token under an `ERROR` node so the caller makes progress; + /// [`Parser::enter_recursion`] has already emitted the error message. + pub(crate) fn bail_recursion(&mut self) -> CompletedMarker { + let m = self.start(); + self.bump_any(); + m.complete(self, ERROR) } pub(crate) fn finish(self) -> (Vec, Vec) { diff --git a/crates/parser/src/tests.rs b/crates/parser/src/tests.rs index cec50aa54b7f..b0ef798e2a7f 100644 --- a/crates/parser/src/tests.rs +++ b/crates/parser/src/tests.rs @@ -194,3 +194,44 @@ fn run_and_expect_errors_with_edition(path: &str, edition: Edition) { p.set_extension("rast"); expect_file![p].assert_eq(&actual) } + +/// Pathological nesting must produce a "recursion limit" error instead of +/// overflowing the stack, one case per guarded grammar family (#9824). +/// Inputs are generated rather than committed as fixtures because of their +/// size; the small explicit stack makes an unguarded recursion overflow +/// reliably instead of depending on the host's stack size. +#[test] +fn parser_recursion_limit() { + fn nested(prefix: &str, open: &str, depth: usize, inner: &str, close: &str) -> String { + format!("{prefix}{}{inner}{}", open.repeat(depth), close.repeat(depth)) + } + + let cases = [ + // The original reproducer from #9358. + ("paren_expr", nested("fn f(){", "(", 100_000, "0", ")") + "}"), + ("else_if_chain", nested("fn f(){", "if true{}else ", 1024, "", "") + "{}}"), + ("tuple_pat", nested("fn f(){let ", "(", 1024, "_", ")") + " = x;}"), + ("ref_type", nested("type T=", "&", 1024, "u8", "") + ";"), + ("generic_args", nested("type T=", "Vec<", 1024, "u8", ">") + ";"), + ("use_tree", nested("use a", "::{a", 1024, "", "}") + ";"), + ("unsafe_meta", nested("#[", "unsafe(", 1024, "foo", ")") + "]fn f(){}"), + ("cfg_predicate", nested("#[cfg(", "any(", 1024, "foo", ")") + ")]fn f(){}"), + ("token_tree", nested("m!", "{", 1024, "", "}")), + ("nested_items", nested("", "fn f(){", 1024, "", "}")), + ]; + + std::thread::Builder::new() + .stack_size(2 * 1024 * 1024) + .spawn(move || { + for (name, src) in cases { + let (actual, _) = parse(TopEntryPoint::SourceFile, &src, Edition::CURRENT); + assert!( + actual.contains("parser recursion limit reached"), + "{name}: expected a recursion limit error" + ); + } + }) + .unwrap() + .join() + .unwrap(); +}