Critical watch CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in TabStrip
DescriptionUse after free in TabStrip
ComponentTabStrip
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516707881
Fix commitf128c7a3299b (chromium/src) +3/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-05-27 (public git)
Disclosed2026-06-08
Patch gap12 days — fix public in source before the advisory

Background

`TabDragContextImpl`
the inner class of TabStrip that manages an in-progress tab-drag session; OnGestureEvent (the edited function) handles touch gesture events during a drag.
Tab drag nested run loop
dragging a tab can spin a nested run loop (via ContinueDrag/EndDrag), during which the TabStrip and its TabDragContextImpl can be destroyed before control returns.
`Liveness`
an enum (kAlive/kDeleted) returned by drag helpers to report whether the tab strip survived a nested operation.
`WeakPtr`/`weak_factory_`
a self-invalidating pointer; a WeakPtr obtained from weak_factory_ becomes null once its owning object is destroyed, giving a second, independent liveness signal.

Root Cause Analysis

TabDragContextImpl::OnGestureEvent processes gesture events that can enter nested drag loops (ContinueDrag, EndDrag), after which it continues to touch members of this.

Pre-patch, it decided whether it was safe to proceed solely from the Liveness tabstrip_alive return value, returning early only when tabstrip_alive == Liveness::kDeleted. That single signal was insufficient: a path could leave tabstrip_alive as kAlive while the object (or its tab strip) had in fact been destroyed during the nested loop, so the subsequent member access on this was a use-after-free.

The fix captures auto weak_this = weak_factory_.GetWeakPtr() at function entry and changes the guard to if (!weak_this || tabstrip_alive == Liveness::kDeleted) return;, so execution aborts whenever either the weak pointer was invalidated (object destroyed) or the explicit liveness flag says deleted. This works because the WeakPtr is nulled by the object’s own destruction, catching the destruction cases the Liveness return value missed.

Key insight
Relying on a single Liveness return value to detect destruction across a nested run loop was incomplete; adding an independent WeakPtr self-check (!weak_this) before any post-loop member access closes the gap.

Attack Path

  1. Start a tab drag The user (or automated UI interaction) begins a touch-driven tab drag, entering TabDragContextImpl::OnGestureEvent.
  2. Enter a nested loop A gesture event (e.g. kGestureScrollUpdate/kGestureScrollEnd) drives ContinueDrag/EndDrag, which spins a nested run loop.
  3. Destroy the tab strip During that loop the drag concludes in a way that destroys the TabStrip/TabDragContextImpl while leaving tabstrip_alive as kAlive.
  4. Use after free Control returns past the old guard and dereferences the freed this in the code below the check (inferred downstream consequence; the diff shows the guard now prevents it via !weak_this).

Impact Assessment

The bug is a use-after-free of the TabStrip/TabDragContextImpl in the browser process, reached through tab-drag gesture handling; it requires an active tab-drag interaction (a UI/timing-dependent gesture sequence) rather than plain web content. The vendor advisory rates it critical and does not report in-the-wild exploitation. Exploitation beyond the browser-process UAF/crash — controlled reuse of the freed object or code execution — is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/tabs/tab_strip.cc
modified

Files Changed

  • chrome/browser/ui/views/tabs/tab_strip.cc

Audit Directions

  • Single-signal liveness after nested loops
    flag code that gauges survival across a nested run loop from only a returned Liveness/bool and then touches this; require an independent WeakPtr check.
  • Drag/drop and gesture handlers
    review other TabStrip, drag-controller, and gesture paths that spin nested loops (ContinueDrag, RunMoveLoop, modal drags) for post-loop member access without a self-weak-pointer guard.
  • `weak_factory_` presence vs use
    where a class already owns a weak_factory_, verify it is actually consulted after every re-entrant call, not just handed to posted callbacks.
From f128c7a3299b173395d29b9d1d267ee18c9a0858 Mon Sep 17 00:00:00 2001
From: Eshwar Stalin <estalin@chromium.org>
Date: Wed, 27 May 2026 15:09:36 -0700
Subject: [PATCH] Fix for potential UAF in TabDragContextImpl::OnGestureEvent

Using a weak_ptr in addition to the return value to determine if the tab
strip has been deleted after we exit out of the nested run loop.

Fixed: 516707881
Change-Id: I33d2aa5ccadd0a2b9057e978ea794245898d7c25
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7878394
Reviewed-by: Kaan Alsan <alsan@chromium.org>
Commit-Queue: Eshwar Stalin <estalin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637264}
---

diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
index b15e859..dc2fe4f 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -222,6 +222,8 @@
   void OnMouseCaptureLost() override { EndDrag(EndDragReason::kCaptureLost); }
 
   void OnGestureEvent(ui::GestureEvent* event) override {
+    auto weak_this = weak_factory_.GetWeakPtr();
+
     Liveness tabstrip_alive = Liveness::kAlive;
     switch (event->type()) {
       case ui::EventType::kGestureScrollEnd:
@@ -251,7 +253,7 @@
 
     // If tabstrip was destroyed (during ContinueDrag above), return early to
     // avoid UAF below.
-    if (tabstrip_alive == Liveness::kDeleted) {
+    if (!weak_this || tabstrip_alive == Liveness::kDeleted) {
       return;
     }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in TabDragContextImpl::OnGestureEvent due to incorrect liveness tracking

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the Google Chrome browser process due to incorrect liveness tracking inside TabDragContextImpl. When dragging tabs enters a nested event loop or executes a drag completion/cancellation, the context can be destroyed, but the subsequent liveness checks are bypassed. This results in dereferencing the freed context (this) to invoke a virtual method, potentially leading to arbitrary code execution in the unsandboxed browser process.

Affected files:

  • chrome/browser/ui/views/tabs/tab_strip.cc

Estimated timestamp from git blame: 2025-02-06

Summary

There is a potential Use-After-Free (UAF) vulnerability in TabDragContextImpl::OnGestureEvent in chrome/browser/ui/views/tabs/tab_strip.cc. The core issue is twofold:

  1. TabDragContextImpl::ContinueDrag incorrectly returns the liveness of the TabDragController instead of the TabDragContextImpl (this) object itself. Because the drag controller can be transferred to and owned by a new window during dragging, the controller can remain alive while the source TabDragContextImpl is destroyed.
  2. In OnGestureEvent, the local safety check variable tabstrip_alive is initialized to Liveness::kAlive but is only updated during kGestureScrollUpdate events. For other events that invoke EndDrag (e.g., kGestureScrollEnd), tabstrip_alive remains Liveness::kAlive even if the context is destroyed during the teardown process.

In either case, safety checks are bypassed, and the code proceeds to invoke tab_strip_->OnGestureEvent(event), causing a UAF when dereferencing the freed this context to read tab_strip_ and call a virtual method on it.

Root Cause Analysis

In chrome/browser/ui/views/tabs/tab_strip.cc, ContinueDrag handles dragging progress and can enter a nested event/move loop (e.g., via drag_controller_->Drag()):

[[nodiscard]] Liveness ContinueDrag(views::View* view,
                                    const ui::LocatedEvent& event) {
  if (!drag_controller_.get() || ...) return Liveness::kAlive;
  ...
  // Note: `tab_strip_` can be destroyed during drag, also destroying `this`.
  const TabDragController::Liveness drag_controller_alive =
      drag_controller_->Drag(screen_location);            // <-- Nested event loop
  return drag_controller_alive == TabDragController::Liveness::kAlive
             ? Liveness::kAlive : Liveness::kDeleted;      // <-- Tracks the wrong object's liveness
}

If the source window (and thus this context) is closed during the nested loop, this is freed. However, because TabDragController may have been transferred to a new window (which is common when detaching tabs), drag_controller_alive is returned as Liveness::kAlive. This is incorrectly propagated back to the caller as the liveness of the TabDragContextImpl itself.

In TabDragContextImpl::OnGestureEvent (lines 224-262):

void OnGestureEvent(ui::GestureEvent* event) override {
  Liveness tabstrip_alive = Liveness::kAlive;
  switch (event->type()) {
    ...
    case ui::EventType::kGestureScrollUpdate:
      tabstrip_alive = ContinueDrag(this, *event);   // Line 240
      break;
    ...
  }
  event->SetHandled();
  if (tabstrip_alive == Liveness::kDeleted) return;  // Guard fails to prevent execution
  tab_strip_->OnGestureEvent(event);                 // Line 261 — UAF virtual call on freed 'this'
}

If the context is destroyed during ContinueDrag under kGestureScrollUpdate, the liveness check evaluates to kAlive and is bypassed. Alternatively, if the event type is kGestureScrollEnd, it calls EndDrag(EndDragReason::kComplete). If EndDrag results in the destruction of the context (which occurs if detaching the last tab causes the source browser window to close synchronously or via posted tasks executed inside nested event pumps), tabstrip_alive remains its initial value of Liveness::kAlive. The guard at line 254 is entirely bypassed, and tab_strip_->OnGestureEvent(event) is called on the freed object.

Impact and MiraclePtr Status

Because the UAF occurs on the implicit this pointer (which is a raw C++ pointer), MiraclePtr / BackupRefPtr does not protect this scenario. BRP protects the pointee of a live raw_ptr; it cannot prevent reading raw pointer storage from an already freed enclosing object (this). Consequently, an attacker who successfully controls the freed memory block via heap grooming can hijack the control flow during the virtual method dispatch.

Potential Trigger Path (Theoretical/Suggested Steps)

Note: These steps are suggested based on static code analysis, as our tooling currently lacks the capability to execute and validate dynamic code.

  1. Touch Drag Start: A user touch-drags a tab in a source window A (containing at least two tabs). OnGestureEvent is dispatched, entering ContinueDrag which runs the nested move loop via drag_controller_->Drag().
  2. Tab Detachment: The tab is dragged out of window A, which transfers the TabDragController (TDC) to a newly created window C. Window A’s drag_controller_ is set to null, making its window closeable.
  3. Source Context Destruction: While the nested event loop of the TDC continues to run, window A is closed (e.g., via a script-triggered window.close() or an extension’s chrome.windows.remove call). This posts a destruction task that is processed inside the nested event loop, synchronously destroying window A’s TabStrip and its TabDragContextImpl.
  4. Propagation of Fake Liveness: The user finishes dragging, ending the nested loop. Since the TDC is still alive in window C, it returns Liveness::kAlive, which incorrectly propagates back as the liveness of the now-freed window A’s TabDragContextImpl.
  5. Use-After-Free: The liveness check is bypassed, and a virtual call to tab_strip_->OnGestureEvent(event) is executed using the freed this pointer.

Suggested Fix

To resolve this, the vertical tab drag implementation has already been hardened with a self-WeakPtr guard (see chrome/browser/ui/views/tabs/vertical/vertical_tab_drag_handler.cc:332-336).

A similar fix should be applied to TabDragContextImpl inside chrome/browser/ui/views/tabs/tab_strip.cc by tracking the liveness of the context using its existing weak_factory_:

void OnGestureEvent(ui::GestureEvent* event) override {
  auto weak_this = weak_factory_.GetWeakPtr();
  
  switch (event->type()) {
    ...
  }
  event->SetHandled();

  if (!weak_this) {
    return;
  }

  tab_strip_->OnGestureEvent(event);
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker