Skip to content

Commit 2a57b01

Browse files
committed
Auto merge of rust-lang#14122 - lowr:patch/abort-macro-expansion-on-overflow, r=Veykril
fix: Don't expand macros in the same expansion tree after overflow This patch fixes 2 bugs: - In `Expander::enter_expand_id()` (and in code paths it's called), we never check whether we've reached the recursion limit. Although it hasn't been reported as far as I'm aware, this may cause hangs or stack overflows if some malformed attribute macro is used on associated items. - We keep expansion even when recursion limit is reached. Take the following for example: ```rust macro_rules! foo { () => {{ foo!(); foo!(); }} } fn main() { foo!(); } ``` We keep expanding the first `foo!()` in each expansion and would reach the limit at some point, *after which* we would try expanding the second `foo!()` in each expansion until it hits the limit again. This will (by default) lead to ~2^128 expansions. This is essentially what's happening in rust-lang#14074. Unlike rustc, we don't just stop expanding macros when we fail as long as it produces some tokens so that we can provide completions and other services in incomplete macro calls. This patch provides a method that takes care of recursion depths (`Expander::within_limit()`) and stops macro expansions in the whole macro expansion tree once it detects recursion depth overflow. To be honest, I'm not really satisfied with this fix because it can still be used in unintended ways to bypass overflow checks, and I'm still seeking ways such that misuses are caught by the compiler by leveraging types or something. Fixes rust-lang#14074
2 parents 3812951 + ae7e62c commit 2a57b01

File tree

4 files changed

+114
-49
lines changed

4 files changed

+114
-49
lines changed

crates/hir-def/src/body.rs

+91-49
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use la_arena::{Arena, ArenaMap};
1919
use limit::Limit;
2020
use profile::Count;
2121
use rustc_hash::FxHashMap;
22-
use syntax::{ast, AstPtr, SyntaxNodePtr};
22+
use syntax::{ast, AstPtr, SyntaxNode, SyntaxNodePtr};
2323

