← All reports

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

LowWebCore Layout Grid Formatting ContextOther

ec935b6

Low. 이번 commit에서 수정된 버그는 없습니다. author가 제어하는 두 길이의 몫으로 반복 횟수가 정해지는 기능 추가 작업이며, 이 몫에는 상한이 없습니다. 이론적으로 예상되는 최대 영향은 renderer의 메모리 또는 CPU 고갈입니다. 그마저도 auto-fill template을 가진 grid가 새 layout 경로로 들어오는 경우에 한합니다.

Layout engine은 반복되는 CSS 구문을 구체적인 목록으로 펼치며, 이때 목록의 길이는 container의 geometry에서 결정됩니다. WebKit은 legacy 구현과 별도로 Source/WebCore/layout/ 아래에 새로운 grid layout 구현인 Grid Formatting Context(GFC)를 개발하고 있습니다. 여기서는 ExplicitGridResolver가 grid-template-columns/rows를 explicit track 목록으로 변환합니다. repeat(auto-fill, ...)는 track 목록을 들어갈 수 있는 만큼 반복하도록 engine에 요청합니다. 이때 지켜져야 할 invariant는, 결과 track 수가 이후 allocation에 무리가 없을 만큼 작게 유지되어야 한다는 점입니다.

관전 포인트: auto-fill template을 가진 grid가 새 layout 경로에 도달한다고 가정할 수 있습니다. 이 경우 폭이 0인 repeat 목록, 0인 gap, 매우 큰 container를 조합한 author CSS로 explicit track 수를 수천만 단위까지 끌어올리는 것이 이론적으로 가능합니다. 결과적으로 renderer의 메모리나 CPU 고갈로 이어질 가능성이 있습니다.

auto-fill을 중심으로 auto-repeat track에 대한 기본 지원을 추가합니다. ExplicitGridResolver는 track 목록에 auto-fill track이 있는지 확인하고, 새로 추가된 AutoRepeatResolver로 반복 횟수를 계산합니다. AutoRepeatResolver는 spec 내용을 그대로 구현합니다. 반복 track이 채울 공간을 계산하고, 목록의 각 항목이 definite track size를 갖는지 확인한 뒤, 목록이 몇 번 반복되는지 산출합니다. 이후 ExplicitGridResolver가 style에서 지정한 insertion point에 반복분을 삽입합니다. 이를 위해 used gap size를 전달하는 코드 경로가 추가로 필요합니다.

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));

새로 추가된 AutoRepeatResolver.cpp에는 네 개의 함수가 구현되었습니다. 먼저 trackSizeForRepetitions()는 track의 definite size를 선택합니다. max sizing function이 definite이면 그 값을, 아니면 min 값을 사용하고, 둘 다 definite이면 두 값의 std::max를 취합니다. nonRepeatedTracksSpace()는 반복 구간 밖 track이 차지하는 공간에 track당 gap 하나를 더해 합산합니다. 이때 각 std::optional을 검사 없이 역참조합니다.

repetitionsToFill()은 반복 track 크기의 합을 구한 뒤, 그 합의 하한을 1_lu로 고정합니다. spec이 요구하는 division-by-zero 방지 장치에 해당합니다. 이어서 (spaceForRepetitions / sizeOfRepetition).toUnsigned()를 계산하고, SmallestFulfillingSize 전략에서는 반복을 하나 더 추가합니다. 마지막으로 AutoRepeatResolver::resolveRepetitions()는 채울 크기를 순서대로 고릅니다. definite available space가 있으면 그 값을, 다음으로 max(container max size, container min size)를, 그다음 container min size를 사용합니다. 어느 것도 해당하지 않으면 1을 반환합니다.

ExplicitGridResolver.cpp에는 trackSizesWithAutoRepetitions()가 새로 추가되었습니다. 이 함수는 reserveInitialCapacity(sizes.size() + repetitionCount * autoRepeatSizes.size())를 호출한 뒤, autoRepeatInsertionPoint 위치에 반복 횟수만큼 repeat 목록을 이어 붙입니다. 다만 이 insertion point는 debug ASSERT로만 검사됩니다. resolveTrackSizes()는 autoRepeatType == AutoRepeatType::Fill이고 repeat 목록이 비어 있지 않을 때 이 삽입을 수행합니다.

한편 ExplicitGridResolver::resolve()와 GridFormattingContext.cpp의 호출 지점들은 used column/row gap을 전달받도록 변경되었습니다. intrinsic sizing에서는 아직 min-width나 max-width를 전달하지 않는다는 FIXME도 함께 추가되었습니다. 반복 횟수를 최대 track 수에 맞춰 제한하는 코드는 어디에도 없습니다.

제수에만 하한이 있고 몫에는 상한이 없는 상태에서, attacker가 제어하는 두 크기의 나눗셈 결과가 allocation 크기와 loop 범위를 결정하는 패턴.

이 코드가 있는 위치. WebKit은 기존 render-tree layout과 별도로 Source/WebCore/layout/ 아래에 새로운 layout engine(LFC)을 구축하고 있습니다. GFC는 그중 grid를 담당하는 부분입니다. LayoutIntegrationGridCoverage는 grid마다 GFC와 legacy RenderGrid 중 어느 쪽이 layout을 처리할지 결정합니다. avoidance reason 중에는 GridFormattingContextIntegrationDisabled도 포함되어 있습니다.

repeat(auto-fill, ...). track 목록을 grid container에 들어갈 수 있는 만큼 반복합니다. spec은 free space에서 반복되지 않는 track(gap 포함)을 뺀 값을 반복 1회분의 크기로 나눕니다. 이때 division by zero를 막기 위해 UA가 정의한 제수 하한(권장값 1px)을 두도록 요구합니다. <auto-track-list> 문법은 auto-repeat 옆에 <fixed-size> track만 허용합니다.

