[2] WebViewImpl: re-entrant IPC frees the modal context-menu presenter
A modal context menu that never stopped listening to the web process.
Medium — content process에서 도달 가능한 UI-process use-after-free이지만, 이를 유발하려면 WebContent 측이 적절한 시점에 두 번째 data-detection 결과를 이미 전송할 수 있는 상태여야 합니다. 또한 해제되는 대상은 범용 heap primitive가 아니라 UI helper 객체입니다. 이 vulnerability를 단순한 robustness crash 이상으로 끌어올리는 요인은, 그 대상이 sandbox 경계의 어느 쪽에 위치하는가입니다.
WebKit UI process 안의 Objective-C 객체들은 대개 WebViewImpl — WKWebView마다 macOS UI-process 상태를 보관하는 C++ 객체 — 의 C++ 멤버 하나에 의해서만 소유됩니다. 코드는 어떤 객체를 가리키는 멤버가 그 객체를 통해 이루어지는 호출보다 더 오래 살아남는다고 가정합니다. 이 가정은 heap이 아니라 stack에 대한 진술이며, 호출 도중 다른 무언가가 그 멤버를 재할당할 수 없는 동안에만 성립합니다. AppKit의 modal presentation은 바로 이 조건을 깨뜨립니다. modal context menu는 nested run loop를 돌리는데, 이 동안에도 UI process는 main thread에서 들어오는 IPC 메시지를 계속 dispatch합니다.
관전 포인트: 첫 번째 context menu가 열려 있는 동안 두 번째 data-detection click 결과를 전달할 수 있는 WebContent process는, Objective-C method가 아직 stack에 남아 있는 상태에서 그 presenter를 해제시킬 수 있습니다. 결과적으로 sandbox 경계의 권한 있는 쪽에서 use-after-free가 발생합니다.
From the commit message:
A crash occurs in
-[WKRevealItemPresenter showContextMenu]because the presenter is held by a single strong referencem_revealItemPresenterinWebViewImpl. During the modal popup, this reference may be cleared by a new data detection click arriving via IPC, which overwritesm_revealItemPresenterwith a newly allocated presenter, releasing the old one.Use
protect(m_revealItemPresenter)to retain the presenter for the duration of theshowContextMenucall.
Source/WebKit/UIProcess/mac/WebViewImpl.mm
Patch Details
WebKit::WebViewImpl::handleClickForDataDetectionResult에서 한 줄이 수정되었습니다. -showContextMenu를 호출하는 receiver가 멤버 m_revealItemPresenter에서 protect(m_revealItemPresenter)로 바뀌었는데, 이는 Objective-C 포인터에 대한 local strong reference를 만들어냅니다. 이 local retain은 modal 호출 전체 구간 동안 유지되다가 statement 종료 시점에 해제되므로, 호출이 진행 중인 동안 멤버가 재할당되더라도 presenter는 살아남습니다. 함수의 다른 부분 — adoptNS construction이나 setShouldUseDefaultHighlight: 호출 — 은 손대지 않았습니다.
nested modal run loop 도중 re-entrant IPC에 의해 sole-owner 멤버 포인터가 변경되어, 진행 중인 method 호출의 receiver가 해제되는 패턴.
Background
Where this lives.
macOS UI process는 브라우저 UI 셸과 WKWebView에 대한 모든 AppKit 상호작용을 담당하며, WebContent process는 렌더링을 수행하고 결과를 IPC로 돌려보냅니다. WebViewImpl은 macOS에서 UI-process 상태를 보관하는, WKWebView마다 존재하는 C++ 객체입니다.
Data-detection click handling.
WKRevealItemPresenter는 Apple의 Reveal framework를 사용해 data-detection match — 전화번호, 주소, 일정 등 — 에 대한 context menu를 표시하는 Objective-C helper입니다. WebViewImpl은 adoptNS를 통해 할당된 단일 strong 멤버 m_revealItemPresenter로 이를 소유합니다. handleClickForDataDetectionResult는 WebContent process가 IPC로 data-detection click hit 결과를 돌려보낼 때 호출되는 handler입니다.
Modal presentation and nested run loops.
showContextMenu는 AppKit context menu를 표시하는데, 이는 modal 방식으로 동작합니다. AppKit은 caller의 run loop로 돌아가는 대신, 사용자가 메뉴를 닫을 때까지 nested run loop를 돌립니다. 이 nested loop가 실행되는 동안에도 UI-process main thread는 들어오는 IPC 메시지를 계속 dispatch합니다. 따라서 바깥쪽 showContextMenu 호출이 반환되기 전에, WebContent process로부터의 메시지가 UI-process IPC handler로 진입할 수 있습니다.
protect().
protect(...)는 smart-pointer 멤버로부터 local strong reference — 대개 RetainPtr<> temporary — 를 만들어내는 WebKit의 관용적 패턴입니다. 이 패턴은 re-entrant 가능한 호출을 위해 존재합니다. "객체는 멤버가 그것을 가리키는 동안 살아있다"는 보장을, 실제로 필요한 보장인 "객체는 이 stack frame이 존재하는 동안 살아있다"로 바꿔주는 역할을 합니다.
Analysis
이 vulnerability는 일반적인 의미의 lifetime 오계산이 아니라 re-entrancy에 의한 use-after-free입니다. 멤버의 소유자 자체는 올바르지만, 그 ownership이 살아있는 stack frame 아래에서 빠져나가 버립니다.
UI process main thread WebContent process
────────────────────────── ──────────────────
handleClickForDataDetectionResult(#1)
m_revealItemPresenter = P1
[P1 showContextMenu] ── spins nested AppKit run loop ──┐
│ send DD click result #2
(nested loop dispatches IPC) ◄──────────────────────────┘
handleClickForDataDetectionResult(#2)
m_revealItemPresenter = P2 ← releases P1, last reference
[P2 showContextMenu]
...
◄─ returns to P1's showContextMenu frame
self == P1, freed ← UAF
다이어그램 첫 번째 열의 stack frame은 P1에서 실행 중인 -[WKRevealItemPresenter showContextMenu]에 해당합니다. m_revealItemPresenter가 sole owner라는 점은 construction 지점에서 확인됩니다. 이 함수 안에서 다른 retainer가 만들어지는 부분 없이, 객체가 adoptNS로 바로 멤버에 할당되기 때문입니다. 따라서 re-entered handler에서의 재할당은 마지막 reference를 제거하고 P1을 deallocate시킵니다. 이후 제어 흐름이 P1의 method로 다시 돌아오면서 self — 이미 해제된 Objective-C 객체 — 를 건드리게 됩니다. nested release 이후 self에 대한 내부 접근이 더 일찍 일어나더라도 결과는 동일합니다.
이 trigger에는 두 가지 전제조건이 필요하며, 둘 다 이 함수 자체가 아니라 dispatch 모델의 특성에서 비롯됩니다. 먼저, WebContent로부터의 IPC 메시지가 nested modal loop이 활성화된 동안에도 main thread에서 dispatch되어야 합니다. 이 조건이 성립해야 handleClickForDataDetectionResult로의 re-entry 자체가 가능해집니다. 둘째, 첫 번째 메뉴가 열려 있는 동안 두 번째 data-detection click 결과가 전달 가능해야 합니다. compromise된 WebContent process는 이 메시지를 언제 보낼지 스스로 제어하므로, 이 조건은 content-level의 별다른 트릭이 아니라 modal window와의 timing 문제로 귀결됩니다.
protect() fix는 두 번째 조건 자체가 아니라 그 결과를 해결합니다. 멤버는 여전히 호출 도중 재할당될 수 있고, re-entered handler는 여전히 P2를 construct하고 present합니다. 달라지는 것은, stack이 멤버와 무관한 독립적인 reference를 보유하게 되면서 P1이 자신의 method가 끝날 때까지 살아남는다는 점입니다. 이 정도가 여기서는 적절한 대응입니다. re-entry 자체를 막으려면 modal presentation이 IPC dispatch와 상호작용하는 방식 전체를 재구성해야 하는데, 이는 이 crash가 요구하는 수준을 훨씬 넘어서는 변경입니다.
crash를 넘어선 exploitability는, release 시점과 이후의 self 접근 사이 window에서 해제된 WKRevealItemPresenter allocation에 무엇을 채워 넣을 수 있는가에 달려 있습니다. 이 window는 re-entered handler 자신의 작업 — 객체 construction, setShouldUseDefaultHighlight:, 그리고 nested modal presentation — 으로 한정되는데, 이는 attacker의 영향을 받는 상당한 양의 allocation 활동에 해당합니다. 두 번째 data-detection 결과의 내용이 P2가 무엇을 allocate할지를 좌우하기 때문입니다. 이를 controlled reclaim으로 발전시키는 것은 예상되는 방향일 뿐입니다. 이번 diff가 확립하는 것은 dangling-self primitive이며, grooming 자체는 아닙니다.
이 vulnerability는 UI process 내부 — WebContent sandbox 경계의 권한 있는 쪽 — 의 memory safety를 약화시킵니다. security model은 WebViewImpl 안의 strong-reference 멤버가 stack 위에서 이루어지는 어떤 synchronous 사용보다도 오래 살아남는다고 가정하는데, nested modal run loop을 통해 IPC handler가 re-entry되면 이 가정이 깨집니다. 첫 번째 결과의 context menu가 표시된 동안 조작된 두 번째 data-detection click 결과를 보낼 수 있는 WebContent process는 UI process 안에서 UAF를 얻게 되며, 이는 확장될 경우 sandbox escape로 향하는 하나의 경로가 될 수 있습니다.
Takeaway: nested run loop을 돌릴 수 있는 blockingMethod에 대해 [m_member blockingMethod] 형태로 호출하는 모든 곳에는 protect(m_member)가 필요합니다. 멤버에 의한 sole ownership은 heap에 관한 사실인데, nested run loop은 이를 그 멤버를 건드릴 수 있는 모든 IPC handler와의 race로 바꿔버리기 때문입니다.
Audit directions
- nested run loop을 돌리는 호출로 곧바로 dereference되는 sole-owner 멤버 smart pointer이면서, 동일한 멤버가 같은 thread의 IPC handler에서 재할당 가능한 경우입니다.
Source/WebKit/UIProcess/mac/WebViewImpl.mm과WebKit/UIProcess/ios/WKContentView*.mm에서,someBlockingMethod가 modal menu/alert/popover를 표시하거나-[NSApplication runModal...]스타일의 nested loop을 실행하는[m_someMember someBlockingMethod]형태를 찾아야 합니다.m_revealItemPresenter,m_immediateActionController,m_quickLookPreviewController,m_shareSheet등 유사한RetainPtr<>UI-helper 멤버들의 호출 지점을 검색하는 것부터 시작하는 것이 좋습니다. 리뷰 시, UI-helper 멤버로의 message send가[m_로 맨몸으로 시작하는 부분이 시각적으로 눈에 띄는 신호이며,protect(wrapper가 기본값이어야 하고 그것이 없다면 이유가 정당화되어야 합니다. - 이전에 진행 중인 작업을 먼저 protect하지 않은 채 UIProcess 멤버 상태를 변경하는 re-entrant IPC handler입니다.
WebViewImpl의 IPC entry point들(handleClickForDataDetectionResult, immediate-action handler, drag-and-drop, services lookup)에서(m_x = adoptNS(...new...);[m_x blockingCall]) 시퀀스를 점검해야 합니다. 위험한 조합은 (1) IPC로 촉발된 strong 멤버 할당, (2) run loop을 pump하는 method 안에서 그 멤버를 즉시 synchronous하게 사용, (3) local retain의 부재입니다.adoptNS(\[\[.*Presenter\|.*Controller\|.*Sheet alloc\]뒤 몇 줄 이내에[m_...가 뒤따르는 패턴을 검색하는 것이 유용합니다. - macOS UIProcess의 AppKit modal 호출이 WebKit IPC dispatch를 re-entrant 상태로 빠뜨리는 경우입니다.
-[NSMenu popUpMenuPositioningItem:...]/-[RVPresenter ...]가 실행되는 동안 어떤 IPC 메시지가 여전히 main thread에서 dispatch되는지 확인하고, 그중 어떤 메시지가 다른 UI-helper 멤버 포인터를 변경하는지 식별해야 합니다.WebPageProxy/WebViewImpl의 message-receiver 테이블을 대조하여,RetainPtr<>멤버를 건드리는 handler들을 확인하는 것이 도움이 됩니다. - 오직 C++ 멤버에 의해서만 소유되는 Objective-C
self가 nested run loop을 거치며 갖는 lifetime입니다.WKRevealItemPresenter,WKImmediateActionController,WKQuickLookPreviewController,WKShareSheet, 그리고 유사한WK*presenter/controller 클래스들을 살펴보고, 내부적으로 run loop을 돌리면서self가 오직 바깥쪽 C++ owner에 의해서만 살아있다고 가정하는 instance method가 있는지 확인해야 합니다. 해당 클래스 스스로가[retain] self를 그 구간 동안 보장할 수 없다면,WebViewImpl의 모든 호출 지점에protect()스타일의 커버리지가 필요합니다.