2424
use crate::{
2525
attr::Attrs,
@@ -51,7 +51,8 @@ pub struct Expander {
5151
def_map: Arc<DefMap>,
5252
current_file_id: HirFileId,
5353
module: LocalModuleId,
54-
recursion_limit: usize,
54+
/// `recursion_depth == usize::MAX` indicates that the recursion limit has been reached.
55+
recursion_depth: usize,
5556
}
5657

5758
impl CfgExpander {
@@ -84,7 +85,7 @@ impl Expander {
8485
def_map,
8586
current_file_id,
8687
module: module.local_id,
87-
recursion_limit: 0,
88+
recursion_depth: 0,
8889
}
8990
}
9091

@@ -93,47 +94,52 @@ impl Expander {
9394
db: &dyn DefDatabase,
9495
macro_call: ast::MacroCall,
9596
) -> Result<ExpandResult<Option<(Mark, T)>>, UnresolvedMacro> {
96-
if self.recursion_limit(db).check(self.recursion_limit + 1).is_err() {
97-
cov_mark::hit!(your_stack_belongs_to_me);
98-
return Ok(ExpandResult::only_err(ExpandError::Other(
99-
"reached recursion limit during macro expansion".into(),
100-
)));
97+
let mut unresolved_macro_err = None;
98+
99+
let result = self.within_limit(db, |this| {
100+
let macro_call = InFile::new(this.current_file_id, &macro_call);
101+
102+
let resolver =
103+
|path| this.resolve_path_as_macro(db, &path).map(|it| macro_id_to_def_id(db, it));
104+
105+
let mut err = None;
106+
let call_id = match macro_call.as_call_id_with_errors(
107+
db,
108+
this.def_map.krate(),
109+
resolver,
110+
&mut |e| {
111+
err.get_or_insert(e);
112+
},
113+
) {
114+
Ok(call_id) => call_id,
115+
Err(resolve_err) => {
116+
unresolved_macro_err = Some(resolve_err);
117+
return ExpandResult { value: None, err: None };
118+
}
119+
};
120+
ExpandResult { value: call_id.ok(), err }
121+
});
122+
123+
if let Some(err) = unresolved_macro_err {
124+
Err(err)
125+
} else {
126+
Ok(result)
101127
}
102-
103-
let macro_call = InFile::new(self.current_file_id, &macro_call);
104-
105-
let resolver =
106-
|path| self.resolve_path_as_macro(db, &path).map(|it| macro_id_to_def_id(db, it));
107-
108-
let mut err = None;
109-
let call_id =
110-
macro_call.as_call_id_with_errors(db, self.def_map.krate(), resolver, &mut |e| {
111-
err.get_or_insert(e);
112-
})?;
113-
let call_id = match call_id {
114-
Ok(it) => it,
115-
Err(_) => {
116-
return Ok(ExpandResult { value: None, err });
117-
}
118-
};
119-
120-
Ok(self.enter_expand_inner(db, call_id, err))
121128
}
122129

123130
pub fn enter_expand_id<T: ast::AstNode>(
124131
&mut self,
125132
db: &dyn DefDatabase,
126133
call_id: MacroCallId,
127134
) -> ExpandResult<Option<(Mark, T)>> {
128-
self.enter_expand_inner(db, call_id, None)
135+
self.within_limit(db, |_this| ExpandResult::ok(Some(call_id)))
129136
}
130137

131-
fn enter_expand_inner<T: ast::AstNode>(
132-
&mut self,
138+
fn enter_expand_inner(
133139
db: &dyn DefDatabase,
134140
call_id: MacroCallId,
135141
mut err: Option<ExpandError>,
136-
) -> ExpandResult<Option<(Mark, T)>> {
142+
) -> ExpandResult<Option<(HirFileId, SyntaxNode)>> {
137143
if err.is_none() {
138144
err = db.macro_expand_error(call_id);
139145
}
@@ -154,29 +160,21 @@ impl Expander {
154160
}
155161
};
156162

157-
let node = match T::cast(raw_node) {
158-
Some(it) => it,
159-
None => {
160-
// This can happen without being an error, so only forward previous errors.
161-
return ExpandResult { value: None, err };
162-
}
163-
};
164-
165-
tracing::debug!("macro expansion {:#?}", node.syntax());
166-
167-
self.recursion_limit += 1;
168-
let mark =
169-
Mark { file_id: self.current_file_id, bomb: DropBomb::new("expansion mark dropped") };
170-
self.cfg_expander.hygiene = Hygiene::new(db.upcast(), file_id);
171-
self.current_file_id = file_id;
172-
173-
ExpandResult { value: Some((mark, node)), err }
163+
ExpandResult { value: Some((file_id, raw_node)), err }
174164
}
175165

176166
pub fn exit(&mut self, db: &dyn DefDatabase, mut mark: Mark) {
177167
self.cfg_expander.hygiene = Hygiene::new(db.upcast(), mark.file_id);
178168
self.current_file_id = mark.file_id;
179-
self.recursion_limit -= 1;
169+
if self.recursion_depth == usize::MAX {
170+
// Recursion limit has been reached somewhere in the macro expansion tree. Reset the
171+
// depth only when we get out of the tree.
172+
if !self.current_file_id.is_macro() {
173+
self.recursion_depth = 0;
174+
}
175+
} else {
176+
self.recursion_depth -= 1;
177+
}
180178
mark.bomb.defuse();
181179
}
182180

@@ -215,6 +213,50 @@ impl Expander {
215213
#[cfg(test)]
216214
return Limit::new(std::cmp::min(32, limit));
217215
}
216+
217+
fn within_limit<F, T: ast::AstNode>(
218+
&mut self,
219+
db: &dyn DefDatabase,
220+
op: F,
221+
) -> ExpandResult<Option<(Mark, T)>>
222+
where
223+
F: FnOnce(&mut Self) -> ExpandResult<Option<MacroCallId>>,
224+
{
225+
if self.recursion_depth == usize::MAX {
226+
// Recursion limit has been reached somewhere in the macro expansion tree. We should
227+
// stop expanding other macro calls in this tree, or else this may result in
228+
// exponential number of macro expansions, leading to a hang.
229+
//
230+
// The overflow error should have been reported when it occurred (see the next branch),
231+
// so don't return overflow error here to avoid diagnostics duplication.
232+
cov_mark::hit!(overflow_but_not_me);
233+
return ExpandResult::only_err(ExpandError::RecursionOverflowPosioned);
234+
} else if self.recursion_limit(db).check(self.recursion_depth + 1).is_err() {
235+
self.recursion_depth = usize::MAX;
236+
cov_mark::hit!(your_stack_belongs_to_me);
237+
return ExpandResult::only_err(ExpandError::Other(
238+
"reached recursion limit during macro expansion".into(),
239+
));
240+
}
241+
242+
let ExpandResult { value, err } = op(self);
243+
let Some(call_id) = value else {
244+
return ExpandResult { value: None, err };
245+
};
246+
247+
Self::enter_expand_inner(db, call_id, err).map(|value| {
248+
value.and_then(|(new_file_id, node)| {
249+
let node = T::cast(node)?;
250+
251+
self.recursion_depth += 1;
252+
self.cfg_expander.hygiene = Hygiene::new(db.upcast(), new_file_id);
253+
let old_file_id = std::mem::replace(&mut self.current_file_id, new_file_id);
254+
let mark =
255+
Mark { file_id: old_file_id, bomb: DropBomb::new("expansion mark dropped") };
256+
Some((mark, node))
257+
})
258+
})
259+
}
218260
}
219261

220262
#[derive(Debug)]

crates/hir-def/src/body/lower.rs

+6
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,10 @@ impl ExprCollector<'_> {
624624
krate: *krate,
625625
});
626626
}
627+
Some(ExpandError::RecursionOverflowPosioned) => {
628+
// Recursion limit has been reached in the macro expansion tree, but not in
629+
// this very macro call. Don't add diagnostics to avoid duplication.
630+
}
627631
Some(err) => {
628632
self.source_map.diagnostics.push(BodyDiagnostic::MacroError {
629633
node: InFile::new(outer_file, syntax_ptr),
@@ -636,6 +640,8 @@ impl ExprCollector<'_> {
636640

637641
match res.value {
638642
Some((mark, expansion)) => {
643+
// Keep collecting even with expansion errors so we can provide completions and
644+
// other services in incomplete macro expressions.
639645
self.source_map.expansions.insert(macro_call_ptr, self.expander.current_file_id);
640646
let prev_ast_id_map = mem::replace(
641647
&mut self.ast_id_map,

crates/hir-def/src/body/tests.rs

+13
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ fn main() { n_nuple!(1,2,3); }
6161
);
6262
}
6363

64+
#[test]
65+
fn your_stack_belongs_to_me2() {
66+
cov_mark::check!(overflow_but_not_me);
67+
lower(
68+
r#"
69+
macro_rules! foo {
70+
() => {{ foo!(); foo!(); }}
71+
}
72+
fn main() { foo!(); }
73+
"#,
74+
);
75+
}
76+
6477
#[test]
6578
fn recursion_limit() {
6679
cov_mark::check!(your_stack_belongs_to_me);

crates/hir-expand/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub type ExpandResult<T> = ValueResult<T, ExpandError>;
5555
pub enum ExpandError {
5656
UnresolvedProcMacro(CrateId),
5757
Mbe(mbe::ExpandError),
58+
RecursionOverflowPosioned,
5859
Other(Box<str>),
5960
}
6061

@@ -69,6 +70,9 @@ impl fmt::Display for ExpandError {
6970
match self {
7071
ExpandError::UnresolvedProcMacro(_) => f.write_str("unresolved proc-macro"),
7172
ExpandError::Mbe(it) => it.fmt(f),
73+
ExpandError::RecursionOverflowPosioned => {
74+
f.write_str("overflow expanding the original macro")
75+
}
7276
ExpandError::Other(it) => f.write_str(it),
7377
}
7478
}

0 commit comments

Comments
 (0)