Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in TabStrip
DescriptionUse after free in TabStrip
ComponentTabStrip
Bug ClassUAF
Tracker516707881
Fix commitf128c7a3299b (chromium/src) +3/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

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

Files Changed

  • chrome/browser/ui/views/tabs/tab_strip.cc
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