GFC 구성 요소. ExplicitGridResolver는 explicit grid의 track 목록을 생성하고, ImplicitGrid는 item 배치에 쓰이는 GridMatrix(행 × 열)를 구성합니다. AxisConstraint는 하나의 축을 Definite(available space 포함), MinContent, MaxContent 중 하나로 나타냅니다. 필요에 따라 container의 min/max size도 함께 담습니다.

타입. LayoutUnit은 WebKit의 fixed-point layout 타입으로, 1/64px 정밀도와 saturating arithmetic을 갖습니다. WTF Vector::reserveInitialCapacity는 backing buffer를 미리 할당합니다.

이번 diff에서 수정된 vulnerability는 없습니다. 보안과 인접한 기능 추가이므로, 새 코드가 강제하지 않고 전제만 하는 invariant가 무엇인지가 핵심입니다. 변경 이전 GFC는 gridTemplateList.sizes를 그대로 사용했습니다. 따라서 explicit grid의 크기는 author가 작성한 track 목록 길이로 제한되었습니다. 변경 이후에는 author가 제어하는 두 길이의 몫에 repeat 목록의 길이를 곱한 값이 됩니다.

  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

여기서 중요한 전제는 세 가지입니다.

  1. 상한 없는 반복 횟수. 폭이 0인 track으로 이루어진 repeat 목록에 gap까지 0이면 제수는 1px이 됩니다. container 크기는 saturating LayoutUnit 범위(1/64px fixed point, 최대 약 2^25 px)로만 제한됩니다. 그래서 반복 횟수가 수천만 단위에 도달할 수 있습니다. trackSizesWithAutoRepetitions()는 이 값에 autoRepeatSizes.size()를 곱해 reserveInitialCapacity()에 전달합니다. 이후 ImplicitGrid의 GridMatrix(totalRowsCount, GridRow(totalColumnsCount))도 explicit column 수를 기준으로 크기가 정해집니다. 매우 큰 allocation과 긴 loop, 즉 resource exhaustion으로 이어질 가능성이 높습니다. 결말이 통제된 crash일지 더 심각한 결과일지는 이후의 size_t index 연산에 달려 있습니다. 참고로 WTF Vector는 capacity overflow 시 CRASH()하는 것으로 알려져 있습니다. 한편 legacy RenderGrid 경로는 auto-repeat 횟수를 최대 grid position(clampAutoRepeatTracks / GridPosition::max())에 맞춰 제한하는 것으로 보입니다. 다만 제공된 RenderGrid.cpp 발췌는 해당 코드 이전에서 끊겨 있습니다. 따라서 GFC에서 clamp가 빠졌다는 판단은 추정입니다.
  2. 문법 전제에 기댄 optional 역참조. nonRepeatedTracksSpace()는 *trackSizeForCount를 검사 없이 역참조합니다. style parser가 auto-repeat 옆에 <fixed-size> track만 허용한다는 점에 의존하는 셈입니다. parser를 거치지 않는 경로(CSSOM, interpolation, computed-value 변환)가 존재한다고 가정할 수 있습니다. 그 경로로 definite breadth가 없는 비반복 track이 포함된 목록이 만들어진다면, 빈 std::optional을 역참조하게 될 가능성이 있습니다. libc++ hardening이 적용되어 있으면 trap이 발생하고, 그렇지 않으면 undefined behavior로 이어집니다.
  3. Debug build에서만 검사되는 insertion point. autoRepeatInsertionPoint는 span().first()와 subspan() 호출 전에 ASSERT로만 보호됩니다. release build에서 남는 안전장치는 hardened std::span의 bounds check뿐입니다.

web content가 이 코드에 도달할 수 있는지는 두 가지 조건에 달려 있습니다. GFC integration이 활성화되어 있어야 하고, LayoutIntegrationGridCoverage가 auto-fill template을 허용해야 합니다. 다만 제공된 coverage 발췌는 해당 로직 이전에서 잘려 있습니다. 버그 유형으로 보면, author가 제어하는 상한 없는 count가 allocation을 결정하는 경우에 해당합니다. exhaustion 외의 가능성으로는 이후 track index 연산에서 size_t가 unsigned나 int로 truncation되는 경우를 들 수 있습니다. 이를 위해서는 narrowing이 일어나는 지점이 필요한데, 제공된 context에서는 그런 지점이 확인되지 않습니다.

직접적으로 약화되는 trust boundary는 없습니다. GFC가 활성화되어 있고 coverage가 auto-fill template을 허용한다고 가정할 수 있습니다. 이 경우 author CSS로 explicit track 수를 수천만까지 늘리는 것이 이론적으로 가능합니다. 결과적으로 WebContent process에서 통제된 crash나 hang으로 이어질 가능성이 있습니다. 즉 가장 가능성 있는 영향은 renderer availability 측면입니다.

Insight: layout engine을 새로 작성하는 과정에서는, 기존 코드가 강제하던 방어적 제한이 누락되는 경향이 있습니다. spec에 언급되지 않는 제한이라면 더욱 그렇습니다. auto-repeat track 수의 상한은 spec 요구사항이 아니라 resource exhaustion을 막기 위한 장치입니다. 그래서 spec 문구를 충실히 따르는 구현에서는 재현되지 않습니다. spec의 "제수 하한 1px" 규칙은 division by zero를 방지합니다. 하지만 동시에 몫의 최댓값을 container 크기 / 1px로 고정하는 결과를 낳습니다.