e9f65dc37dd37c35111cb04b57dcccbf8f0dbd06 Fix crash in frame_serializer.cc
Triage note: Null-deref crash reachable through page save / MHTML serialization of a page containing nested CSS declarations. Renderer/utility DoS, no corruption; fixed with a null guard.
The bug at a glance
Low because the bug is a renderer-process null dereference (a DoS/crash), not a memory-corruption or information-disclosure primitive. kNestedDeclarationsRule is a CSSRule subtype that is grouped by the switch alongside real grouping rules but does not implement cssRules(), so rule->cssRules() returns nullptr and the immediate rule_list->length() dereferences null, crashing the renderer during frame serialization (Save Page As / MHTML capture). It stays watch-tier: reaching the code requires a serialization operation over a document that contains a nested-declarations rule inside one of the enumerated grouping-rule contexts (media/supports/container/layer-block/scope/starting-style), and the outcome is a deterministic null read that crashes a single renderer, not a controllable UAF or OOB. There is no attacker-controlled pointer value, no write, and no cross-origin data exposure.
This is a defensive null check that turns a serialization-time renderer crash into a no-op skip. CSS nesting shipped a new rule type, kNestedDeclarationsRule, that the frame serializer’s grouping-rule switch treated like a container of sub-rules even though it exposes no cssRules() list. Defenders should file this as a crash-resilience/robustness fix reachable via Save-Page/MHTML serialization on modern-nesting CSS, with a low, DoS-only ceiling; it is worth noting as a class reminder (nullable cssRules() must always be checked) rather than a security emergency.
Root cause
frame_serializer.cc walks the CSS object model to collect subresources referenced by stylesheets so they can be inlined into a serialized (MHTML) capture. A switch over CSSRule types handles grouping rules that contain nested rules: kMediaRule, kSupportsRule, kContainerRule, kLayerBlockRule, kScopeRule, kStartingStyleRule (and, on main, kNestedDeclarationsRule fell into the same handling). For those cases the code called rule->cssRules() and iterated the returned CSSRuleList.
The root cause is a type-model mismatch: kNestedDeclarationsRule represents the bare declarations that appear directly inside a nesting context (e.g. properties written alongside nested selectors). It is not a CSSGroupingRule and does not own a child rule list, so its cssRules() returns nullptr rather than an empty list. The pre-fix loop wrote CSSRuleList* rule_list = rule->cssRules(); for (unsigned i = 0; i < rule_list->length(); ++i) … which dereferences rule_list unconditionally, producing a null-pointer read and renderer crash the moment such a rule is encountered during serialization. The fix wraps the access in if (CSSRuleList* rule_list = rule->cssRules()) { … }, so any rule whose cssRules() is null is simply skipped. The commit message notes this ‘catch[es] this problem generically’, i.e. it defends every current and future rule type in the switch that might return null, not just the nested-declarations case.
Key code
Guard the nullable cssRules() before iterating (frame_serializer.cc)
- 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));
+ }
}
Patch walkthrough
third_party/blink/renderer/core/frame/frame_serializer.cc— Within SerializeCSSRuleResources’ grouping-rule switch case, the raw fetch of rule->cssRules() and immediate rule_list->length() call is replaced by an if-with-initializer guard. If cssRules() returns null (as kNestedDeclarationsRule does), the loop body is skipped entirely, avoiding the null dereference. Behavior for genuine grouping rules is unchanged; the only observable change is that nested-declaration rules no longer crash serialization and contribute no sub-resources (correct, since they contain only declarations).
Background
kNestedDeclarationsRule — A CSSRule type introduced with CSS Nesting to model the loose property declarations that appear directly inside a nesting block alongside nested selectors. It holds declarations, not child rules, so it is not a CSSGroupingRule and returns null from cssRules().
CSSGroupingRule / cssRules() — Grouping rules (@media, @supports, @container, @layer{}, @scope, @starting-style) own a CSSRuleList of nested rules, returned by cssRules(). Non-grouping rules may return null. The serializer’s switch conflated a non-grouping type into the grouping handling.
FrameSerializer / MHTML — Blink’s frame serializer produces a self-contained capture of a page (Save Page As Complete / MHTML). It must walk stylesheets to inline referenced resources, which is why it recursively descends CSS rule lists.
Reachability — The crash requires (a) a document whose CSS uses nesting such that a nested-declarations rule sits inside one of the enumerated grouping contexts, and (b) a serialization pass over that document (user Save-Page, MHTML generation, or any internal caller of the serializer).
Vulnerability window
- Introduction — Latent since CSS Nesting added kNestedDeclarationsRule; the serializer switch was never updated to account for its null cssRules(), so the crash lay dormant until such CSS was serialized.
- Discovery — Tracked as crbug 477316498, ‘Fix crash in frame_serializer.cc’ — a reported/observed crash rather than a proactive audit.
- Fix — 2026-01-21, main@{#1572511}, by Dan Harrington, reviewed by Ian Kilpatrick. Per the task note, the main-branch fix was backported (as b92b3b5af5). [M###] backport — flag for branch-coverage tracking.
Proof of concept
No test shipped in this diff. Recipe to reproduce (no exploit, DoS only): construct a page whose stylesheet uses CSS nesting so a nested-declarations rule appears inside a grouping context, e.g. an @media/@container/@layer block containing a selector with bare declarations that Blink represents as kNestedDeclarationsRule; then invoke frame serialization (Save Page As -> Webpage, Complete, or trigger MHTML capture). Pre-fix, the serializer hits rule->cssRules() == nullptr and crashes the renderer during the walk. No memory-safety consequence beyond the null read; no exploit demonstrated.
Exploitation
- Precondition — Victim must serialize a page (user-initiated Save/MHTML, or an internal serialization path) whose CSS produces a nested-declarations rule inside an enumerated grouping rule.
- Primitive — Deterministic null-pointer read -> renderer crash (denial of service). No pointer control, no write, no read of attacker-influenced address.
- Blockers — Requires the serialization action to actually run; a page merely being open does not trigger it. Impact ceiling is a single-renderer crash, which is why it is low/DoS and watch-tier.
Detection & hunting
For defenders and detection engineers:
- Crash telemetry — Look for renderer null-deref crashes with SerializeCSSRuleResources / FrameSerializer frames on the stack, correlated with Save-Page/MHTML operations. The faulting address will be a small offset (CSSRuleList::length vtable/field access on nullptr).
- Content heuristic — Pages combining CSS nesting with @media/@container/@layer/@scope/@starting-style are the trigger surface; flag serialization attempts over such documents in fuzzing/regression corpora.
Audit directions
- frame_serializer.cc — Audit the rest of the CSS rule switch and any other callers of CSSRule::cssRules() in the serializer for unchecked dereferences; confirm no other non-grouping rule type is grouped with real grouping rules.
- CSS OM callers generally — Grep Blink for
cssRules()results dereferenced without a null check (DevTools, inspector, style serialization, editing). Nested-declarations and other declaration-only rule types are the recurring source of null returns.