← All issues

[24] [grid layout] Don't call viewportContentsChanged() from scroll updates during render tree layout

Severity: Low | Component: WebCore rendering | bf80e3e

Rated Low because the diff confirms scrollTo() invoked viewportContentsChanged() while isInRenderTreeLayout() was true, tripping an assertion in debug and reading partially-initialized grid track state in release; the corrupted state flows into layout arithmetic rather than a memory-copy primitive, so the most plausible observable impact is a reliable renderer crash from attacker-shaped HTML/CSS.

RenderLayerScrollableArea::scrollTo() called LocalFrameView::viewportContentsChanged(), which computes visibility rects by querying renderer geometry. When invoked during layout (e.g. via updateScrollInfoAfterLayout during grid pre-layout), containing blocks have not finished, causing an assertion failure in gridAreaRangeForOutOfFlow when resolving percentage padding on an absolutely positioned iframe against a grid area not yet populated. The call is guarded with isInRenderTreeLayout().

Source/WebCore/rendering/RenderLayerScrollableArea.cpp

if (scrollsOverflow())
view.frameView().didChangeScrollOffset();
 
- view.frameView().viewportContentsChanged();
+ if (!view.frameView().layoutContext().isInRenderTreeLayout())
+ view.frameView().viewportContentsChanged();

Layout-phase invariant violation: a post-layout visibility-rect query is invoked from a scroll update that fires mid-layout, reading renderer geometry before containing blocks have completed layout.

The viewportContentsChanged() call in scrollTo() is guarded with !layoutContext().isInRenderTreeLayout(). The deferral is safe because performPostLayoutTasks() already calls viewportContentsChanged() unconditionally after every layout completes. A regression test nests an absolutely positioned <iframe> with percentage padding-left inside a CSS grid container holding a vertical-rl scrollable child with a scaled descendant.

Render tree layout is phased: the engine computes box geometry top-down, and certain helpers assume layout has completed before reading geometry. LocalFrameView::layoutContext().isInRenderTreeLayout() returns true during the layout pass. viewportContentsChanged() walks the renderer tree to compute visibility post-scroll, normally invoked from performPostLayoutTasks() once layout finishes. CSS grid uses a pre-layout pass to size tracks before placing items; out-of-flow grid items resolve their containing block through gridAreaRangeForOutOfFlow, which reads track ranges. Percentage padding resolves against the inline size of the containing block.

Pre-fix scrollTo() unconditionally called viewportContentsChanged(). When reached via updateScrollInfoAfterLayout during grid pre-layout, containing blocks had not finished and geometry was partially constructed. In the test configuration this led gridAreaRangeForOutOfFlow to resolve percentage padding-left against a grid area whose tracks were unpopulated, tripping the assertion in debug and reading uninitialized grid track state in release.

🔒

The phase-ordering invariant behind this layout assertion is examined, along with what the partially-constructed grid state could plausibly mean in release builds.

Subscribe to read more

🔒

Multiple reusable audit patterns across scroll-update entry points and grid out-of-flow resolution, with concrete WebCore starting points for variant discovery.

Subscribe to read more