Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2595,6 +2595,8 @@ mod stylesheet_impl {
err: None,
selector_expansion_multiplier: 1,
selector_expansion_total: 0,
selector_expansion_chain_bytes: 0,
selector_expansion_bytes_total: 0,
};

if self.rules.minify(&mut minify_ctx, false).is_err() {
Expand Down
9 changes: 9 additions & 0 deletions src/css/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ pub enum MinifyErrorKind {
/// Compiling nested rules for the configured browser targets would expand to
/// more than [`crate::css_rules::MAX_SELECTOR_EXPANSION`] selectors.
selector_expansion_limit_exceeded,
/// Compiling nested rules for the configured browser targets would expand
/// selectors into more than
/// [`crate::css_rules::MAX_SELECTOR_EXPANSION_BYTES`] estimated bytes.
selector_expansion_bytes_limit_exceeded,
/// Rule minification failed without recording a more specific diagnostic on
/// `MinifyContext::err`. Defensive fallback — every failing path is expected
/// to record one before returning an error.
Expand All @@ -540,6 +544,11 @@ impl fmt::Display for MinifyErrorKind {
"Nested CSS rules expand to more than {} selectors when compiled for the configured browser targets. Reduce the nesting depth or the number of selectors per rule, or target browsers that support CSS nesting.",
crate::css_rules::MAX_SELECTOR_EXPANSION,
),
Self::selector_expansion_bytes_limit_exceeded => write!(
f,
"Nested CSS rules expand to more than {} bytes of selectors when compiled for the configured browser targets. Reduce the nesting depth, the number or size of selectors per rule, or target browsers that support CSS nesting.",
crate::css_rules::MAX_SELECTOR_EXPANSION_BYTES,
),
Self::unknown => write!(f, "CSS minification failed"),
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/css/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ pub struct Printer<'a> {
/// `serialize::serialize_nesting` so deeply nested rules with multiple
/// `&` references per level cannot expand exponentially.
pub nesting_expansions: u32,
/// Running total of bytes emitted by `&` parent-selector substitutions
/// (accumulated across the whole stylesheet, never reset). Complements
/// `nesting_expansions`: the count bounds how many substitutions happen,
/// this bounds what they emit: each substitution writes the parent
/// selector list, whose size is input-controlled. Bounded in
/// `serialize::serialize_nesting`.
pub nesting_expansion_bytes: usize,
/// Recursion depth of in-progress `serialize_nesting` substitutions; only
/// the outermost one measures its byte span into
/// `nesting_expansion_bytes` (inner substitutions are contained in it).
pub nesting_expansion_meter_depth: u32,
/// Running total of bytes emitted by duplicate vendor-prefix passes. A rule
/// whose selector list carries more than one vendor prefix (e.g. a list
/// mixing `:-webkit-autofill` with an unprefixed pseudo-class, or a single
Expand Down Expand Up @@ -347,6 +358,8 @@ impl<'a> Printer<'a> {
css_module: None,
ctx: None,
nesting_expansions: 0,
nesting_expansion_bytes: 0,
nesting_expansion_meter_depth: 0,
prefix_expansion_bytes: 0,
error_kind: None,
}
Expand Down
25 changes: 25 additions & 0 deletions src/css/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,22 @@ pub struct StyleContext<'a> {
/// instead.
pub const MAX_SELECTOR_EXPANSION: u32 = 65_536;

/// Upper bound on the estimated serialized bytes of the selectors that
/// compiling nested rules away for the configured targets may expand a
/// stylesheet into.
///
/// [`MAX_SELECTOR_EXPANSION`] bounds how many selectors the expansion
/// produces, but each expanded selector repeats its ancestor chain, whose
/// per-level size is input-controlled (long identifiers, multi-argument
/// `:lang()`, raw custom pseudo-class arguments). Count × size lets a few KB
/// of input expand into hundreds of megabytes of cloned rules and output while
/// staying under the count cap, so the expansion is also charged by estimated
/// bytes ([`selector::selector_list_weight`](crate::selectors::selector)).
/// Exceeding this is reported as a `selector_expansion_bytes_limit_exceeded`
/// minify error. The thresholds are ordered so that stylesheets with
/// ordinary-sized selectors keep hitting the count limit first.
pub const MAX_SELECTOR_EXPANSION_BYTES: u64 = 64 << 20;

/// Per-stylesheet minification state threaded through `CssRuleList::minify`
/// and every leaf rule's `minify`.
///
Expand Down Expand Up @@ -1104,4 +1120,13 @@ pub struct MinifyContext<'a, 'bump> {
/// Running total of selectors that compiling nested rules for the targets
/// will expand to, checked against [`MAX_SELECTOR_EXPANSION`].
pub selector_expansion_total: u32,
/// Estimated serialized bytes one expanded selector's ancestor chain
/// contributes: the sum over the enclosing (compiled) style rules of the
/// average selector weight of their lists. `0` at the top level;
/// maintained alongside `selector_expansion_multiplier` in
/// `StyleRule::minify_nested_rules`.
pub selector_expansion_chain_bytes: u32,
/// Running estimate of the serialized bytes the expansion will produce,
/// checked against [`MAX_SELECTOR_EXPANSION_BYTES`].
pub selector_expansion_bytes_total: u64,
}
50 changes: 42 additions & 8 deletions src/css/rules/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,23 +385,47 @@ impl<R> StyleRule<R> {
/// otherwise a few hundred bytes of deeply nested multi-selector rules
/// expand into gigabytes of cloned rules and output. See
/// [`css_rules::MAX_SELECTOR_EXPANSION`](super::MAX_SELECTOR_EXPANSION).
///
/// The expansion is charged twice, in this order: by selector count
/// (bounds how many rules/selectors materialize) and by estimated
/// serialized bytes (bounds their size; per-selector size is
/// input-controlled, so a few fat selectors could otherwise expand into
/// hundreds of megabytes while staying under the count cap). See
/// [`css_rules::MAX_SELECTOR_EXPANSION_BYTES`](super::MAX_SELECTOR_EXPANSION_BYTES).
pub(crate) fn charge_selector_expansion(
&self,
context: &mut MinifyContext<'_, '_>,
) -> Result<(), MinifyErr> {
if context.selector_expansion_multiplier > 1 {
context.selector_expansion_total = context.selector_expansion_total.saturating_add(
context
.selector_expansion_multiplier
.saturating_mul(self.selectors.v.len().max(1)),
);
let len = self.selectors.v.len().max(1);
context.selector_expansion_total = context
.selector_expansion_total
.saturating_add(context.selector_expansion_multiplier.saturating_mul(len));
if context.selector_expansion_total > super::MAX_SELECTOR_EXPANSION {
context.err = Some(crate::error::MinifyError {
kind: crate::error::MinifyErrorKind::selector_expansion_limit_exceeded,
loc: self.loc,
});
return Err(MinifyErr::minify_err);
}

// Every expanded copy of one of this rule's selectors also repeats
// its ancestor chain, so charge chain bytes once per copy.
let own_bytes = selector::selector_list_weight(self.selectors.v.slice()) as u64;
let chain_bytes =
(context.selector_expansion_chain_bytes as u64).saturating_mul(len as u64);
context.selector_expansion_bytes_total =
context.selector_expansion_bytes_total.saturating_add(
(context.selector_expansion_multiplier as u64)
.saturating_mul(own_bytes.saturating_add(chain_bytes)),
);
if context.selector_expansion_bytes_total > super::MAX_SELECTOR_EXPANSION_BYTES {
context.err = Some(crate::error::MinifyError {
kind: crate::error::MinifyErrorKind::selector_expansion_bytes_limit_exceeded,
loc: self.loc,
});
return Err(MinifyErr::minify_err);
}
}
Ok(())
}
Expand Down Expand Up @@ -432,6 +456,7 @@ impl<R> StyleRule<R> {
// nesting is compiled away the printed output still fans out per
// selector, which is why the nesting branch bumps unconditionally.
let saved_expansion_multiplier = context.selector_expansion_multiplier;
let saved_expansion_chain_bytes = context.selector_expansion_chain_bytes;
let selectors_incompatible = self.selectors.v.len() > 1
&& context.targets.should_compile_selectors()
&& !self.is_compatible(context.targets);
Expand All @@ -440,9 +465,17 @@ impl<R> StyleRule<R> {
&& !self.selectors.any_has_pseudo_element()
&& self.selectors.specifities_all_equal());
if context.targets.should_compile_same(css::Feature::Nesting) || splits_selectors {
context.selector_expansion_multiplier = context
.selector_expansion_multiplier
.saturating_mul(self.selectors.v.len().max(1));
let len = self.selectors.v.len().max(1);
context.selector_expansion_multiplier =
context.selector_expansion_multiplier.saturating_mul(len);
// Each expanded descendant selector is prefixed with one selector
// from this level; track the level's average selector weight as
// the chain-bytes contribution.
let avg_weight =
(selector::selector_list_weight(self.selectors.v.slice()) / len).max(1);
context.selector_expansion_chain_bytes = context
.selector_expansion_chain_bytes
.saturating_add(avg_weight);
}

let mut handler_context = context.handler_context.child(DeclarationContext::StyleRule);
Expand All @@ -456,6 +489,7 @@ impl<R> StyleRule<R> {
&mut handler_context,
);
context.selector_expansion_multiplier = saved_expansion_multiplier;
context.selector_expansion_chain_bytes = saved_expansion_chain_bytes;
result
}

Expand Down
Loading
Loading