CVE-2025-24216
Overview
Background
- CSS nesting resolution
- Flattening nested CSS rules into fully-qualified selectors when building a RuleSet.
- StyleSheetContents / RuleSet
- Parsed stylesheet contents and the built rule set; RuleSets can be rebuilt, re-running resolution.
- hasResolvedNesting flag
- New per-sheet state ensuring nesting is resolved once, reset when the sheet mutates.
- destructorHasBeenCalled
- A CSSSelector flag, checked under ASSERT_WITH_SECURITY_IMPLICATION, that detects use of a destroyed selector.
Root Cause Analysis
CSS nesting resolution flattens nested style rules into resolved selectors when building a RuleSet from a stylesheet. RuleSetBuilder drove this with a per-builder flag (m_shouldResolveNesting), but resolution could run more than once against the same StyleSheetContents (e.g. when a RuleSet is rebuilt) and, on a subsequent pass, operate on CSSSelector data whose owning objects had already been destroyed — a use-after-destroy of selector memory (surfacing as a Safari crash).
The fix makes nesting resolution idempotent per sheet and adds a memory-safety assertion: StyleSheetContents gains a hasResolvedNesting()/setHasResolvedNesting() flag, RuleSetBuilder computes m_shouldResolveNestingForSheet as builderShouldResolveNesting && !sheet.hasResolvedNesting() (so a sheet’s nesting is resolved exactly once and marked resolved via setHasResolvedNesting(true)), CSSStyleSheet::didMutateRules resets the flag with setHasResolvedNesting(false) so a genuinely mutated sheet re-resolves, and SelectorChecker/CSSSelector add ASSERT_WITH_SECURITY_IMPLICATION(!selector.destructorHasBeenCalled()) backed by a destructorHasBeenCalled flag to catch use of a destroyed selector.
The restored invariant is that a stylesheet’s nesting is resolved once per (unmutated) content, so resolution never re-runs over selectors that have since been destroyed.
Attack Path
- Author nested CSS Provide a stylesheet using CSS nesting whose rules get resolved into flattened selectors.
- Trigger a rebuild Cause the RuleSet to be rebuilt so nesting resolution runs again against the same sheet contents.
- Re-resolve over destroyed selectors Pre-patch, the second resolution pass operates on CSSSelector data whose owning objects were already destroyed.
- Use-after-destroy Reading the destroyed selector memory crashes or corrupts memory in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
RuleSetBuilder::addChildRule / addRulesFromSheetContents / addStyleRuleWithSelectorListSource/WebCore/style/RuleSetBuilder.cpp |
modified | Gates nesting resolution on m_shouldResolveNestingForSheet = builderShouldResolveNesting && !sheet.hasResolvedNesting(), and marks the sheet resolved, so nesting resolves exactly once per unmutated sheet. |
CSSStyleSheet::didMutateRulesSource/WebCore/css/CSSStyleSheet.cpp |
modified | Calls setHasResolvedNesting(false) so a mutated sheet re-resolves nesting on the next build. |
SelectorChecker / CSSSelector::destructorHasBeenCalledSource/WebCore/css/SelectorChecker.cpp |
modified | Adds ASSERT_WITH_SECURITY_IMPLICATION(!selector.destructorHasBeenCalled()) and a destructor flag on CSSSelector to catch use of a destroyed selector. |
Files Changed
LayoutTests/fast/selectors/has-nesting-crash-expected.txtLayoutTests/fast/selectors/has-nesting-crash.htmlSource/WebCore/css/CSSSelector.hSource/WebCore/css/CSSStyleSheet.cppSource/WebCore/css/SelectorChecker.cppSource/WebCore/css/StyleSheetContents.hSource/WebCore/style/RuleSetBuilder.cppSource/WebCore/style/RuleSetBuilder.h
Audit Directions
- Repeated resolution passesAudit RuleSet rebuild paths for operations that assume single-pass resolution over selector data that may be freed between passes.
- Selector lifetimegrep for CSSSelector/CSSSelectorList uses across rule-set rebuilds; extend destructorHasBeenCalled-style checks where selectors outlive expectations.
- didMutateRules invalidationConfirm every cached resolution/state keyed on sheet contents is reset in didMutateRules and similar mutation hooks.