← 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 trigger requires only HTML/CSS: a display: grid container holding a scrollable inner box whose layout updates its scroll offset during grid pre-layout (the regression test uses vertical-rl overflow:scroll with a scaled descendant) and an absolutely positioned grid item with percentage padding that forces gridAreaRangeForOutOfFlow resolution before tracks are populated. The mid-layout scrollTo then calls viewportContentsChanged(), which recurses into renderer geometry and queries the unfinished grid area.

The most plausible release-build effect is an uninitialized read of grid track range state used to resolve the percentage padding. The consumer is layout arithmetic, not a copy into a JS-visible buffer, so no write primitive is visible. The minimum observable consequence is reliable renderer crash from untrusted CSS. This is part of a recurring class where scroll-offset adjustments inside updateScrollInfoAfterLayout re-enter helpers assuming layout completion.

This weakens renderer-process robustness rather than a trust boundary.