f9d51570c8ac619668a15fcaa1ca0ef62bddb58a [M154] Fix UAF in TouchEventAckQueue::ProcessAckedTouchEvents
The bug at a glance
A use-after-free in the browser process (components/input RenderWidgetHostInputEventRouter), reachable through ordinary multi-touch input dispatch. During synchronous touch-ack gesture dispatch the WebContents and its RenderWidgetHostInputEventRouter (owner of the TouchEventAckQueue) can be destroyed; the queue’s while loop then keeps reading the freed std::deque ack_queue_ and the freed client_. Browser-process UAFs are high-value for exploitation and sandbox escape, raising severity. It is not critical because triggering requires the specific synchronous-destruction timing (multiple queued acked events plus a dispatch path that tears down the router mid-loop), which constrains reliability.
ProcessAckedTouchEvents iterates ack_queue_ while dispatching acks. Dispatch (HandleTouchEventAck in the touch emulator, and ProcessAckedTouchEvent on the root view) can synchronously destroy the owning router. The original liveness check (!weak_client) sat at the TOP of the loop body, so after a destruction mid-body the code continued to touch ack_queue_ and client_ before the next iteration’s check — a use-after-free window.
Root cause
TouchEventAckQueue is owned by RenderWidgetHostInputEventRouter and holds a back-pointer client_ plus the std::deque ack_queue_. ProcessAckedTouchEvents loops while the front event is acked. Inside the body it calls weak_touch_emulator->HandleTouchEventAck(…) and root_view->ProcessAckedTouchEvent(…), either of which can synchronously destroy the WebContents and thus the router (and the TouchEventAckQueue, freeing ack_queue_ and client_). The pre-patch code only checked weak_client at the start of the loop body, so after a synchronous teardown it still read ack_queue_.front()/client_ for the remainder of that iteration and into the loop condition. The fix moves/adds weak_client liveness checks immediately after BOTH destruction points (after HandleTouchEventAck and after ProcessAckedTouchEvent) and bails out before any further access, and hardens the member client_ from raw_ptr to const raw_ref (constructed via CHECK_DEREF).
Key code
Liveness (weak_client) is now re-checked immediately after each synchronous destruction point, bailing out before the loop re-reads the freed ack_queue_ / client_.
if (!weak_touch_emulator) ... weak_touch_emulator->HandleTouchEventAck(...);
+ if (!weak_client) {
+ return;
+ }
- if (!handled_by_emulator && weak_client) {
+ if (!handled_by_emulator) {
if (client_->IsViewInMap(root_view) || client_->ViewMapIsEmpty()) {
root_view->ProcessAckedTouchEvent(touch_event, ack_result);
+ if (!weak_client) {
+ return;
+ }
}
}
Patch walkthrough
components/input/render_widget_host_input_event_router.cc— Core fix. Removes the single top-of-loop !weak_client check and instead checks weak_client immediately after HandleTouchEventAck and again after root_view->ProcessAckedTouchEvent, returning early if the router (and therefore the TouchEventAckQueue/ack_queue_) was synchronously destroyed. Also changes member client_ from raw_ptr to const raw_ref<…>, constructed with CHECK_DEREF(client) (adds base/check_deref.h and base/memory/raw_ref.h), so a null client is caught at construction and the never-null invariant is explicit.content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc— Adds regression test AckedTouchEventsAfterRouterDestroyed: queues two touch events (touchstart id=1, touchend id=2), installs a callback so that when the first event is acked the delegate resets its input event router (simulating synchronous destruction), then triggers the ack. The queue must exit safely without reading the freed ack_queue_. Also adds a set_on_process_acked_touch_event_callback hook to the mock view.content/test/mock_render_widget_host_delegate.h— Adds ResetInputEventRouter() { rwh_input_event_router_.reset(); } so the test can deterministically destroy the router during ack dispatch.
Background
TouchEventAckQueue — Queue inside RenderWidgetHostInputEventRouter holding acked touch events (std::deque ack_queue_) and a back-pointer to its owning router (client_). ProcessAckedTouchEvents drains events whose ack status is TouchEventAcked.
weak_client — A WeakPtr to the router captured in ProcessAckedTouchEvents, used to detect that the router (and the queue it owns) has been synchronously destroyed during dispatch.
Synchronous destruction during ack dispatch — HandleTouchEventAck (touch emulator) and ProcessAckedTouchEvent (root view) can synchronously drive gesture handling that tears down the WebContents and its RenderWidgetHostInputEventRouter, freeing the TouchEventAckQueue mid-loop.
raw_ref vs raw_ptr — MiraclePtr types; raw_ref encodes a non-null invariant and, like raw_ptr, provides dangling-pointer protection. Converting client_ to const raw_ref with CHECK_DEREF makes the never-null contract explicit and removes mutable reseating.
Vulnerability window
- Introduction — ProcessAckedTouchEvents was written with only a top-of-loop liveness check, assuming the router could not be destroyed between that check and the end of the iteration (inferred).
- Discovery — Tracked as Bug 553918193 (merge/cherry-pick also 556543595); recognized as a UAF where multiple queued acked events let a mid-loop destruction leave later iterations reading freed ack_queue_.
- Fix — Original CL 8338168 by Kartar Singh (reviewed by Dave Tapuska) at main@{#1690508}; cherry-picked to M154 branch (8037) as this commit on 2026-09-03.
Proof of concept
Conceptual only. Reproduce the structure of the regression test: enqueue at least two touch events so ack_queue_ holds multiple acked entries, then arrange for dispatch of the first ack (via HandleTouchEventAck or root view ProcessAckedTouchEvent) to synchronously destroy the owning RenderWidgetHostInputEventRouter / WebContents. In the pre-patch code the loop then re-reads the freed ack_queue_ and client_. No weaponized exploit is provided; real-world triggering depends on a renderer/gesture path that tears down the WebContents during touch-ack dispatch.
Exploitation
- Trigger — Deliver a multi-touch sequence so TouchEventAckQueue holds multiple acked events, then cause the dispatch of the first ack to synchronously destroy the WebContents/router (e.g. a navigation, tab close, or gesture-driven teardown during ack). (inferred)
- Free — The router and its TouchEventAckQueue (ack_queue_, client_) are freed inside the first iteration’s dispatch call. (inferred)
- Use — The while loop continues and reads ack_queue_.front() and dereferences client_ on freed memory; with heap grooming this read/dereference of attacker-influenced freed memory is a UAF primitive in the browser process. (inferred)
Detection & hunting
For defenders and detection engineers:
- ASAN/MiraclePtr dangling-pointer reports in TouchEventAckQueue::ProcessAckedTouchEvents — Run touch-heavy fuzzing with ASAN or BackupRefPtr enabled and watch for use-after-free on ack_queue_ / client_ during synchronous WebContents destruction.
- Crash reports in RenderWidgetHostInputEventRouter touch-ack path — Correlate browser-process crashes referencing TouchEventAckQueue, ProcessAckedTouchEvents, or the input event router with concurrent navigation/teardown events.
Audit directions
- Loops that dispatch while holding container state in input routing — Review other input/gesture dispatch loops in components/input and content/browser that iterate a member container while calling out to views/emulators which can synchronously destroy the owner; ensure liveness is re-checked after EVERY reentrant call, not only at the top of the loop.
- raw_ptr back-pointers to owning objects — Find other raw_ptr members pointing to an owner that can be destroyed during reentrant dispatch; consider raw_ref/CHECK_DEREF and post-callback liveness checks as applied here.