← Chrome — 2026-W41

2cc38f4761a2421d61e2c1e049016244832fa96f  Prevent dangling ScopedInputDispatchPin during fling teardown

severity high class UAF confidence 0.9 signal score 12.47 content/browser/ patch-now AI review CONFIRMED
Primitive: dangling raw_ptr to a destroyed RenderWidgetHostViewInput used during async touch-ack / GestureFlingCancel
Triage note: Converts AckData target_view/root_view from raw_ptr to WeakPtr and clears last_fling_start_target_ before UpdateQueueAfterTargetDestroyed() (which can flush acks and route a GestureFlingCancel), plus destroy_pending() guards. Browser-process UAF where an async-ACKed fling target view is destroyed before use; attacker-reachable via touch/gesture (Bug 570558164).
Structural signals: smartptr_member_added (+6.47), security_assert (+3.0), msg_medium (+2.0), area:content/browser/ (+1.0)

The bug at a glance

Use-after-free of a RenderWidgetHostViewInput in the browser process, reachable from unprivileged touch/gesture input (a fling) with no special permissions. Two independent dangling-pointer vectors are fixed: AckData raw_ptr members that can outlive an async touch-ack, and a last_fling_start_target_ raw pointer that is re-pinned by a synchronously-routed GestureFlingCancel during view teardown. Browser-process input routing runs for every page, the trigger (fling then navigate/destroy) is attacker-reachable, and the freed object is pinned/used via ScopedInputDispatchPin — High severity. Exploitability is moderated by the narrow timing window.

