b92b3b5af59864beba802b4b825d1aa2e383e46e [M145] Fix crash in frame_serializer.cc
Triage note: M145 cherry-pick of e9f65dc37d (Bug 478214622). Same null-deref crash on page/MHTML serialization; deduped to the primary.
The bug at a glance
This is a null-pointer dereference reachable during DOM/CSS serialization, fixed by a defensive null check. It is a crash, not a memory-corruption primitive: cssRules() returns a genuine nullptr (not a dangling or attacker-typed pointer), and the immediate dereference of rule_list->length() faults on a null read, which the sandbox handles as a clean renderer crash. There is no write, no type confusion, no controlled offset — the null is a fixed near-zero address, so it is not an exploitable-grade condition. It rates low with a modest CVSS (4.0) because the impact is denial of service (renderer tab crash) rather than any confidentiality/integrity loss. It is watch-tier rather than patch-now for two reasons: the crash triggers on the frame-serialization path (Save Page As / MHTML capture / SavePage-style operations), which is typically user-initiated rather than a silently-attacker-triggered web surface, and the trigger requires a page containing CSS that produces a kNestedDeclarationsRule reaching this switch arm. That said, it was worth an M145 backport because a web page can reliably author the offending CSS, so a malicious or malformed stylesheet can crash the renderer whenever the page is serialized.
For defenders, the lesson is a classifier bug in Blink’s CSSOM: kNestedDeclarationsRule looks like it belongs with the CSS grouping rules (kMediaRule, kSupportsRule, kLayerBlockRule, kScopeRule, kStartingStyleRule) that this switch iterates via cssRules(), but it is NOT a CSSGroupingRule and therefore returns null from cssRules(). The serializer assumed every rule reaching that switch arm exposes a non-null child rule list and dereferenced it unconditionally. The fix is deliberately generic: instead of special-casing kNestedDeclarationsRule, it guards the returned pointer for null so any current or future rule type that returns null from cssRules() in this arm is handled safely. Anyone auditing serialization, devtools, or CSSOM-walking code should treat ‘rule->cssRules() dereferenced without a null check’ as a bug pattern, because rule-type-to-interface assumptions drift as new CSS nesting features land.
Root cause
The root cause is an incorrect assumption that every CSS rule type routed into the grouping-rule arm of SerializeCSSRuleResources exposes a non-null CSSRuleList via cssRules(). CSS nesting introduced kNestedDeclarationsRule, which wraps bare declarations inside a nested context. Despite being grouped alongside conditional/grouping rules in the serializer’s switch, it does not implement the CSSGroupingRule interface, so CSSRule::cssRules() returns nullptr for it.
Pre-patch, the code did: CSSRuleList* rule_list = rule->cssRules(); then immediately entered a for loop reading rule_list->length(). When rule was a kNestedDeclarationsRule (or any type returning null there), rule_list was nullptr and rule_list->length() dereferenced a null pointer, faulting the renderer. This is reached when Blink serializes a frame’s CSS resources — walking the stylesheet rule tree to capture referenced resources for saving/archiving the page. A page whose stylesheet uses CSS nesting such that a nested-declarations rule appears where the serializer recurses will therefore crash the renderer at serialization time.
The fix wraps the assignment in an if condition: if (CSSRuleList* rule_list = rule->cssRules()) { …iterate… }. When cssRules() returns null, the loop is skipped entirely and serialization proceeds. The commit message is explicit that the guard is intended to ‘catch this problem generically’ rather than enumerate the specific offending rule type, hardening the path against future CSSOM interface changes. No other behavior changes; well-formed grouping rules still iterate their child rules exactly as before.
Key code
Null-guard the child rule list before iterating; kNestedDeclarationsRule returns null from cssRules()
case CSSRule::kLayerBlockRule:
case CSSRule::kScopeRule:
case CSSRule::kStartingStyleRule: {
- CSSRuleList* rule_list = rule->cssRules();
- for (unsigned i = 0; i < rule_list->length(); ++i) {
- SerializeCSSRuleResources(rule_list->item(i));
+ if (CSSRuleList* rule_list = rule->cssRules()) {
+ for (unsigned i = 0; i < rule_list->length(); ++i) {
+ SerializeCSSRuleResources(rule_list->item(i));
+ }
}
break;
}
Patch walkthrough
third_party/blink/renderer/core/frame/frame_serializer.cc— In SerializeCSSRuleResources, the grouping-rule switch arm (kMediaRule/kSupportsRule/kLayerBlockRule/kScopeRule/kStartingStyleRule) previously stored rule->cssRules() and dereferenced it unconditionally. The change moves the assignment into an if-condition so the iteration is skipped when cssRules() is null. This prevents the null-deref when a kNestedDeclarationsRule (which is not a CSSGroupingRule and returns null from cssRules()) reaches this arm, and generically protects against any other rule type that returns null here.
Background
frame_serializer.cc — Blink’s frame serializer walks a document (and its CSS) to produce a self-contained capture — used for Save Page As (complete/MHTML), page archiving, and related features. It recursively visits CSS rules to collect referenced resources (fonts, images via url()). Crashes here manifest during save/serialize operations.
CSSGroupingRule / cssRules() — Grouping rules (@media, @supports, @layer block, @scope, @starting-style) contain child rules exposed through the cssRules() accessor as a CSSRuleList. Rule types that are not grouping rules return null from cssRules(). The serializer’s switch conflated a non-grouping rule with the grouping set.
kNestedDeclarationsRule — A CSSOM rule type introduced with CSS Nesting to represent bare declarations that appear inside a nested rule context. Although semantically it lives inside other rules, it does not itself implement CSSGroupingRule and therefore has no child rule list — cssRules() returns null for it.
Renderer-crash DoS in the sandbox — A null-pointer read in the renderer traps and the process is torn down cleanly by the sandbox. There is no cross-process corruption; the security impact is availability (the tab/frame dies), which is why this is a low-severity DoS rather than an exploitable memory-safety flaw.
Vulnerability window
- Root cause introduced — CSS Nesting’s kNestedDeclarationsRule became reachable in the serializer’s grouping-rule switch arm without the serializer accounting for its null cssRules(), leaving the unconditional dereference latent until such a rule is serialized.
- Fixed on main — Original commit e9f65dc37dd37c35111cb04b57dcccbf8f0dbd06, authored by Dan Harrington, reviewed by Ian Kilpatrick. Bug 477316498. Landed main@{#1572511} on 2026-01-23.
- Backport [M145] — This artifact (b92b3b5af5) is the M145 cherry-pick of e9f65dc37d, landed on branch-heads/7632@{#858}, tracked under Bug 478214622 (backport tracking) plus the original 477316498, Rubber-Stamper/Cherry-Picker approved. The [M145] tag and ‘cherry picked from’ line confirm a stable-branch backport, meaning the crash was judged worth fixing in the released milestone.
Proof of concept
No test or reproducer ships in this diff. No exploit is possible or claimed; the outcome is a renderer crash only. Reconstruction recipe (not demonstrated): author a page with a stylesheet using CSS nesting that produces a kNestedDeclarationsRule inside a rule context the serializer recurses into (bare declarations following a nested selector within an @media/@supports/@layer/@scope/@starting-style block), then invoke a serialization path that calls SerializeCSSRuleResources — e.g. Save Page As (Complete/MHTML) or an automation/devtools capture of the frame. Pre-patch, the walk over that nested-declarations rule dereferences a null CSSRuleList and crashes the renderer. Post-patch the loop is skipped. This is a crash reproducer only; there is no memory disclosure or code-execution consequence.
Exploitation
- Craft trigger — Attacker authors CSS (fully web-controllable) using nesting so a kNestedDeclarationsRule reaches the serializer’s grouping-rule arm. No special privilege is needed to place such CSS on a page.
- Reach the sink — The serialization path must run. This is the key blocker: frame serialization is generally user-initiated (Save Page As / archive) or triggered by embedder/automation features, not a silent always-on web surface. An attacker cannot usually force a victim to serialize the page purely from script, which caps real-world reach to scenarios where serialization is invoked (user save, print-to-archive flows, headless capture pipelines, extensions/automation).
- Impact ceiling — Result is a null read -> clean renderer crash (DoS of the tab/frame or of a capture pipeline). No write, no controlled address, no info leak. The fixed null address makes exploitation beyond DoS infeasible. This is why the finding is watch-tier.
Detection & hunting
For defenders and detection engineers:
- Renderer crash with null-deref in frame_serializer.cc / SerializeCSSRuleResources — Crash reports (pre-patch) showing a null read inside SerializeCSSRuleResources during a save/serialize operation indicate this bug or a sibling CSSOM-null-deref. Correlate with pages using CSS nesting.
- Serialization/capture pipelines crashing on specific sites — Headless or automation MHTML/save pipelines that crash consistently on particular pages should be examined for CSS-nesting content; a crash localized to CSS rule walking points here.
Audit directions
- CSSOM-walking code that calls cssRules() — Audit frame_serializer.cc and other consumers (devtools CSS agents, editing/style code, inspector serialization) for rule->cssRules() dereferenced without a null check. Any code assuming a rule type in a grouping-rule switch always returns a non-null CSSRuleList shares this bug’s assumption.
- Rule-type interface assumptions — Enumerate CSS rule types that are grouped with CSSGroupingRule in switches but do NOT implement it (kNestedDeclarationsRule and any future nesting/declaration rule types). Confirm each consumer handles their null child-list and non-grouping interface correctly.