← Chrome — 2026-W03

e7d8e0eb72f2403d4ce1ce4d92550ead6fe263ac  [layout] Avoid <fieldset> crash for float state change.

severity low class layout-crash CVSS 5.0 confidence 0.4 signal score 3.8 third_party/blink/renderer AI review CONFIRMED
Primitive: A recent feature allowed adding a fieldset child with a non-null before_child, which LayoutFieldset::AddChild did not expect, crashing on a float dynamic change. Fix routes through the existing before_child selection logic.
Triage note: Renderer layout crash reachable via <fieldset> plus a float state change, but only when RuntimeEnabledFeatures::LayoutReinsertOnInFlowStateChange (non-default) is enabled. Gated; watch.

The bug at a glance

This is a renderer-process crash reachable from ordinary, unprivileged web content: a containing a block child and a , followed by a script that toggles the child to float:right after layout. The included WPT crashtest reproduces it in ~10 lines with no special flags beyond the feature being enabled. There is no evidence in the diff of memory corruption, controlled write, or type confusion — the failure is an unexpected non-null before_child violating an invariant that LayoutFieldset::AddChild encoded (‘before_child is always nullptr’), which manifests as a crash (DCHECK/CHECK or null-deref) rather than an exploitable primitive. Impact is therefore denial of service / tab crash, which is why low / CVSS ~5.0 is honest and not inflated. It is flagged isSecurity=true because it is an attacker-reachable crash introduced by a recent feature (a regression), but it is watch-tier: no exploitation path beyond DoS is shown, and the crash is a defensive invariant firing, not silent corruption.

The interesting part is that this is a regression created by another in-flight feature. crrev.com/c/7427180 (the LayoutReinsertOnInFlowStateChange work) newly allows a child to be re-added with a non-null before_child during an in-flow/out-of-flow (float) state change. LayoutFieldset::AddChild was written under the older, now-false assumption that adding a non-text/non-anonymous child to a fieldset always triggers a full reattach, so before_child would always be nullptr. The angle is the invariant drift: a hard assumption baked into layout code silently became reachable-as-false when a neighboring feature changed the reinsertion model, and the fieldset’s specialized child-insertion path had no handling for the newly possible input.

Root cause

LayoutFieldset::AddChild specializes how children are threaded into the fieldset’s anonymous layout structure (it has to keep the rendered legend and the anonymous content box in the right order). Its first branch handled the !IsText() && !IsAnonymous() case under the comment ‘Adding a child LayoutObject always causes reattach of . So before_child is always nullptr. See HTMLFieldSetElement::DidRecalcStyle().’ That branch therefore did not account for a meaningful before_child. Once c/7427180 enabled reinserting a fieldset child in place on a float (in-flow) state change — rather than reattaching the whole fieldset — AddChild could be invoked with a real before_child pointing at a sibling (e.g. the legend), breaking the invariant and crashing. The fix gates that branch behind !RuntimeEnabledFeatures::LayoutReinsertOnInFlowStateChangeEnabled(): when the feature is on, the branch is skipped and control falls through to the general before_child-selection logic added in c/2975047, which already computes the correct DOM-tree-ordered before_child (including legend placement). This unifies the two code paths instead of special-casing the reattach assumption.

Key code

Gate the reattach-assumption branch off when in-flow reinsert is enabled, so a real before_child is honored

