← 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 — a UI-process use-after-free reachable from the content process, but reaching it requires the WebContent side to already be able to send a second data-detection result at the right moment, and the freed object is a UI helper rather than a general-purpose heap primitive. What lifts it above a robustness crash is which side of the sandbox boundary it lands on.

Objective-C objects in WebKit's UI process are frequently owned by exactly one C++ member on a WebViewImpl — the per-WKWebView object holding macOS UI-process state — and the code assumes that a member holding an object outlives any call made through it. That assumption is a statement about the stack, not about the heap, and it holds only while nothing else can reassign the member mid-call. AppKit modal presentations break that condition: a modal context menu spins a nested run loop, and the UI process keeps dispatching incoming IPC messages on the main thread while it spins.

The angle: a WebContent process that can deliver a second data-detection click result while the first one's context menu is open frees the presenter whose Objective-C method is still on the stack, yielding a use-after-free on the privileged side of the sandbox boundary.

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];

One line in WebKit::WebViewImpl::handleClickForDataDetectionResult. The receiver of -showContextMenu changes from the member m_revealItemPresenter to protect(m_revealItemPresenter), which materialises a local strong reference to the Objective-C pointer. The local retain is held across the whole modal invocation and released at statement end, so the presenter survives any reassignment of the member that happens while the call is in flight. Nothing else in the function — the adoptNS construction, the setShouldUseDefaultHighlight: call — is touched.

Sole-owner member pointer mutated by re-entrant IPC during a nested modal run loop, freeing the receiver of the in-flight method call.

Where this lives. The macOS UI process hosts the browser UI shell and all AppKit interaction for a WKWebView; the WebContent process renders and sends results back over IPC. WebViewImpl is the per-WKWebView C++ object holding UI-process state on macOS.

Data-detection click handling. WKRevealItemPresenter is an Objective-C helper that displays a context menu for a data-detection match — a phone number, address, calendar event — using Apple's Reveal framework. WebViewImpl owns it through a single strong member m_revealItemPresenter, assigned via adoptNS. handleClickForDataDetectionResult is the handler invoked when the WebContent process sends back a data-detection click hit result over IPC.

Modal presentation and nested run loops. showContextMenu displays an AppKit context menu, which is modal: AppKit spins a nested run loop until the user dismisses the menu, rather than returning to the caller's run loop. While that nested loop runs, the UI-process main thread continues to dispatch incoming IPC messages, so a message from the WebContent process can enter a UI-process IPC handler before the outer showContextMenu call has returned.

protect(). protect(...) is a WebKit idiom that materialises a local strong reference — typically a RetainPtr<> temporary — from a smart-pointer member. The idiom exists specifically for calls that can re-enter: it converts "the object lives as long as the member points at it" into "the object lives as long as this stack frame", which is the guarantee the call actually needs.

This is a use-after-free driven by re-entrancy, not by a lifetime miscalculation in the ordinary sense — the member's owner is correct, but the ownership is transferred out from under a live 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

The stack frame in the diagram's first column belongs to -[WKRevealItemPresenter showContextMenu] running on P1. Because m_revealItemPresenter is the sole owner (verified from the construction site: the object is adoptNS-allocated straight into the member, with no other retainer established in this function), the reassignment in the re-entered handler drops the last reference and deallocates P1. Control eventually unwinds back into P1's method, which touches self — a freed Objective-C object. Any inner access to self after a nested release has the same effect earlier.

Two preconditions gate the trigger, and both are properties of the dispatch model rather than of this function. First, IPC messages from WebContent must be dispatched on the main thread while the nested modal loop is active — this is what makes re-entry into handleClickForDataDetectionResult possible at all. Second, a second data-detection click result must be deliverable while the first menu is open; a compromised WebContent process controls when it sends these, so this reduces to timing the second message against the modal window rather than to any content-level trick.

The protect() fix addresses the second condition's consequence rather than the condition itself: the member can still be reassigned mid-call, and the re-entered handler still constructs and presents P2. What changes is that P1 survives to the end of its own method because the stack holds a reference independent of the member. That is the correct shape here — preventing the re-entry outright would require restructuring how modal presentation interacts with IPC dispatch, which is a far larger change than the crash warrants.

Exploitability beyond the crash depends on what can be placed into the freed WKRevealItemPresenter allocation between the release and the subsequent self access. The window is bounded by the re-entered handler's own work — object construction, setShouldUseDefaultHighlight:, and the nested modal presentation — which is a substantial amount of allocation activity under the attacker's influence, since the second data-detection result's contents drive what P2 allocates. Turning that into a controlled reclaim is a projected direction; the diff establishes the dangling-self primitive, not the grooming.

This vulnerability weakens memory safety inside the UI process — the privileged side of the WebContent sandbox boundary. The security model assumes strong-reference members in WebViewImpl outlive any synchronous use on the stack; that assumption fails when an IPC handler is re-entered through a nested modal run loop. A WebContent process able to send a crafted second data-detection click result while the first is showing its context menu gets a UAF in the UI process, an avenue toward sandbox escape if escalated.

Takeaway: any [m_member blockingMethod] where blockingMethod can spin a nested run loop needs protect(m_member) — sole ownership by a member is a heap fact, and a nested run loop turns it into a race with every IPC handler that can touch that member.