M156-era release branch (refs/branch-heads/8078, Cr-Branched-From main@{#1706539}); cherry-pick of main commit 96464d3f00 (main@{#1707192}), branch bug 570558164. Browser input-routing UAF, distinct from the three FSA locks on the same week. The angle: a dangling raw_ptr to a destroyed RenderWidgetHostViewInput used during async touch-ack / GestureFlingCancel, where ScopedInputDispatchPin pins a freed view. The fix is both a type change (raw_ptr -> base::WeakPtr) and a destruction-ordering fix (clear last_fling_start_target_ before the synchronous ack flush that can route a fling-cancel back into the dying view).

Root cause

RenderWidgetHostInputEventRouter queues touch acks in AckData entries whose target_view/root_view were raw_ptr; because acks are handled asynchronously, the pointed-to view can be destroyed before the ack returns from the renderer, leaving dangling raw_ptrs. Separately, OnRenderWidgetHostViewInputDestroyed() cleared last_fling_start_target_ only after calling UpdateQueueAfterTargetDestroyed(), which can synchronously flush queued touch acks via root_view->ProcessAckedTouchEvent() and synthesize/route a GestureFlingCancel; that cancel was dispatched to the still-set last_fling_start_target_ (the view being destroyed) and pinned it with input::ScopedInputDispatchPin, a use-after-free. The fix changes AckData::target_view/root_view to base::WeakPtr (populated via GetInputWeakPtr()) so stale entries read as null instead of dangling, updates dequeue/erase logic to treat a null WeakPtr as an already-gone target, and moves the ‘if (view == last_fling_start_target_) last_fling_start_target_ = nullptr;’ clearing to BEFORE UpdateQueueAfterTargetDestroyed() so the reentrant fling-cancel cannot target the dying view. Defense-in-depth is added in RenderWidgetHostViewAndroid::ProcessAckedTouchEvent() and SendGestureEvent(), which now early-return if destroy_pending() before taking a ScopedInputDispatchPin.

Key code

AckData pointers become WeakPtr, and last_fling_start_target_ is cleared before the synchronous ack flush can route a GestureFlingCancel

struct AckData {
  TouchEventWithLatencyInfo touch_event;
  base::WeakPtr<RenderWidgetHostViewInput> target_view;
  base::WeakPtr<RenderWidgetHostViewInput> root_view;
  ...
};

// Must be cleared before UpdateQueueAfterTargetDestroyed(), which can
// synchronously flush touch acks and route a GestureFlingCancel. Otherwise
// that cancel would be dispatched to, and pin, |view| while it is being
// destroyed.
if (view == last_fling_start_target_) {
  last_fling_start_target_ = nullptr;
}

Patch walkthrough

  • components/input/render_widget_host_input_event_router.cc — AckData::target_view and AckData::root_view are changed from raw_ptr to base::WeakPtr. These entries live in ack_queue_ across the async round-trip to the renderer, so a raw_ptr dangles if the view is destroyed meanwhile; a WeakPtr instead becomes null.
  • components/input/render_widget_host_input_event_router.cc — When an AckData is constructed, target_view/root_view are now populated as target_view ? target_view->GetInputWeakPtr() : nullptr (and likewise root_view), so queued entries hold weak references obtained from the view itself.
  • components/input/render_widget_host_input_event_router.cc — The ack-processing/dequeue path now reads root_view as a base::WeakPtr, guards dispatch with ‘if (!handled_by_emulator && root_view)’ and uses root_view.get()/IsViewInMap(root_view.get()) so a view destroyed while its ack was in flight is simply skipped instead of dereferenced. The matching-ack DCHECK is updated to compare against it->target_view.get().
  • components/input/render_widget_host_input_event_router.cc — UpdateQueueAfterTargetDestroyed(): the erase predicate becomes ‘return !data.root_view || data.root_view.get() == target_view;’ and the per-entry loop clears via data.target_view.reset() when ‘!data.target_view || data.target_view.get() == target_view’, treating already-null weak refs as destroyed targets.
  • components/input/render_widget_host_input_event_router.cc — OnRenderWidgetHostViewInputDestroyed(): the ‘if (view == last_fling_start_target_) last_fling_start_target_ = nullptr;’ is moved up to BEFORE the UpdateQueueAfterTargetDestroyed() call (and removed from its old position lower in the function). This is the ordering fix: UpdateQueueAfterTargetDestroyed() can synchronously flush acks and route a GestureFlingCancel, which must not find the dying view still registered as the fling target.
  • components/input/render_widget_host_input_event_router.cc — The GestureFlingStart assignment ’last_fling_start_target_ = touchscreen_gesture_target_.get();’ is reformatted (brace-wrapped) but unchanged in behavior; it marks the source of the raw pointer that the ordering fix protects.
  • content/browser/renderer_host/render_widget_host_view_android.cc — Defense-in-depth: ProcessAckedTouchEvent() and SendGestureEvent() now ‘if (destroy_pending()) return;’ before constructing input::ScopedInputDispatchPin(this), so a view already pending destruction cannot be pinned/re-entered during teardown.
  • content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc — Adds test FlingCancelAfterTargetDestroyed plus a last_fling_start_target() accessor. It routes touch/tap-down/fling-start to a child view, destroys the child while an unacked touch is queued, and asserts last_fling_start_target_ is already null when the synchronous ProcessAckedTouchEvent callback routes a GestureFlingCancel, and that a later GestureFlingCancel does not crash or pin a dangling pointer.

Background

RenderWidgetHostInputEventRouter (rwhier) — Browser-process object that routes touch/gesture/mouse input to the correct RenderWidgetHostViewInput (per frame/surface), including cross-frame hit-testing and ack bookkeeping in ack_queue_.

RenderWidgetHostViewInput — Interface for a view that receives routed input. Its lifetime is independent of the router’s queued references; destruction is signaled via OnRenderWidgetHostViewInputDestroyed().

ScopedInputDispatchPin — RAII helper (input::ScopedInputDispatchPin) that pins a view for the duration of an input dispatch. Pinning a freed view is the use step of this UAF; the Android fix early-returns before constructing it when destroy_pending().

GestureFlingStart / GestureFlingCancel — A fling begins with kGestureFlingStart (which records last_fling_start_target_) and is later cancelled with kGestureFlingCancel. The cancel can be synthesized synchronously while flushing acks during view destruction, which is how the dying view gets re-targeted.

raw_ptr vs base::WeakPtr — raw_ptr (MiraclePtr/BackupRefPtr) detects dangling but is still an owning-style raw reference that must not outlive the object; base::WeakPtr nulls out automatically on destruction, which is correct for pointers parked across async work.

Vulnerability window

  1. Latent — Fling targets in RenderWidgetHostInputEventRouter were raw_ptr but used for async event handling; the view could be destroyed before acks returned (per the commit message).
  2. Fix landed — Original fix 96464d3f002bc477effa861aa948d9998ff9e73e landed on main@{#1707192} (CL 8451950), author jonross@chromium.org, reviewed by kartarsingh@google.com.
  3. Backport — Cherry-picked on 2026-10-06 via CL 8527449 with Owners-Override by govind@chromium.org, Cr-Commit-Position refs/branch-heads/8078@{#580}, branch bug 570558164.

Proof of concept

The diff adds unit test FlingCancelAfterTargetDestroyed that reproduces the exact sequence: route a touch-start and tap-down to a child view, route a GestureFlingStart (setting last_fling_start_target_ = child view), then destroy the child view while an unacked touch is queued. During OnRenderWidgetHostViewInputDestroyed, UpdateQueueAfterTargetDestroyed() synchronously calls root_view->ProcessAckedTouchEvent(), whose callback synthesizes and routes a GestureFlingCancel; the test asserts last_fling_start_target_ is already null so the cancel cannot pin the dying view, and that a subsequent GestureFlingCancel does not crash. Real-world reachability (inferred): a page performs a touchscreen fling gesture on a subframe/view that is then torn down (e.g. the frame navigates or is removed) while a touch ack is still outstanding, triggering the synchronous fling-cancel into the freed view. No test exercises renderer-side exploitation primitives.

// Regression test FlingCancelAfterTargetDestroyed (abbreviated):
rwhier()->RouteTouchEvent(view_root_.get(), &touch_event, ui::LatencyInfo());
rwhier()->RouteGestureEvent(view_root_.get(), &tap_down, ui::LatencyInfo());
rwhier()->RouteGestureEvent(view_root_.get(), &fling_start, ui::LatencyInfo());
EXPECT_EQ(child.view.get(), last_fling_start_target());
// On destroy, UpdateQueueAfterTargetDestroyed() synchronously routes a
// GestureFlingCancel; last_fling_start_target_ must already be null:
view_root_->set_on_process_acked_touch_event_callback(...route sync fling_cancel...);
rwhier()->OnRenderWidgetHostViewInputDestroyed(child.view.get());
EXPECT_EQ(nullptr, last_fling_start_target());

Exploitation

  1. Trigger — Start a touchscreen fling on a view (recording last_fling_start_target_) with an unacked touch still queued, then cause that view to be destroyed (frame navigation/removal), as in the regression test.
  2. Free — The RenderWidgetHostViewInput (child view) is destroyed via OnRenderWidgetHostViewInputDestroyed(); in the unpatched code last_fling_start_target_ and AckData raw_ptrs still reference the freed object. (Allocation size/bucket of the view object not shown by the patch — inferred.)
  3. Use — UpdateQueueAfterTargetDestroyed() synchronously flushes acks -> ProcessAckedTouchEvent() synthesizes a GestureFlingCancel routed to the stale last_fling_start_target_, constructing input::ScopedInputDispatchPin on the freed view and operating through its (dangling) vtable/members.
  4. Escalation — (inferred) Groom the freed RenderWidgetHostViewInput allocation with a controlled object so the pin/dispatch dereferences an attacker-controlled vtable, yielding browser-process control-flow hijack or type confusion. The patch demonstrates only the crash/pin, not a weaponized primitive.

Detection & hunting

For defenders and detection engineers:

  • UAF crash in input routing during fling teardown —
  • raw_ptr dangling (BackupRefPtr) reports on AckData —
  • Android ScopedInputDispatchPin on destroy_pending view —

Audit directions

  • raw_ptr held across async input acks —
  • Clear-before-reentrant-flush ordering —
  • ScopedInputDispatchPin call sites —
  • Synthesized gesture routing during destruction —

Before / after

Loading diff…