← All issues

[3] Use-after-free in LocalFrameView::scrollToAnchorFragment via NavigateEvent finish

Severity: High | Component: WebCore Navigation API / frame scrolling | 7b74291

diff는 beforematch handler가 호출 도중 해제할 수 있는 LocalFrameView receiver에 strong reference를 추가합니다. 이로써 web-reachable UAF가 있었음이 확인되어 High로 평가됩니다. 다만 memory-corruption primitive로의 확장은 해제된 view slot 재사용에 달려 있으며, 이번 변경은 그 경로를 보여주지 않습니다.

LocalFrameView의 use-after-free를 수정하기 위해 smart pointer가 적용되었습니다. NavigateEvent::processScrollBehavior는 raw document.frame()->view() pointer를 통해 LocalFrameView 메서드를 호출하고 있었습니다. 수정 후에는 view receiver를 ref를 보유하는 smart pointer로 감싸, 호출이 진행되는 동안 객체가 살아있도록 보장합니다.

Source/WebCore/page/NavigateEvent.cpp

void NavigateEvent::processScrollBehavior(Document& document)
...
if (!document.url().hasFragmentIdentifier()) {
if (m_navigationType == NavigationNavigationType::Reload)
document.frame()->loader().history().restoreScrollPositionAndViewState();
else
- protect(document)->frame()->view()->setScrollPosition({ 0, 0 });
+ protect(protect(document)->frame()->view())->setScrollPosition({ 0, 0 });
return;
}
...
if (!document.haveStylesheetsLoaded())
document.setGotoAnchorNeededAfterStylesheetsLoad(true);
else
- protect(document)->frame()->view()->scrollToFragment(document.url());
+ protect(protect(document)->frame()->view())->scrollToFragment(document.url());

LayoutTests/navigation-api/resources/navigation-api-fragment-intercept-crash-inner.html

+<span id="target" hidden="until-found">trigger text</span>
+<script>
+document.addEventListener('beforematch', function (event) {
+ parent.document.querySelector('iframe').remove();
+}, {once: true});
+
+navigation.addEventListener('navigate', function (event) {
+ if (event.destination.url.includes('#target'));
+ event.intercept();
+});
+
+onload = () => {
+ setTimeout(() => {
+ navigation.navigate(location.href + '#target');
+ parent.scheduleFinish();
+ }, 0);
+}
+</script>

processScrollBehavior 내 두 곳의 호출 지점이 변경되었습니다. protect(document)->frame()->view()->setScrollPosition({ 0, 0 })protect(protect(document)->frame()->view())->setScrollPosition({ 0, 0 })으로 바뀌었고, scrollToFragment 호출도 동일한 방식으로 감싸졌습니다. 추가된 내부 protect(...)LocalFrameView*를 ref를 보유하는 smart pointer로 래핑하여, 호출 중에도 view가 살아있도록 합니다. 아울러 page/NavigateEvent.cpp 항목이 UncheckedCallArgsCheckerExpectationsUncountedCallArgsCheckerExpectations 양쪽에서 모두 제거되었습니다. 이는 해당 파일이 이제 SaferCPP checker를 통과함을 나타내며, 기존에 지적되었던 uncounted call-argument가 정확히 이 raw view() receiver였음이 확인됩니다.

동기 script dispatch (beforematch) re-entrancy 경계를 넘는 동안 this receiver에 strong reference를 보유하지 않은 패턴.

Navigation API는 navigate 이벤트를 통해 script가 navigation을 관찰하고 가로챌 수 있도록 합니다. event.intercept()를 호출하면 navigation이 앱에 위임되고, fragment navigation에서는 WebKit이 자체적인 scroll 동작 처리 단계(NavigateEvent::processScrollBehavior)를 거칩니다.

hidden="until-found"는 content-visibility를 활용하는 기능입니다. 이 방식으로 숨겨진 요소는 브라우저가 fragment-anchor 스크롤 등을 통해 화면에 표시해야 할 때 노출되면서, 동기적으로 beforematch 이벤트를 script에 발생시킵니다.

