[3] GFC auto-fill repeat count has no upper clamp before allocation
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
Source/WebCore/layout/formattingContexts/grid/ExplicitGridResolver.cpp
Patch Details
새로 추가된 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 범위를 결정하는 패턴.
Background
이 코드가 있는 위치.
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를 미리 할당합니다.
Analysis
이번 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
여기서 중요한 전제는 세 가지입니다.
- 상한 없는 반복 횟수. 폭이 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_tindex 연산에 달려 있습니다. 참고로 WTFVector는 capacity overflow 시CRASH()하는 것으로 알려져 있습니다. 한편 legacyRenderGrid경로는 auto-repeat 횟수를 최대 grid position(clampAutoRepeatTracks/GridPosition::max())에 맞춰 제한하는 것으로 보입니다. 다만 제공된RenderGrid.cpp발췌는 해당 코드 이전에서 끊겨 있습니다. 따라서 GFC에서 clamp가 빠졌다는 판단은 추정입니다. - 문법 전제에 기댄 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로 이어집니다. - Debug build에서만 검사되는 insertion point.
autoRepeatInsertionPoint는span().first()와subspan()호출 전에ASSERT로만 보호됩니다. release build에서 남는 안전장치는 hardenedstd::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로 고정하는 결과를 낳습니다.
Audit directions
- 상한 없는 "몇 개 들어가는가" 몫. attacker가 제어하는 크기를 다른 크기로 나눠 count를 계산하는 패턴입니다. 제수에는 하한이 있지만 몫에는 상한이 없고, 이 값이 allocation 크기나 loop 범위로 사용됩니다. 좁은 범위:
AutoRepeatResolver::resolveRepetitions()가 반환하는size_t를ImplicitGrid::createInitialGrid,GridItemPlacer::placeItems, GFC track-sizing loop까지 따라가 볼 필요가 있습니다. 그중 이 값을unsigned나int에 저장하는 곳이 있는지 확인해야 합니다. clamp 없이 narrowing된 track count가 발견되면 해당합니다. 넓은 범위: 다른 CSS tiling 기능도 대상입니다.background-repeat: space/round의 tile 수,border-image-repeat,column-width에서 도출되는 multicolcolumn-count, repeating gradient 등이 있습니다. 두 author length의 정수 나눗셈 결과가 상수와의std::min없이Vector크기나 loop 범위를 결정한다면 해당합니다. 가장 넓은 범위: 신뢰할 수 없는 geometry로 tile/repeat 수를 계산하는 모든 renderer가 대상입니다(PDF tiling pattern, SVG<pattern>, Skia나 Cairo의 tiled shader, spreadsheet fill range 등). 이때 던져야 할 질문은 하나입니다. 제수의 하한에 대응하는 결과값의 상한이 있는가? - legacy clamp를 누락한 재구현. spec을 충실히 따르는 재작성 과정에서, legacy 코드에 있던 engine 고유의 방어적 제한이 빠지는 패턴입니다. 좁은 범위: GFC를
RenderGrid,GridTrackSizingAlgorithm과 비교해GridPosition::max()계열 상수에 대한std::min이나 clamp를 모두 찾아야 합니다(auto-repeat clamp, span 제한, implicit grid 확장 등). 그리고 각각에 대응하는 코드가ExplicitGridResolver,ImplicitGrid,GridItemPlacer에 있는지 확인합니다. GFC에 대응 코드가 없는 legacy clamp가 발견되면 해당합니다. 넓은 범위: 다른 LFC formatting context도 legacy 구현과 같은 방식으로 비교할 수 있습니다. flex formatting context와RenderFlexibleBox, inline formatting context와 legacy line layout이 그 대상입니다. 가장 넓은 범위: 기존 구현과 병행 운영되는 모든 engine 재작성이 해당합니다(Blink LayoutNG와 legacy layout, Servo/Stylo와 Gecko layout, C parser의 Rust 포팅 등). 기존 코드에 있던 제한 중 spec 근거가 없고 별다른 언급 없이 이식되지 않은 것이 발견되면 해당합니다. - 다른 모듈의 문법에 기댄 unwrap. downstream 코드가 다른 곳에서 강제되는 보장을 근거로 optional을 역참조하거나 span을 인덱싱하는 패턴입니다. 좁은 범위:
Source/WebCore/layout/formattingContexts/grid/에서 검사 없는*optional역참조와span().first/subspan호출을 검색합니다. 그중 보호 장치가 주석이나 debugASSERT뿐인 곳을 찾아야 합니다.*trackSizeForCount와autoRepeatInsertionPoint가 그 예입니다. 넓은 범위: style 값이 parser 외의 경로로 layout에 도달할 수 있는지 확인합니다. CSSOM setter,grid-template-*의 animation interpolation, computed-value 변환 등이 대상입니다. 이 중 어느 경로든 문법상 거부되었을 template 목록을 만들어낼 가능성이 있습니다. 가장 넓은 범위: back end가 front-end validator를 신뢰하는 모든 계층형 시스템이 대상입니다. parser가 검증한 AST를 신뢰하는 compiler middle end, schema 검증을 신뢰하는 protocol handler 등이 여기에 속합니다. 리뷰 시에는 다른 component의 규칙을 근거로 삼는 검사 없는 unwrap이 신호가 됩니다.