← Chrome — 2026-W05

f31e214aac7d07b39b6d07232f260fefc6776c93  Remove failing CHECK.

severity medium class LogicError CVSS 5.9 confidence 0.45 signal score 3.8 third_party/blink/renderer AI review CONFIRMED
Primitive: A CHECK asserting the containing block equals the transition scope (or is the view-transition root) could fail for view-transition pseudo-elements, crashing the renderer during paint property tree building.
Triage note: Removes a reachable CHECK failure (DoS) for view transitions, which are default-enabled; root cause is unknown and it is a defensive workaround, so severity/exploitability beyond a crash is unproven from the diff.

The bug at a glance

A CHECK in the paint property tree builder could fire for view-transition pseudo-elements when the containing block did not equal the transition scope and was not the view-transition root. A reachable CHECK is a deterministic renderer-process abort: a denial-of-service triggerable by web content that uses the View Transitions API. It is not a demonstrated memory-corruption primitive, and the author explicitly notes the root cause is unknown, so this is triaged as a defensive fix for a crashing invariant rather than a confirmed spatial/temporal safety bug. Medium (5.9) reflects a content-reachable renderer crash with no established path to code execution. Honest caveat: because the underlying reason the invariant was violated is not understood, there is residual risk that the same divergent state has other, less benign consequences elsewhere in property-tree construction; that uncertainty argues against downgrading further.

Reachable from untrusted content through the View Transitions API (document.startViewTransition and the ::view-transition* pseudo-element tree). Some combination of styling/containment on a view-transition pseudo-element produces a containing block that is neither the transition scope nor the view-transition root, which was previously unreachable-by-assumption. Content that constructs that DOM/CSS shape drives paint property tree building into the failing CHECK.

Root cause

During paint property tree building for a view-transition pseudo-element, the builder locates the transition ‘scope’ as the LayoutBox of the pseudo-element’s UltimateOriginatingElement. When a scope exists, the code needed to undo a scroll-origin offset that UpdateScrollAndScrollTranslation() had applied. The pre-fix code asserted, via CHECK(containing_block == scope || containing_block->IsViewTransitionRoot()), that the pseudo-element’s containing block must be either the scope itself or the view-transition root, and then unconditionally subtracted the scroll-origin offset. In practice that invariant did not always hold: the containing block could be some other block, and the CHECK aborted the renderer. The scroll-origin undo is only correct when the scope actually is the containing block (that is the case in which UpdateScrollAndScrollTranslation applied the offset being undone). The fix reframes the code to compute containing_block first and gate the offset adjustment on scope && containing_block == scope, dropping the CHECK entirely. When the containing block is not the scope, the adjustment is simply skipped instead of asserting, which both removes the crash and keeps the offset correction scoped to the only case the author is confident about. The author is candid that the deeper reason the containing block sometimes differs is unresolved; this is a targeted stop-the-crash change, not a root-cause fix.

Key code

paint_property_tree_builder.cc: replace the failing CHECK with a precise guard that only undoes the scroll-origin offset when the scope is the containing block.

auto* scope = DynamicTo<LayoutBox>(To<PseudoElement>(object_.GetNode())
                                       ->UltimateOriginatingElement()
                                       .GetLayoutObject());
LayoutBlock* containing_block = object_.ContainingBlock();
if (scope && containing_block == scope) {
  // Undo the scroll origin offset that was applied during
  // UpdateScrollAndScrollTranslation().
  context_.current.paint_offset -= /* scroll origin offset */;
}

Patch walkthrough

  • third_party/blink/renderer/core/paint/paint_property_tree_builder.cc — The only changed file. Previously: if (scope) { compute containing_block; CHECK(containing_block == scope || containing_block->IsViewTransitionRoot()); undo scroll-origin offset; }. After: compute containing_block unconditionally, then if (scope && containing_block == scope) { undo scroll-origin offset; }. Two effects: (1) the CHECK is deleted, so the previously reachable abort can no longer fire; (2) the scroll-origin undo now runs only in the scope==containing_block case rather than also in the IsViewTransitionRoot() case. The author’s rationale is that the offset undo is only warranted when the scope is the containing block, so restricting it is both safe and the natural way to drop the assertion without changing correct-case behavior.

Background

Paint property tree builder — Blink component that constructs the transform/clip/effect/scroll property trees consumed by compositing and paint. It runs during the pre-paint lifecycle phase and enforces many invariants via CHECK/DCHECK; a firing CHECK here aborts the renderer.

View transitions / pseudo-elements — The View Transitions API builds a tree of ::view-transition* pseudo-elements to animate between DOM states. These pseudo-elements have an UltimateOriginatingElement and participate in a dedicated ‘scope’ and a view-transition root in the layout/paint tree.

Transition scope / view-transition root — The scope is the LayoutBox of the pseudo-element’s originating element; the view-transition root is the synthetic root of the transition subtree. The removed CHECK assumed a pseudo-element’s containing block is always one of these two.

Scroll origin offset — UpdateScrollAndScrollTranslation() applies a paint-offset adjustment for scroll origin. The property builder later undoes it for the scope so the pseudo-element is positioned correctly; the undo only makes sense when the scope is the containing block that received the original adjustment.

Vulnerability window

  1. Reported — crbug.com/476169471 captured a paint-property-tree CHECK failure crash tied to view-transition pseudo-elements.
  2. Triage — The exact state that violates the containing-block == scope invariant was not identified (‘Root cause is unknown’), so the response was scoped to remove the crash safely.
  3. Fix — CHECK removed and the scroll-origin undo gated on scope == containing_block; landed on main@{#1577505}.

Proof of concept

No proof-of-concept or regression test is included in the diff, and the commit states the root cause is unknown, so a reliable trigger recipe is not established by the patch. Honest characterization: content using document.startViewTransition with view-transition pseudo-elements whose containing block resolves to something other than the transition scope reaches the removed CHECK; the specific CSS/DOM shape that produces that condition was not pinned down by the author. I will not fabricate exploit content for an unconfirmed trigger.

Exploitation

  1. Setup — Drive a view transition (document.startViewTransition) so ::view-transition* pseudo-elements are created with a scope box and enter paint property tree building.
  2. Trigger — Arrange styling/containment so a pseudo-element’s containing block is neither the scope nor the view-transition root. Pre-fix, the CHECK fires and the renderer aborts (DoS).
  3. Post-fix behavior — The offset undo is skipped in that case. Because the true root cause is unknown, skipping it could leave a subtle paint-offset error in the divergent state; no memory-safety consequence is demonstrated, but the state confusion warrants continued monitoring.

Detection & hunting

For defenders and detection engineers:

  • CHECK-failure crashes in paint_property_tree_builder.cc —
  • Correlation with View Transitions API usage —

Audit directions

  • Find the real root cause of the invariant break —
  • Other assumptions about scope == containing_block —
  • Paint-offset correctness in the skipped case —

Before / after

Loading diff…