LocalFrameView는 frame이 소유하는 reference-counted view 객체로, frame이 detach될 때 파괴됩니다. iframe의 경우 <iframe>이 상위 document에서 제거되는 시점이 이에 해당합니다. 이 파일에서 protect(...)는 ref를 보유하는 smart pointer를 반환하며, 해당 표현식이 실행되는 동안 대상 객체를 살아있게 유지합니다. WebKit의 일반적인 모델에서, 마지막 reference를 해제할 수 있는 script를 동기적으로 실행할 가능성이 있는 경우 메서드는 this에 대한 strong reference를 보유해야 합니다.

event-dispatch 경계를 가로지르는 JS re-entrancy로 인한 use-after-free 취약점입니다. 수정 전 processScrollBehavior는 대상 LocalFrameView를 bare LocalFrameView*로 가져와 scrollToFragment() / setScrollPosition()을 호출했으며, Document만 ref-protected 상태였습니다. view receiver — 해당 메서드 내부의 this — 는 보호 대상이 아니었습니다. LocalFrameView::scrollToFragmentscrollToAnchorFragment 경로에서 hidden="until-found" anchor가 노출되고, 이때 beforematch 이벤트가 page script에 동기적으로 발생합니다. 다만 이 scroll/anchor 경로는 diff에 드러나지 않으므로, trigger 순서는 테스트에서 추론된 것입니다.

테스트의 beforematch handler는 상위 document에서 <iframe>을 제거합니다. 이로 인해 child frame이 detach되어 해제되고, 그 LocalFrameView도 함께 파괴됩니다. view가 진행 중인 메서드 호출의 un-refcounted receiver였으므로 this는 dangling pointer 상태가 됩니다. 이 상태에서 event dispatch 이후 scrollToAnchorFragment 내부 실행이 이어지면, 해제된 메모리에 대한 역참조가 발생합니다.

즉각적으로 관찰되는 결과는 crash 또는 ASAN abort입니다. memory-corruption primitive로의 확장은 제어된 heap 조건 하에서 해제된 view slot을 재사용하는 것에 달려 있습니다. 해제된 LocalFrameView slot을 grooming하면 이후 member access가 controlled read 또는 type-confused dereference로 이어질 가능성이 있습니다. primitive의 품질은 재사용 allocation에 대한 공격자의 제어 수준에 따라 달라집니다.

이 취약점은 WebContent process 내부의 memory safety를 약화시키는 결과를 낳습니다. 보안 모델은 메서드가 호출되는 동안 해당 객체가 살아있다는 전제에 기반합니다. 수정 전에는 scrollToFragmentLocalFrameView receiver에서 이 불변성이 깨졌는데, beforematch handler가 호출 도중 frame의 view를 파괴할 수 있었기 때문입니다. 다만 단독으로는 sandbox escape가 별도로 필요합니다.

이는 WebKit의 전형적인 "re-entrancy가 receiver를 해제하는" 패턴에 해당합니다. raw this 위에서 실행 중인 native 메서드가 동기적으로 이벤트를 dispatch하고, 그 이벤트가 this를 파괴하는 구조입니다. SaferCPP checker의 expectations 목록은 사실상 un-refcounted receiver를 전달하는 호출 지점들의 수정 대기 목록이라 볼 수 있습니다. NavigateEvent.cpp가 두 목록에서 모두 제거된 것은 수정 작업과 정적 분석 정책이 수렴하고 있음을 나타냅니다. hidden=until-found / beforematch는 layout/scroll 중 동기 script 실행이 일어나는 지점으로, re-entrancy 버그의 요소로서 간과하기 쉬운 부분입니다.

Note: scroll/anchor 경로 내 동기 beforematch dispatch, SaferCPP의 해당 항목, 그리고 iframe 제거 시 동기적인 LocalFrameView 파괴는 diff가 아닌 테스트와 주변 패턴에서 추론된 것입니다. receiver lifetime 수정 사항과 crash 조건은 patch에서 직접 확인됩니다.