fix: avoid pathological macro expansions

Today, rust-analyzer (and rustc, and bat, and IntelliJ) fail badly on
some kinds of maliciously constructed code, like a deep sequence of
nested parenthesis.

"Who writes 100k nested parenthesis" you'd ask?

Well, in a language with macros, a run-away macro expansion might do
that (see the added tests)! Such expansion can be broad, rather than
deep, so it bypasses recursion check at the macro-expansion layer, but
triggers deep recursion in parser.

In the ideal world, the parser would just handle deeply nested structs
gracefully. We'll get there some day, but at the moment, let's try to be
simple, and just avoid expanding macros with unbalanced parenthesis in
the first place.

closes #9358
This commit is contained in:
Aleksey Kladov 2021-08-09 16:06:49 +03:00
parent 9aa6be71a5
commit 3e5b155716
6 changed files with 41 additions and 24 deletions

1
Cargo.lock generated
View file

@ -508,6 +508,7 @@ version = "0.0.0"
dependencies = [
"base_db",
"cfg",
"cov-mark",
"either",
"expect-test",
"la-arena",

View file

@ -1,4 +1,5 @@
use super::*;
use crate::nameres::proc_macro::{ProcMacroDef, ProcMacroKind};
#[test]
@ -1021,3 +1022,20 @@ pub mod prelude {
"#]],
)
}
#[test]
fn issue9358_bad_macro_stack_overflow() {
cov_mark::check!(issue9358_bad_macro_stack_overflow);
check(
r#"
macro_rules! m {
($cond:expr) => { m!($cond, stringify!($cond)) };
($cond:expr, $($arg:tt)*) => { $cond };
}
m!(
"#,
expect![[r#"
crate
"#]],
)
}

View file

@ -9,6 +9,7 @@ edition = "2018"
doctest = false
[dependencies]
cov-mark = "2.0.0-pre.1"
log = "0.4.8"
either = "1.5.3"
rustc-hash = "1.0.0"

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use base_db::{salsa, SourceDatabase};
use limit::Limit;
use mbe::{ExpandError, ExpandResult};
use parser::FragmentKind;
use parser::{FragmentKind, T};
use syntax::{
algo::diff,
ast::{self, NameOwner},
@ -273,6 +273,23 @@ fn macro_arg_text(db: &dyn AstDatabase, id: MacroCallId) -> Option<GreenNode> {
let loc = db.lookup_intern_macro(id);
let arg = loc.kind.arg(db)?;
let arg = process_macro_input(db, arg, id);
if matches!(loc.kind, MacroCallKind::FnLike { .. }) {
let first = arg.first_child_or_token().map_or(T![.], |it| it.kind());
let last = arg.last_child_or_token().map_or(T![.], |it| it.kind());
let well_formed_tt =
matches!((first, last), (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']));
if !well_formed_tt {
// Don't expand malformed (unbalanced) macro invocations. This is
// less than ideal, but trying to expand unbalanced macro calls
// sometimes produces pathological, deeply nested code which breaks
// all kinds of things.
//
// Some day, we'll have explicit recursion counters for all
// recursive things, at which point this code might be removed.
cov_mark::hit!(issue9358_bad_macro_stack_overflow);
return None;
}
}
Some(arg.green().into())
}

View file

@ -308,27 +308,7 @@ fn quux(x: i32) {
m!(x$0
}
"#,
expect![[r#"
kw unsafe
kw match
kw while
kw while let
kw loop
kw if
kw if let
kw for
kw true
kw false
kw return
kw self
kw super
kw crate
lc y i32
bt u32
lc x i32
fn quux() fn(i32)
ma m!() macro_rules! m
"#]],
expect![[r#""#]],
);
}

View file

@ -921,13 +921,13 @@ fn preserves_whitespace_within_macro_expansion() {
macro_rules! macro1 {
($a:expr) => {$a}
}
fn f() {macro1!(1 * 2 + 3 + 4}
fn f() {macro1!(1 * 2 + 3 + 4)}
"#,
expect![[r#"
macro_rules! macro1 {
($a:expr) => {$a}
}
fn f() {macro1!(4 - (3 - 1 * 2)}
fn f() {macro1!(4 - (3 - 1 * 2))}
"#]],
)
}