Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/parser/src/grammar/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!['(']);
Expand Down
18 changes: 18 additions & 0 deletions crates/parser/src/grammar/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,24 @@ fn expr_bp(
m: Option<Marker>,
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<Marker>,
r: Restrictions,
bp: u8,
) -> Option<(CompletedMarker, BlockLike)> {
let m = m.unwrap_or_else(|| {
let m = p.start();
Expand Down
8 changes: 8 additions & 0 deletions crates/parser/src/grammar/expressions/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
14 changes: 14 additions & 0 deletions crates/parser/src/grammar/generic_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ const GENERIC_ARG_RECOVERY_SET: TokenSet = TokenSet::new(&[T![>], T![,]]);
// test generic_arg
// type T = S<i32, dyn T, fn()>;
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),
Expand Down
37 changes: 37 additions & 0 deletions crates/parser/src/grammar/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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![')'],
Expand Down
11 changes: 11 additions & 0 deletions crates/parser/src/grammar/items/use_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions crates/parser/src/grammar/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions crates/parser/src/grammar/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
41 changes: 41 additions & 0 deletions crates/parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,19 @@ pub(crate) struct Parser<'t> {
/// into this vec, keeping `Event` itself a flat 8-byte enum.
errors: Vec<String>,
steps: Cell<u32>,
depth: Cell<u32>,
}

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 {
Expand All @@ -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<Event>, Vec<String>) {
Expand Down
41 changes: 41 additions & 0 deletions crates/parser/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading