CVE-2026-11632
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/tabs/tab_strip.cc |
modified |
Files Changed
chrome/browser/ui/views/tabs/tab_strip.cc
Patch
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;
}
Original Bug Report
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:
TabDragContextImpl::ContinueDragincorrectly returns the liveness of theTabDragControllerinstead of theTabDragContextImpl(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 sourceTabDragContextImplis destroyed.- In
OnGestureEvent, the local safety check variabletabstrip_aliveis initialized toLiveness::kAlivebut is only updated duringkGestureScrollUpdateevents. For other events that invokeEndDrag(e.g.,kGestureScrollEnd),tabstrip_aliveremainsLiveness::kAliveeven 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.
- Touch Drag Start: A user touch-drags a tab in a source window
A(containing at least two tabs).OnGestureEventis dispatched, enteringContinueDragwhich runs the nested move loop viadrag_controller_->Drag(). - Tab Detachment: The tab is dragged out of window
A, which transfers theTabDragController(TDC) to a newly created windowC. WindowA’sdrag_controller_is set to null, making its window closeable. - Source Context Destruction: While the nested event loop of the TDC continues to run, window
Ais closed (e.g., via a script-triggeredwindow.close()or an extension’schrome.windows.removecall). This posts a destruction task that is processed inside the nested event loop, synchronously destroying windowA’sTabStripand itsTabDragContextImpl. - Propagation of Fake Liveness: The user finishes dragging, ending the nested loop. Since the TDC is still alive in window
C, it returnsLiveness::kAlive, which incorrectly propagates back as the liveness of the now-freed windowA’sTabDragContextImpl. - Use-After-Free: The liveness check is bypassed, and a virtual call to
tab_strip_->OnGestureEvent(event)is executed using the freedthispointer.
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.