[24] [grid layout] Don't call viewportContentsChanged() from scroll updates during render tree layout
diff에서 isInRenderTreeLayout()이 true인 상태로 scrollTo()가 viewportContentsChanged()를 호출했음이 확인됩니다. debug 빌드에서는 assertion이 유발되고, release 빌드에서는 부분적으로 초기화된 grid track 상태가 읽힙니다. 손상된 상태는 memory-copy primitive가 아닌 layout 연산으로 흘러 들어가므로, 관찰 가능한 가장 현실적인 영향은 공격자가 조작한 HTML/CSS에 의한 renderer crash 재현입니다.
RenderLayerScrollableArea::scrollTo()가 LocalFrameView::viewportContentsChanged()를 호출합니다. 이 함수는 renderer geometry를 조회하여 visibility rect를 계산합니다. layout 도중(grid pre-layout 중 updateScrollInfoAfterLayout을 통한 경우 등) 호출되면 containing block이 아직 완료되지 않은 상태입니다. 이 상태에서 absolutely positioned iframe의 percentage padding을 아직 채워지지 않은 grid area에 대해 해석하려 하면 gridAreaRangeForOutOfFlow에서 assertion 실패가 발생합니다. 이 호출은 isInRenderTreeLayout()으로 보호되었습니다.
Source/WebCore/rendering/RenderLayerScrollableArea.cpp
Layout-phase invariant 위반: layout 진행 중 발생한 scroll update에서 post-layout visibility-rect 조회가 호출되어, containing block이 layout을 완료하기 전에 renderer geometry를 읽는 패턴.
Patch Details
scrollTo() 안의 viewportContentsChanged() 호출에 !layoutContext().isInRenderTreeLayout() 조건이 추가되었습니다. 이 호출 지연은 안전합니다. performPostLayoutTasks()가 매 layout 완료 후 무조건적으로 viewportContentsChanged()를 이미 호출하고 있기 때문입니다. regression test에서는 percentage padding-left를 가진 absolutely positioned <iframe>을 CSS grid container 안에 중첩합니다. 이 container는 scaled descendant를 가진 vertical-rl 스크롤 가능한 자식을 포함합니다.
Background
Render tree layout은 단계적으로 진행됩니다. engine은 top-down 방식으로 box geometry를 계산하며, 일부 helper는 geometry를 읽기 전에 layout이 완료된 상태를 전제로 합니다. LocalFrameView::layoutContext().isInRenderTreeLayout()은 layout pass 중에 true를 반환합니다. viewportContentsChanged()는 scroll 이후의 visibility를 계산하기 위해 renderer tree를 순회하는 함수로, 일반적으로 layout 완료 후 performPostLayoutTasks()에서 호출됩니다. CSS grid는 item 배치 전 pre-layout pass를 통해 track 크기를 결정합니다. out-of-flow grid item은 gridAreaRangeForOutOfFlow를 통해 containing block을 해석하며, 이 과정에서 track range를 읽습니다. percentage padding은 containing block의 inline size를 기준으로 해석됩니다.
Analysis
패치 이전 scrollTo()는 viewportContentsChanged()를 무조건적으로 호출했습니다. grid pre-layout 중 updateScrollInfoAfterLayout을 통해 이 경로에 도달하면 containing block이 완료되지 않아 geometry가 부분적으로 구성된 상태입니다. 이 테스트 구성에서는 gridAreaRangeForOutOfFlow가 track이 채워지지 않은 grid area를 대상으로 percentage padding-left를 해석하려 합니다. 결과적으로 debug 빌드에서는 assertion이 유발되고, release 빌드에서는 초기화되지 않은 grid track 상태가 읽힙니다.
트리거는 HTML/CSS만으로 구성할 수 있습니다. display: grid container 안에 grid pre-layout 중 scroll offset이 갱신되는 스크롤 가능한 inner box가 필요합니다(regression test에서는 scaled descendant를 가진 vertical-rl overflow:scroll 사용). 여기에 더해, track이 채워지기 전에 gridAreaRangeForOutOfFlow 해석을 강제하는 percentage padding을 가진 absolutely positioned grid item이 있어야 합니다. layout 도중 발생한 scrollTo가 viewportContentsChanged()를 호출하고, 이 함수가 renderer geometry를 재귀적으로 조회하면서 완료되지 않은 grid area를 참조하게 됩니다.
release 빌드에서 가장 현실적으로 관찰 가능한 영향은 percentage padding 해석에 사용되는 grid track range 상태의 초기화되지 않은 읽기입니다. 해당 값의 소비처는 layout 연산이며 JS에서 접근 가능한 buffer로의 복사가 아니므로, write primitive는 드러나지 않습니다. 최소한으로 관찰 가능한 결과는 신뢰할 수 없는 CSS에 의한 renderer crash입니다. 이는 updateScrollInfoAfterLayout 내의 scroll offset 조정이 layout 완료를 전제로 하는 helper를 재진입하는 반복적인 문제 유형에 해당합니다.
이 취약점은 trust boundary가 아닌 renderer process의 안정성을 약화시키는 수준입니다.
Audit directions
- render tree layout 도중 발생하는 scroll offset 조정에서 호출되는 post-layout helper.
LocalFrameView::viewportContentsChanged()와applyRecursivelyWithVisibleRect의 모든 호출 지점에서isInRenderTreeLayout()이 true인 동안 해당 지점에 도달 가능한지 점검합니다.RenderLayerScrollableArea,LocalFrameView,ScrollView의 모든scrollTo/setScrollPositionoverride를 시작점으로 삼고, grid, flexbox, multicol layout 코드에서updateScrollInfoAfterLayout호출 지점을 추적합니다. - track이 채워지기 전에 grid track 상태에 대해 해석되는 out-of-flow grid item. pre-layout 중 진입 가능한 경로를 확인하기 위해
gridAreaRangeForOutOfFlow와RenderGrid내 해당 함수의 호출 지점을 점검합니다. 특히 absolutely positioned grid item의 percentage-resolved 속성에 주목합니다. - initial layout 도중 scroll offset을 변경시키는 writing-mode와
transform: scale간의 상호작용. 자식의 intrinsic geometry 피드백이 부모의 scroll position을 layout 도중 변경할 수 있는 다른 layout 경로를 살펴봅니다.RenderLayer::updateScrollInfoAfterLayout과RenderBoxscroll origin 재계산을 검토합니다. - 보호된 행 위에서 여전히 무조건 호출되는
didChangeScrollOffset()이 layout 완료를 요구하는 renderer geometry 조회로 재귀적으로 진입하는지 여부. visibility-rect 또는 geometry 순회 경로를 확인하기 위해LocalFrameView::didChangeScrollOffset을 추적합니다.