[3] GFC auto-fill repeat count has no upper clamp before allocation
Low. No bug is fixed here; this is feature work whose repetition count is a quotient of two author-controlled lengths with no ceiling. The plausible ceiling is renderer memory or CPU exhaustion, and only if grids with auto-fill templates are admitted to the new layout path.
Layout engines expand repeating CSS constructs into concrete lists whose length derives from container geometry. WebKit is building a new Grid Formatting Context (GFC), a grid layout implementation under Source/WebCore/layout/ developed alongside the legacy one, in which ExplicitGridResolver turns grid-template-columns/rows into explicit track lists. repeat(auto-fill, ...) asks the engine to repeat a track list as many times as fits, and the invariant is that the resulting track count stays small enough for downstream allocation.
The angle: author CSS with a zero-width repeat list, zero gap and a huge container could drive the explicit track count into the tens of millions and exhaust renderer memory or CPU, if grids with auto-fill templates reach the new layout path.
Add basic support for auto-repeat tracks, focused on auto-fill.
ExplicitGridResolverchecks for auto-fill tracks in the track list and uses the newAutoRepeatResolverto compute how many repetitions come from them.AutoRepeatResolverimplements the spec text: it computes the space the repeated tracks fill, ensures each entry in the list has a definite track size, and works out how many times the list repeats.ExplicitGridResolverthen splices the repetitions into the track list at the insertion point given by style. This needs extra plumbing for the used gap sizes.
Source/WebCore/layout/formattingContexts/grid/AutoRepeatResolver.cpp
Source/WebCore/layout/formattingContexts/grid/ExplicitGridResolver.cpp
Patch Details
The new AutoRepeatResolver.cpp adds four functions. trackSizeForRepetitions() picks a definite size for a track: the max sizing function if definite, otherwise the min, and std::max of both when both are definite. nonRepeatedTracksSpace() sums the space used by tracks outside the repeat plus one gap per track, dereferencing each std::optional without a check. repetitionsToFill() sums the repeated tracks' sizes, floors the sum to 1_lu (the spec's division-by-zero guard), and computes (spaceForRepetitions / sizeOfRepetition).toUnsigned(), adding one repetition for the SmallestFulfillingSize strategy. AutoRepeatResolver::resolveRepetitions() picks the size to fill: the definite available space, then max(container max size, container min size), then the container min size, and otherwise returns 1.
In ExplicitGridResolver.cpp, the new trackSizesWithAutoRepetitions() calls reserveInitialCapacity(sizes.size() + repetitionCount * autoRepeatSizes.size()) and appends the repeat list once per repetition at autoRepeatInsertionPoint. That insertion point is checked only by a debug ASSERT. resolveTrackSizes() runs this splice when autoRepeatType == AutoRepeatType::Fill and the repeat list is non-empty. ExplicitGridResolver::resolve() and its callers in GridFormattingContext.cpp now receive the used column and row gaps, and a FIXME notes that intrinsic sizing does not yet pass min-width or max-width. Nothing clamps the repetition count against a maximum track count.
A count from dividing one attacker-controlled size by another, with only a divisor floor and no quotient ceiling, sizes an allocation and a loop.
Background
Where this lives.
WebKit is building a new layout engine under Source/WebCore/layout/ (LFC) alongside the legacy render-tree layout. The Grid Formatting Context (GFC) is its grid part. LayoutIntegrationGridCoverage decides per grid whether GFC handles layout or the legacy RenderGrid does, and it includes a GridFormattingContextIntegrationDisabled avoidance reason.
repeat(auto-fill, ...).
It repeats a track list as many times as fits in the grid container. The spec divides the free space (minus the non-repeated tracks, with gaps counted) by the size of one repetition, and requires a UA-defined floor on the divisor (suggested as 1px) to avoid division by zero. The <auto-track-list> grammar allows only <fixed-size> tracks beside an auto-repeat.
GFC pieces.
ExplicitGridResolver produces the explicit grid's track lists, and ImplicitGrid builds the GridMatrix (rows by columns) used for item placement. AxisConstraint describes an axis as Definite (with available space), MinContent or MaxContent, optionally carrying the container's min and max sizes.
Types.
LayoutUnit is WebKit's fixed-point layout type, with 1/64px precision and saturating arithmetic. WTF Vector::reserveInitialCapacity allocates the backing buffer up front.
Analysis
No vulnerability is fixed in this diff. This is a security-adjacent feature addition, so the question is which invariants the new code assumes rather than enforces. Before the change, GFC used gridTemplateList.sizes as is, so the explicit grid size was bounded by the length of the author's track list. Afterward it is a quotient of two author-controlled lengths multiplied by the repeat list's length.
Before: tracks = declared list (length fixed by the author's list)
After: space = container - nonRepeated + gap
rep = space / max(1px, sum(repeat)) <- floor on the divisor only
tracks = rep * repeatList.size()
└─► reserveInitialCapacity ─► GridMatrix sizing ─► per-track loops
^ no ceiling on rep
Three assumptions matter.
- Unbounded repetition count. A repeat list of zero-width tracks with zero gap gives a 1px divisor. The container size is bounded only by the saturating
LayoutUnitrange (1/64px fixed point, maximum around 2^25 px), so the count can reach tens of millions.trackSizesWithAutoRepetitions()multiplies it byautoRepeatSizes.size()forreserveInitialCapacity(), and downstreamImplicitGrid'sGridMatrix(totalRowsCount, GridRow(totalColumnsCount))is sized from the explicit column count. The likely result is very large allocations and long loops, i.e. resource exhaustion. Whether it ends as a controlled crash (WTF'sVectoris known toCRASH()on capacity overflow) or something worse depends on latersize_tindex arithmetic. The legacyRenderGridpath is believed to clamp auto-repeat counts against a maximum grid position (clampAutoRepeatTracks/GridPosition::max()), but the suppliedRenderGrid.cppexcerpt cuts off before that code, so a dropped clamp in GFC is an inference. - Optional dereferenced on a grammar assumption.
nonRepeatedTracksSpace()dereferences*trackSizeForCountunchecked, relying on the style parser to accept only<fixed-size>tracks beside an auto-repeat. If any non-parser path (CSSOM, interpolation, computed-value conversion) could produce a list where a non-repeated track has no definite breadth, this would dereference an emptystd::optional: a trap under libc++ hardening, undefined behavior without it. - Insertion point checked only in debug builds.
autoRepeatInsertionPointis guarded by anASSERTbeforespan().first()andsubspan(); in release the only safety is hardenedstd::spanbounds checking.
Whether web content reaches this code depends on whether GFC integration is enabled and LayoutIntegrationGridCoverage admits auto-fill templates. The supplied coverage excerpt is truncated before that logic. The bug class is an unbounded, author-controlled count driving allocation. Beyond exhaustion, the possibilities are size_t-to-unsigned or int truncation in downstream track-index arithmetic, which would need a narrowing site that is not visible in the supplied context.
No trust boundary is directly weakened. The most plausible impact is renderer availability: if GFC is enabled and coverage admits auto-fill templates, author CSS could drive the explicit track count to tens of millions and end in a controlled crash or a hang in the WebContent process.
Insight: layout-engine rewrites tend to drop defensive limits that the old code enforced but the spec never mentions. A cap on auto-repeat tracks is a resource-exhaustion fix rather than a spec requirement, so an implementation that follows the spec text closely will not reproduce it. The spec's "floor the divisor at 1px" rule prevents division by zero, but it also fixes the maximum quotient at container-size / 1px.
Audit directions
- Uncapped "how many fit" quotients. A count computed as one attacker-controlled size divided by another, where the divisor is floored but the quotient is never capped, is then used as an allocation size or loop bound. Narrow: follow the
size_treturned byAutoRepeatResolver::resolveRepetitions()intoImplicitGrid::createInitialGrid,GridItemPlacer::placeItemsand the GFC track-sizing loops, and check whether any of them stores it inunsignedorint. A match is a track count narrowed without a clamp. Wider: other CSS tiling features, namelybackground-repeat: space/roundtile counts,border-image-repeat, multicolcolumn-countderived fromcolumn-width, and repeating gradients. The match is integer division of two author lengths whose result sizes aVectoror loop with nostd::minagainst a constant. Widest: any renderer computing tile or repeat counts from untrusted geometry (PDF tiling patterns, SVG<pattern>, Skia or Cairo tiled shaders, spreadsheet fill ranges). The question to carry: is the divisor floor matched by a ceiling on the result? - Reimplementations that drop legacy clamps. A spec-following rewrite omits engine-specific defensive limits the legacy code had. Narrow: diff GFC against
RenderGridandGridTrackSizingAlgorithmfor everystd::minor clamp againstGridPosition::max()-style constants (auto-repeat clamping, span limits, implicit-grid growth), and confirm each has an equivalent inExplicitGridResolver,ImplicitGridorGridItemPlacer. A match is a legacy clamp with no counterpart in GFC. Wider: the same parity audit for the other LFC formatting contexts against their legacy counterparts, flex formatting context vsRenderFlexibleBoxand inline formatting context vs legacy line layout. Widest: any engine rewrite running alongside the old one (Blink LayoutNG vs legacy layout, Servo/Stylo vs Gecko layout, Rust ports of C parsers). A match is a limit in the old code that cites no spec section and was quietly not ported. - Unwraps justified by another module's grammar. A downstream consumer dereferences an optional or indexes a span based on a guarantee enforced elsewhere. Narrow: grep
Source/WebCore/layout/formattingContexts/grid/for unchecked*optionaldereferences andspan().first/subspancalls whose only guard is a comment or a debugASSERT, such as*trackSizeForCountandautoRepeatInsertionPoint. Wider: check whether style values can reach layout through paths other than the parser, such as CSSOM setters, animation interpolation ofgrid-template-*, or computed-value conversion. Any of these could produce a template list the grammar would reject. Widest: any layered system where a back end trusts a front-end validator, such as compiler middle ends trusting parser-checked ASTs or protocol handlers trusting schema validation. In review, an unchecked unwrap whose justification refers to another component's rules is the tell.