diff --git a/compiler/rustc_attr_parsing/src/attributes/crate_level.rs b/compiler/rustc_attr_parsing/src/attributes/crate_level.rs index 774fd0805a7a8..c5f39ee96ffb4 100644 --- a/compiler/rustc_attr_parsing/src/attributes/crate_level.rs +++ b/compiler/rustc_attr_parsing/src/attributes/crate_level.rs @@ -173,6 +173,29 @@ impl SingleAttributeParser for PatternComplexityLimitParser { } } +pub(crate) struct MacroTokenLimitParser; + +impl SingleAttributeParser for MacroTokenLimitParser { + const PATH: &[Symbol] = &[sym::macro_token_limit]; + const ON_DUPLICATE: OnDuplicate = OnDuplicate::WarnButFutureError; + const TEMPLATE: AttributeTemplate = template!(NameValueStr: "N"); + const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(&[Allow(Target::Crate)]); + + fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser) -> Option { + let ArgParser::NameValue(nv) = args else { + let attr_span = cx.attr_span; + cx.adcx().expected_name_value(attr_span, None); + return None; + }; + + Some(AttributeKind::MacroTokenLimit { + limit: cx.parse_limit_int(nv)?, + attr_span: cx.attr_span, + limit_span: nv.value_span, + }) + } +} + pub(crate) struct NoCoreParser; impl NoArgsAttributeParser for NoCoreParser { diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs index b87a71bcbd92b..5a63366615c93 100644 --- a/compiler/rustc_attr_parsing/src/context.rs +++ b/compiler/rustc_attr_parsing/src/context.rs @@ -199,6 +199,7 @@ attribute_parsers!( Single, Single, Single, + Single, Single, Single, Single, diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs index 6c5732f497f8a..b9f669a3f9513 100644 --- a/compiler/rustc_expand/src/errors.rs +++ b/compiler/rustc_expand/src/errors.rs @@ -183,6 +183,21 @@ pub(crate) struct RecursionLimitReached { pub crate_name: Symbol, } +#[derive(Diagnostic)] +#[diag("macro expansion token limit reached while expanding `{$name}!`")] +#[help("the macro input has {$token_count} tokens, exceeding the limit of {$limit}")] +#[note( + "this is typically caused by a macro that recursively produces exponentially growing output; consider adding `#![macro_token_limit = \"{$suggested_limit}\"]` to your crate if this is intentional" +)] +pub(crate) struct MacroInputTooLarge { + #[primary_span] + pub span: Span, + pub name: Ident, + pub token_count: usize, + pub limit: usize, + pub suggested_limit: usize, +} + #[derive(Diagnostic)] #[diag("removing an expression is not supported in this position")] pub(crate) struct RemoveExprNotSupported { diff --git a/compiler/rustc_expand/src/expand.rs b/compiler/rustc_expand/src/expand.rs index e3a15f193e581..b747810144196 100644 --- a/compiler/rustc_expand/src/expand.rs +++ b/compiler/rustc_expand/src/expand.rs @@ -2618,6 +2618,9 @@ pub struct ExpansionConfig<'feat> { pub crate_name: Symbol, pub features: &'feat Features, pub recursion_limit: Limit, + /// Maximum number of tokens allowed as input to a single `macro_rules!` expansion. + /// Prevents exponential token growth from hanging the compiler. + pub macro_token_limit: Limit, pub trace_mac: bool, /// If false, strip `#[test]` nodes pub should_test: bool, @@ -2634,6 +2637,7 @@ impl ExpansionConfig<'_> { features, // FIXME should this limit be configurable? recursion_limit: Limit::new(1024), + macro_token_limit: Limit::new(1 << 20), trace_mac: false, should_test: false, span_debug: false, diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs index fd5dac3cd9263..1716b04285c28 100644 --- a/compiler/rustc_expand/src/mbe/macro_rules.rs +++ b/compiler/rustc_expand/src/mbe/macro_rules.rs @@ -368,6 +368,22 @@ fn expand_macro<'cx, 'a: 'cx>( ) -> Box { let psess = &cx.sess.psess; + // Guard against exponential token growth from recursive macros (issue #95698). + // A macro that doubles its output on each expansion can produce an astronomical + // number of tokens long before the recursion depth limit is reached. + // Configurable via `#![macro_token_limit = "N"]`. + if !cx.ecfg.macro_token_limit.value_within_limit(arg.len()) { + let limit = cx.ecfg.macro_token_limit.0; + let guar = cx.dcx().emit_err(errors::MacroInputTooLarge { + span: sp, + name, + token_count: arg.len(), + limit, + suggested_limit: limit.saturating_mul(2), + }); + return DummyResult::any(sp, guar); + } + if cx.trace_macros() { let msg = format!("expanding `{}! {{ {} }}`", name, pprust::tts_to_string(&arg)); trace_macros_note(&mut cx.expansions, sp, msg); diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs index acbcba90fbcc0..ec8f8239f94f1 100644 --- a/compiler/rustc_feature/src/builtin_attrs.rs +++ b/compiler/rustc_feature/src/builtin_attrs.rs @@ -687,6 +687,11 @@ pub static BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[ template!(NameValueStr: "N", "https://doc.rust-lang.org/reference/attributes/limits.html#the-type_length_limit-attribute"), FutureWarnFollowing, EncodeCrossCrate::No ), + ungated!( + macro_token_limit, CrateLevel, + template!(NameValueStr: "N"), + FutureWarnFollowing, EncodeCrossCrate::No + ), gated!( move_size_limit, CrateLevel, template!(NameValueStr: "N"), ErrorFollowing, EncodeCrossCrate::No, large_assignments, experimental!(move_size_limit) diff --git a/compiler/rustc_hir/src/attrs/data_structures.rs b/compiler/rustc_hir/src/attrs/data_structures.rs index f18d5a1f190a2..29b958ef2b9cc 100644 --- a/compiler/rustc_hir/src/attrs/data_structures.rs +++ b/compiler/rustc_hir/src/attrs/data_structures.rs @@ -1251,6 +1251,14 @@ pub enum AttributeKind { local_inner_macros: bool, }, + /// Represents `#![macro_token_limit = "N"]` — limits the number of tokens in a single + /// `macro_rules!` expansion input to prevent exponential token growth from hanging the compiler. + MacroTokenLimit { + attr_span: Span, + limit_span: Span, + limit: Limit, + }, + /// Represents `#[macro_use]`. MacroUse { span: Span, diff --git a/compiler/rustc_hir/src/attrs/encode_cross_crate.rs b/compiler/rustc_hir/src/attrs/encode_cross_crate.rs index 6612ebd6135b8..4706e3bb06295 100644 --- a/compiler/rustc_hir/src/attrs/encode_cross_crate.rs +++ b/compiler/rustc_hir/src/attrs/encode_cross_crate.rs @@ -60,6 +60,7 @@ impl AttributeKind { LoopMatch(..) => No, MacroEscape(..) => No, MacroExport { .. } => Yes, + MacroTokenLimit { .. } => No, MacroUse { .. } => No, Marker(..) => No, MayDangle(..) => No, diff --git a/compiler/rustc_interface/src/limits.rs b/compiler/rustc_interface/src/limits.rs index 8ae0743886ce5..57dab9001afe4 100644 --- a/compiler/rustc_interface/src/limits.rs +++ b/compiler/rustc_interface/src/limits.rs @@ -40,3 +40,13 @@ pub(crate) fn get_recursion_limit(attrs: &[Attribute], sess: &Session) -> Limit .map_or(limit_from_crate, |min| min.max(limit_from_crate)), ) } + +/// Default token limit for macro expansion input (2^20 ≈ 1 million tokens). +const DEFAULT_MACRO_TOKEN_LIMIT: usize = 1 << 20; + +// This one is also read prior to macro expansion. +pub(crate) fn get_macro_token_limit(attrs: &[Attribute], _sess: &Session) -> Limit { + let limit_from_crate = find_attr!(attrs, MacroTokenLimit { limit, .. } => limit.0) + .unwrap_or(DEFAULT_MACRO_TOKEN_LIMIT); + Limit::new(limit_from_crate) +} diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs index 43efce545fc28..f6f48216a39d8 100644 --- a/compiler/rustc_interface/src/passes.rs +++ b/compiler/rustc_interface/src/passes.rs @@ -201,10 +201,12 @@ fn configure_and_expand( // Create the config for macro expansion let recursion_limit = get_recursion_limit(pre_configured_attrs, sess); + let macro_token_limit = get_macro_token_limit(pre_configured_attrs, sess); let cfg = rustc_expand::expand::ExpansionConfig { crate_name, features, recursion_limit, + macro_token_limit, trace_mac: sess.opts.unstable_opts.trace_macros, should_test: sess.is_test_crate(), span_debug: sess.opts.unstable_opts.span_debug, @@ -1489,3 +1491,17 @@ fn get_recursion_limit(krate_attrs: &[ast::Attribute], sess: &Session) -> Limit ); crate::limits::get_recursion_limit(attr.as_slice(), sess) } + +fn get_macro_token_limit(krate_attrs: &[ast::Attribute], sess: &Session) -> Limit { + let attr = AttributeParser::parse_limited_should_emit( + sess, + &krate_attrs, + sym::macro_token_limit, + DUMMY_SP, + rustc_ast::node_id::CRATE_NODE_ID, + Target::Crate, + None, + ShouldEmit::EarlyFatal { also_emit_lints: false }, + ); + crate::limits::get_macro_token_limit(attr.as_slice(), sess) +} diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 12b583d8fee15..780cafc5d3f49 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -255,6 +255,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { | AttributeKind::LinkSection { .. } | AttributeKind::Linkage(..) | AttributeKind::MacroEscape( .. ) + | AttributeKind::MacroTokenLimit { .. } | AttributeKind::MacroUse { .. } | AttributeKind::Marker(..) | AttributeKind::MoveSizeLimit { .. } diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index aab4aef43a53c..e8c91e38d26b1 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1199,6 +1199,7 @@ symbols! { macro_metavar_expr, macro_metavar_expr_concat, macro_reexport, + macro_token_limit, macro_use, macro_vis_matcher, macros_in_extern, diff --git a/tests/ui/macros/exponential-token-growth-issue-95698.rs b/tests/ui/macros/exponential-token-growth-issue-95698.rs new file mode 100644 index 0000000000000..e40902bcbb693 --- /dev/null +++ b/tests/ui/macros/exponential-token-growth-issue-95698.rs @@ -0,0 +1,21 @@ +// Regression test for #95698. +// A macro that doubles its token output on each recursive call +// should hit the token limit rather than hanging the compiler. +//@ normalize-stderr: "\d+" -> "N" + +macro_rules! from_cow_impls { + ($( $from: ty ),+ $(,)? ) => { + from_cow_impls!( //~ ERROR macro expansion token limit reached + $($from, std::borrow::Cow::from),+ + ); + }; + + ($( $from: ty, $normalizer: expr ),+ $(,)? ) => {}; +} + +from_cow_impls!( + u8, + u16, +); + +fn main() {} diff --git a/tests/ui/macros/exponential-token-growth-issue-95698.stderr b/tests/ui/macros/exponential-token-growth-issue-95698.stderr new file mode 100644 index 0000000000000..6d0e35f1e4fd5 --- /dev/null +++ b/tests/ui/macros/exponential-token-growth-issue-95698.stderr @@ -0,0 +1,20 @@ +error: macro expansion token limit reached while expanding `from_cow_impls!` + --> $DIR/exponential-token-growth-issue-N.rs:N:N + | +LL | / from_cow_impls!( +LL | | $($from, std::borrow::Cow::from),+ +LL | | ); + | |_________^ +... +LL | / from_cow_impls!( +LL | | uN, +LL | | uN, +LL | | ); + | |_- in this macro invocation + | + = help: the macro input has N tokens, exceeding the limit of N + = note: this is typically caused by a macro that recursively produces exponentially growing output; consider adding `#![macro_token_limit = "N"]` to your crate if this is intentional + = note: this error originates in the macro `from_cow_impls` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to N previous error +