← All reports

[3] GFC auto-fill repeat count has no upper clamp before allocation

LowWebCore Layout Grid Formatting ContextOther

ec935b6

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. ExplicitGridResolver checks for auto-fill tracks in the track list and uses the new AutoRepeatResolver to compute how many repetitions come from them. AutoRepeatResolver implements 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. ExplicitGridResolver then 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

+ autoRepeatTrackSizeSum = std::max(1_lu, autoRepeatTrackSizeSum);
+ auto sizeOfRepetition = autoRepeatTrackSizeSum + usedGap * autoRepeatTrackSizes.size();
+ auto spaceForRepetitions = containerSizeForAutoRepeat - nonRepeatedTracksSpace + usedGap;
+ if (spaceForRepetitions <= sizeOfRepetition)
+ return 1;
+
+ size_t repetitions = (spaceForRepetitions / sizeOfRepetition).toUnsigned();
+ if (repeatStrategy == RepeatStrategy::SmallestFulfillingSize && spaceForRepetitions > sizeOfRepetition * repetitions)
+ ++repetitions;
+ return repetitions;
...
+ // <auto-track-list> only allows <fixed-size> tracks alongside the auto-repeat, which always have a definite size.
+ auto trackSizeForCount = trackSizeForRepetitions(trackSize, containerSizeForAutoRepeat, zoom);
+ nonRepeatedTracksSpaceSum += *trackSizeForCount + usedGap;

Source/WebCore/layout/formattingContexts/grid/ExplicitGridResolver.cpp

+ ASSERT(insertionPoint <= gridTemplateListSizes.size());
+ Vector<Style::GridTrackSize> trackSizes;
+ trackSizes.reserveInitialCapacity(gridTemplateListSizes.size() + repetitionCount * autoRepeatSizes.size());
+ trackSizes.append(gridTemplateListSizes.span().first(insertionPoint));
+ for ([[maybe_unused]] auto repetition : std::views::iota(0uz, repetitionCount))
+ trackSizes.append(autoRepeatSizes.span());
+ trackSizes.append(gridTemplateListSizes.span().subspan(insertionPoint));

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.

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.

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.

  1. 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 LayoutUnit range (1/64px fixed point, maximum around 2^25 px), so the count can reach tens of millions. trackSizesWithAutoRepetitions() multiplies it by autoRepeatSizes.size() for reserveInitialCapacity(), and downstream ImplicitGrid's GridMatrix(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's Vector is known to CRASH() on capacity overflow) or something worse depends on later size_t index arithmetic. The legacy RenderGrid path is believed to clamp auto-repeat counts against a maximum grid position (clampAutoRepeatTracks / GridPosition::max()), but the supplied RenderGrid.cpp excerpt cuts off before that code, so a dropped clamp in GFC is an inference.
  2. Optional dereferenced on a grammar assumption. nonRepeatedTracksSpace() dereferences *trackSizeForCount unchecked, 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 empty std::optional: a trap under libc++ hardening, undefined behavior without it.
  3. Insertion point checked only in debug builds. autoRepeatInsertionPoint is guarded by an ASSERT before span().first() and subspan(); in release the only safety is hardened std::span bounds 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.