[3] Use-after-free in LocalFrameView::scrollToAnchorFragment via NavigateEvent finish
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
LayoutTests/navigation-api/resources/navigation-api-fragment-intercept-crash-inner.html
Patch Details
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 항목이 UncheckedCallArgsCheckerExpectations와 UncountedCallArgsCheckerExpectations 양쪽에서 모두 제거되었습니다. 이는 해당 파일이 이제 SaferCPP checker를 통과함을 나타내며, 기존에 지적되었던 uncounted call-argument가 정확히 이 raw view() receiver였음이 확인됩니다.
동기 script dispatch (beforematch) re-entrancy 경계를 넘는 동안 this receiver에 strong reference를 보유하지 않은 패턴.
Background
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를 보유해야 합니다.
Analysis
event-dispatch 경계를 가로지르는 JS re-entrancy로 인한 use-after-free 취약점입니다. 수정 전 processScrollBehavior는 대상 LocalFrameView를 bare LocalFrameView*로 가져와 scrollToFragment() / setScrollPosition()을 호출했으며, Document만 ref-protected 상태였습니다. view receiver — 해당 메서드 내부의 this — 는 보호 대상이 아니었습니다. LocalFrameView::scrollToFragment → scrollToAnchorFragment 경로에서 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를 약화시키는 결과를 낳습니다. 보안 모델은 메서드가 호출되는 동안 해당 객체가 살아있다는 전제에 기반합니다. 수정 전에는 scrollToFragment의 LocalFrameView 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에서 직접 확인됩니다.
Audit directions
- raw pointer로 얻은 frame 소유 객체에 대한 멤버 호출 — callee가 동기적으로 frame을 detach할 수 있는 경우. Navigation API interception 및 fragment navigation에서 도달 가능한 호출 지점 중, view를 먼저
protect()/Ref하지 않고LocalFrameViewscroll/anchor 진입점(scrollToFragment,scrollToAnchorFragment,scrollToAnchor,setScrollPosition)을 호출하는 경우를 점검해야 합니다. - layout과 scrolling 중 숨겨진 re-entrancy 지점으로서의
hidden=until-found/beforematch. WebCore에서beforematchdispatch와DisplayLockContext/content-visibility reveal 경로를 검색하고, dispatch를 넘어 reference를 보유하는 호출자를 추적해야 합니다. dispatch 이후에도 frame/view/renderer 상태를 건드리는 경우는 모두 후보가 됩니다. UncountedCallArgsCheckerExpectations/UncheckedCallArgsCheckerExpectations에 남아 있는 SaferCPP 항목.page/와loader/에서 ref 보호 없이frame()->view()또는frame()->loader()receiver에 메서드를 호출하는 파일을 검토해야 합니다. 이번 수정은 해당 목록이 정확히 이 UAF 취약 패턴을 지적하고 있음을 확인해줍니다.- raw frame/view/document pointer를 사용하는 다른 Navigation API 처리 단계. focus reset,
potentiallyProcessScrollBehavior, handler promise resolution이 script가 관찰 가능한 경계를 넘어 raw pointer를 사용하는지 확인해야 합니다. 또한 처리 중 frame detachment가 발생했을 때 해제된 객체를 역참조하는 경우가 남아 있는지도 점검이 필요합니다.