← All reports

[2] WebViewImpl: re-entrant IPC frees the modal context-menu presenter

MediumWebKit UIProcess (macOS)UAF

A modal context menu that never stopped listening to the web process.

d089fe1

Medium — content process에서 도달 가능한 UI-process use-after-free이지만, 이를 유발하려면 WebContent 측이 적절한 시점에 두 번째 data-detection 결과를 이미 전송할 수 있는 상태여야 합니다. 또한 해제되는 대상은 범용 heap primitive가 아니라 UI helper 객체입니다. 이 vulnerability를 단순한 robustness crash 이상으로 끌어올리는 요인은, 그 대상이 sandbox 경계의 어느 쪽에 위치하는가입니다.

WebKit UI process 안의 Objective-C 객체들은 대개 WebViewImplWKWebView마다 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 reference m_revealItemPresenter in WebViewImpl. During the modal popup, this reference may be cleared by a new data detection click arriving via IPC, which overwrites m_revealItemPresenter with a newly allocated presenter, releasing the old one.

Use protect(m_revealItemPresenter) to retain the presenter for the duration of the showContextMenu call.

Source/WebKit/UIProcess/mac/WebViewImpl.mm

m_revealItemPresenter = adoptNS([[WKRevealItemPresenter alloc] initWithWebViewImpl:*this item:adoptNS([PAL::allocRVItemInstance() initWithDDResult:info.result.get()]).get() frame:info.elementBounds menuLocation:clickLocation]);
[m_revealItemPresenter setShouldUseDefaultHighlight:NO];
- [m_revealItemPresenter showContextMenu];
+ [protect(m_revealItemPresenter) showContextMenu];

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가 해제되는 패턴.

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입니다. WebViewImpladoptNS를 통해 할당된 단일 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이 존재하는 동안 살아있다"로 바꿔주는 역할을 합니다.

이 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로 바꿔버리기 때문입니다.