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
23 changes: 23 additions & 0 deletions compiler/rustc_attr_parsing/src/attributes/crate_level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,29 @@ impl<S: Stage> SingleAttributeParser<S> for PatternComplexityLimitParser {
}
}

pub(crate) struct MacroTokenLimitParser;

impl<S: Stage> SingleAttributeParser<S> for MacroTokenLimitParser {
const PATH: &[Symbol] = &[sym::macro_token_limit];
const ON_DUPLICATE: OnDuplicate<S> = 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<AttributeKind> {
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<S: Stage> NoArgsAttributeParser<S> for NoCoreParser {
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_attr_parsing/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ attribute_parsers!(
Single<LinkSectionParser>,
Single<LinkageParser>,
Single<MacroExportParser>,
Single<MacroTokenLimitParser>,
Single<MoveSizeLimitParser>,
Single<MustNotSuspendParser>,
Single<MustUseParser>,
Expand Down
15 changes: 15 additions & 0 deletions compiler/rustc_expand/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_expand/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions compiler/rustc_expand/src/mbe/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,22 @@ fn expand_macro<'cx, 'a: 'cx>(
) -> Box<dyn MacResult + 'cx> {
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);
Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions compiler/rustc_hir/src/attrs/data_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_hir/src/attrs/encode_cross_crate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl AttributeKind {
LoopMatch(..) => No,
MacroEscape(..) => No,
MacroExport { .. } => Yes,
MacroTokenLimit { .. } => No,
MacroUse { .. } => No,
Marker(..) => No,
MayDangle(..) => No,
Expand Down
10 changes: 10 additions & 0 deletions compiler/rustc_interface/src/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
16 changes: 16 additions & 0 deletions compiler/rustc_interface/src/passes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions compiler/rustc_passes/src/check_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
| AttributeKind::LinkSection { .. }
| AttributeKind::Linkage(..)
| AttributeKind::MacroEscape( .. )
| AttributeKind::MacroTokenLimit { .. }
| AttributeKind::MacroUse { .. }
| AttributeKind::Marker(..)
| AttributeKind::MoveSizeLimit { .. }
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions tests/ui/macros/exponential-token-growth-issue-95698.rs
Original file line number Diff line number Diff line change
@@ -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() {}
20 changes: 20 additions & 0 deletions tests/ui/macros/exponential-token-growth-issue-95698.stderr
Original file line number Diff line number Diff line change
@@ -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

Loading