void LayoutFieldset::AddChild(LayoutObject* new_child,
                              LayoutObject* before_child) {
-  if (!new_child->IsText() && !new_child->IsAnonymous()) {
+  if (!RuntimeEnabledFeatures::LayoutReinsertOnInFlowStateChangeEnabled() &&
+      !new_child->IsText() && !new_child->IsAnonymous()) {
     // Adding a child LayoutObject always causes reattach of <fieldset>. So
     // |before_child| is always nullptr.
     // See HTMLFieldSetElement::DidRecalcStyle().

Patch walkthrough

  • third_party/blink/renderer/core/layout/forms/layout_fieldset.cc — Adds !RuntimeEnabledFeatures::LayoutReinsertOnInFlowStateChangeEnabled() to the guard on the special-case branch. With the feature enabled, the branch that assumes before_child==nullptr (and was written for the full-reattach model) is bypassed, so AddChild falls through to the shared before_child selection logic (from crrev.com/c/2975047) that derives the correct insertion point from the DOM tree. This removes the crashing assumption while keeping legacy behavior intact when the feature is off.
  • third_party/blink/web_tests/external/wpt/css/CSS2/floats/crashtests/float-dynamic-change-5.html — New WPT crashtest: a with a #target div and a ; the script forces layout (document.body.offsetTop) then sets target.style.float=‘right’. This drives the in-flow -> out-of-flow state change that reinserts the child with a non-null before_child (the legend), exercising the previously crashing path. Presence of this test is the concrete reproduction shipped with the fix.

Background

LayoutFieldset::AddChild — Fieldset has bespoke layout: a rendered legend plus an anonymous content wrapper. AddChild must place incoming children relative to that structure, historically assuming a full reattach so before_child could be ignored.

before_child — The layout sibling before which new_child should be inserted. nullptr means append. Correctly computing it for fieldset requires the DOM-tree-aware logic from crrev.com/c/2975047; the old branch shortcut assumed nullptr.

LayoutReinsertOnInFlowStateChange — A runtime-flag feature (crrev.com/c/7427180) that, on an in-flow/out-of-flow transition such as toggling float, reinserts the affected child in place instead of reattaching its container. This is what newly produces a non-null before_child for fieldset children.

HTMLFieldSetElement::DidRecalcStyle — The path referenced by the old comment that forced fieldset reattach on child changes — the mechanism that used to guarantee before_child==nullptr, now no longer the only path under the new feature.

Vulnerability window

  1. latent-assumption — LayoutFieldset::AddChild long assumed non-text/non-anonymous child additions always reattach, so before_child is nullptr (per HTMLFieldSetElement::DidRecalcStyle).
  2. regression-introduced — crrev.com/c/7427180 enabled in-flow-state-change reinsertion, making it possible to call AddChild on a fieldset with a non-null before_child, violating the old assumption.
  3. report — Filed as crbug 476180594: a crash on a float dynamic change.
  4. fix — Ian Kilpatrick landed e7d8e0eb72 on 2026-01-16 (main@{#1570611}), reviewed by David Grogan, gating the branch behind the feature flag and reusing the c/2975047 before_child logic, plus a WPT crashtest.

Proof of concept

The commit ships a WPT crashtest that is a faithful reproducer. It builds a whose first child is a block div (#target) followed by a , forces a layout flush by reading document.body.offsetTop, then sets target.style.float=‘right’. The float change triggers the in-flow -> out-of-flow reinsertion (under the LayoutReinsertOnInFlowStateChange feature), which re-adds #target to the fieldset with a non-null before_child (the legend), hitting the unhandled assumption in LayoutFieldset::AddChild and crashing the renderer pre-patch. No exploit primitive beyond the crash is demonstrated; this is a DoS reproducer, not weaponized code.

<!DOCTYPE html>
<link rel="help" href="https://crbug.com/476180594">
<fieldset>
  <div id="target"></div>
  <legend></legend>
</fieldset>
<script>
document.body.offsetTop;
target.style.float = 'right';
</script>

Exploitation

  1. reachability — Fully reachable from untrusted web content with trivial HTML+JS, provided the LayoutReinsertOnInFlowStateChange feature is enabled in the target build/channel.
  2. trigger — Any layout followed by an in-flow/out-of-flow state change on a fieldset child (float toggle is the demonstrated vector) that causes reinsertion with a real before_child.
  3. impact-ceiling — Renderer crash (tab/DoS). The diff shows no controllable write or object-lifetime corruption; the honest ceiling is denial of service.

Detection & hunting

For defenders and detection engineers:

  • Renderer crashes in LayoutFieldset::AddChild — Look for crash reports / DCHECK failures in fieldset child insertion coinciding with float or position style mutations, especially on builds with LayoutReinsertOnInFlowStateChange enabled.
  • Feature-flag correlation — Correlate fieldset-related layout crashes with the rollout state of LayoutReinsertOnInFlowStateChange; a regression window matching the flag’s ramp confirms this class.

Audit directions

  • Layout::AddChild overrides assuming before_child==nullptr* — Grep layout for AddChild overrides and comments asserting ‘before_child is always nullptr’ or ‘always causes reattach’ (LayoutButton, LayoutListItem, other anonymous-wrapper hosts). Each such assumption may now be falsifiable under LayoutReinsertOnInFlowStateChange and needs the same fall-through to DOM-ordered before_child selection.
  • crrev.com/c/7427180 fan-out — Enumerate all layout objects affected by in-flow/out-of-flow reinsertion introduced by c/7427180 and verify each handles a non-null before_child during float/position/display transitions; this fieldset case is likely one instance of a broader invariant-drift class.

Before / after

Loading diff…