CVE-2026-16804
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/overscroll_controller.cc |
modified | |
ifcontent/browser/renderer_host/overscroll_controller_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/overscroll_controller.cccontent/browser/renderer_host/overscroll_controller_delegate.hcontent/browser/renderer_host/overscroll_controller_unittest.cccontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc
Patch
From 81f6357fbf8386022422e8f88ef3547f91a89f75 Mon Sep 17 00:00:00 2001
From: Bo Liu <boliu@chromium.org>
Date: Mon, 20 Jul 2026 05:26:04 -0700
Subject: [PATCH] content: Post OnOverscrollComplete
To avoid reentrancy issues.
Fixed: 524721670
Change-Id: Ibb60766beba22d1cc64d29505cbeb1860b00dc5d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102902
Commit-Queue: Bo Liu <boliu@chromium.org>
Reviewed-by: Kartar Singh <kartarsingh@google.com>
Cr-Commit-Position: refs/heads/main@{#1664626}
---
diff --git a/content/browser/renderer_host/overscroll_controller.cc b/content/browser/renderer_host/overscroll_controller.cc
index cbd2d1ae..cdfbcb8 100644
--- a/content/browser/renderer_host/overscroll_controller.cc
+++ b/content/browser/renderer_host/overscroll_controller.cc
@@ -9,11 +9,17 @@
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/notreached.h"
+#include "base/task/single_thread_task_runner.h"
#include "content/browser/renderer_host/overscroll_controller_delegate.h"
#include "content/public/browser/overscroll_configuration.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
+namespace features {
+BASE_FEATURE(kOverscrollPostDelegateCompleteKillSwitch,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+} // namespace features
+
namespace content {
namespace {
@@ -561,16 +567,27 @@
void OverscrollController::CompleteAction() {
ignore_following_inertial_events_ = true;
if (delegate_) {
- // The delegate call can lead to the destruction of |this|.
- // Get a weak pointer to |this| before making the call.
- base::WeakPtr<OverscrollController> weak_this = weak_factory_.GetWeakPtr();
+ if (base::FeatureList::IsEnabled(
+ features::kOverscrollPostDelegateCompleteKillSwitch)) {
+ // The delegate call can lead to the destruction of |this|. So post it
+ // instead.
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&OverscrollControllerDelegate::OnOverscrollComplete,
+ delegate_, overscroll_mode_));
+ } else {
+ // The delegate call can lead to the destruction of |this|.
+ // Get a weak pointer to |this| before making the call.
+ base::WeakPtr<OverscrollController> weak_this =
+ weak_factory_.GetWeakPtr();
- delegate_->OnOverscrollComplete(overscroll_mode_);
+ delegate_->OnOverscrollComplete(overscroll_mode_);
- // If |this| was destroyed, the weak pointer will now be invalid.
- // Return immediately to avoid the UAF on the call to Reset().
- if (!weak_this) {
- return;
+ // If |this| was destroyed, the weak pointer will now be invalid.
+ // Return immediately to avoid the UAF on the call to Reset().
+ if (!weak_this) {
+ return;
+ }
}
}
Reset();
diff --git a/content/browser/renderer_host/overscroll_controller_delegate.h b/content/browser/renderer_host/overscroll_controller_delegate.h
index 53882d4..b1f350691 100644
--- a/content/browser/renderer_host/overscroll_controller_delegate.h
+++ b/content/browser/renderer_host/overscroll_controller_delegate.h
@@ -35,7 +35,7 @@
// the delegate consumed the event.
virtual bool OnOverscrollUpdate(float delta_x, float delta_y) = 0;
- // This is called when the overscroll completes.
+ // This is called asynchronously when the overscroll completes.
virtual void OnOverscrollComplete(OverscrollMode overscroll_mode) = 0;
// This is called when the direction of the overscroll changes. When a new
diff --git a/content/browser/renderer_host/overscroll_controller_unittest.cc b/content/browser/renderer_host/overscroll_controller_unittest.cc
index e1a9c01f..99bb6c5 100644
--- a/content/browser/renderer_host/overscroll_controller_unittest.cc
+++ b/content/browser/renderer_host/overscroll_controller_unittest.cc
@@ -10,6 +10,7 @@
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
#include "content/browser/renderer_host/overscroll_controller_delegate.h"
#include "content/common/features.h"
#include "content/public/browser/overscroll_configuration.h"
@@ -26,9 +27,12 @@
OverscrollControllerTest(const OverscrollControllerTest&) = delete;
OverscrollControllerTest& operator=(const OverscrollControllerTest&) = delete;
- void ResetController() {
+ void ResetController(base::OnceClosure closure) {
controller_.reset();
controller_reset_ = true;
+ if (closure) {
+ std::move(closure).Run();
+ }
}
protected:
@@ -137,6 +141,8 @@
base::test::ScopedFeatureList scoped_feature_list_;
+ base::test::SingleThreadTaskEnvironment task_environment_;
+
// This must be the last member.
base::WeakPtrFactory<OverscrollControllerTest> weak_factory_{this};
};
@@ -205,9 +211,13 @@
EXPECT_EQ(OVERSCROLL_NONE, delegate()->completed_mode());
// Inertial update event complete the overscroll action.
+ base::RunLoop run_loop;
EXPECT_FALSE(SimulateGestureScrollUpdate(
100, 0, blink::WebGestureDevice::kTouchpad, timestamp, true));
+ delegate()->set_delete_controller_on_complete(true);
+ delegate()->set_on_complete_callback(run_loop.QuitClosure());
SimulateAck(false);
+ run_loop.Run();
EXPECT_EQ(OVERSCROLL_NONE, controller_mode());
EXPECT_EQ(OverscrollSource::NONE, controller_source());
EXPECT_EQ(OVERSCROLL_NONE, delegate()->current_mode());
@@ -818,10 +828,12 @@
EXPECT_EQ(OVERSCROLL_SOUTH, delegate()->current_mode());
EXPECT_EQ(OVERSCROLL_NONE, delegate()->completed_mode());
+ base::RunLoop run_loop;
// Set up the delegate to invoke a callback that deletes the controller.
delegate()->set_delete_controller_on_complete(true);
- delegate()->set_on_complete_callback(base::BindOnce(
- &OverscrollControllerTest::ResetController, weak_factory_.GetWeakPtr()));
+ delegate()->set_on_complete_callback(
+ base::BindOnce(&OverscrollControllerTest::ResetController,
+ weak_factory_.GetWeakPtr(), run_loop.QuitClosure()));
timestamp += base::Seconds(1);
@@ -831,6 +843,8 @@
blink::WebGestureDevice::kTouchscreen, timestamp));
SimulateAck(false);
+ run_loop.Run();
+
// The callback should have been run, deleting the controller.
EXPECT_TRUE(controller_reset_);
EXPECT_EQ(nullptr, controller());
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index feea736..e0fb732 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -3925,6 +3925,7 @@
base::TimeTicks progress_time =
base::TimeTicks::Now() + base::Milliseconds(17);
widget_host_->ProgressFlingIfNeeded(progress_time);
+ base::RunLoop().RunUntilIdle();
EXPECT_EQ(OVERSCROLL_NONE, overscroll_delegate()->current_mode());
ReleaseAndResetDispatchedMessages();
}
Regression Test / PoC
diff --git a/content/browser/renderer_host/overscroll_controller_unittest.cc b/content/browser/renderer_host/overscroll_controller_unittest.cc
index e1a9c01f..99bb6c5 100644
--- a/content/browser/renderer_host/overscroll_controller_unittest.cc
+++ b/content/browser/renderer_host/overscroll_controller_unittest.cc
@@ -10,6 +10,7 @@
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
#include "content/browser/renderer_host/overscroll_controller_delegate.h"
#include "content/common/features.h"
#include "content/public/browser/overscroll_configuration.h"
@@ -26,9 +27,12 @@
OverscrollControllerTest(const OverscrollControllerTest&) = delete;
OverscrollControllerTest& operator=(const OverscrollControllerTest&) = delete;
- void ResetController() {
+ void ResetController(base::OnceClosure closure) {
controller_.reset();
controller_reset_ = true;
+ if (closure) {
+ std::move(closure).Run();
+ }
}
protected:
@@ -137,6 +141,8 @@
base::test::ScopedFeatureList scoped_feature_list_;
+ base::test::SingleThreadTaskEnvironment task_environment_;
+
// This must be the last member.
base::WeakPtrFactory<OverscrollControllerTest> weak_factory_{this};
};
@@ -205,9 +211,13 @@
EXPECT_EQ(OVERSCROLL_NONE, delegate()->completed_mode());
// Inertial update event complete the overscroll action.
+ base::RunLoop run_loop;
EXPECT_FALSE(SimulateGestureScrollUpdate(
100, 0, blink::WebGestureDevice::kTouchpad, timestamp, true));
+ delegate()->set_delete_controller_on_complete(true);
+ delegate()->set_on_complete_callback(run_loop.QuitClosure());
SimulateAck(false);
+ run_loop.Run();
EXPECT_EQ(OVERSCROLL_NONE, controller_mode());
EXPECT_EQ(OverscrollSource::NONE, controller_source());
EXPECT_EQ(OVERSCROLL_NONE, delegate()->current_mode());
@@ -818,10 +828,12 @@
EXPECT_EQ(OVERSCROLL_SOUTH, delegate()->current_mode());
EXPECT_EQ(OVERSCROLL_NONE, delegate()->completed_mode());
+ base::RunLoop run_loop;
// Set up the delegate to invoke a callback that deletes the controller.
delegate()->set_delete_controller_on_complete(true);
- delegate()->set_on_complete_callback(base::BindOnce(
- &OverscrollControllerTest::ResetController, weak_factory_.GetWeakPtr()));
+ delegate()->set_on_complete_callback(
+ base::BindOnce(&OverscrollControllerTest::ResetController,
+ weak_factory_.GetWeakPtr(), run_loop.QuitClosure()));
timestamp += base::Seconds(1);
@@ -831,6 +843,8 @@
blink::WebGestureDevice::kTouchscreen, timestamp));
SimulateAck(false);
+ run_loop.Run();
+
// The callback should have been run, deleting the controller.
EXPECT_TRUE(controller_reset_);
EXPECT_EQ(nullptr, controller());
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index feea736..e0fb732 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -3925,6 +3925,7 @@
base::TimeTicks progress_time =
base::TimeTicks::Now() + base::Milliseconds(17);
widget_host_->ProgressFlingIfNeeded(progress_time);
+ base::RunLoop().RunUntilIdle();
EXPECT_EQ(OVERSCROLL_NONE, overscroll_delegate()->current_mode());
ReleaseAndResetDispatchedMessages();
}
Original Bug Report
Potential browser-process Use-After-Free in InputRouterImpl::SendKeyboardEvent
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 browser-process Use-After-Free (UAF) vulnerability exists in InputRouterImpl::SendKeyboardEvent. Calling gesture_event_queue_.StopFling() can initiate a synchronous, re-entrant call chain that destroys the owning RenderWidgetHostImpl and the InputRouterImpl itself. Once the stack unwinds, the function proceeds to read member variables and perform virtual calls on the freed object.
Affected files:
components/input/input_router_impl.cccomponents/input/input_router_impl.h
Estimated timestamp from git blame: 2018-03-21
Root Cause Analysis
In components/input/input_router_impl.cc, InputRouterImpl::SendKeyboardEvent calls gesture_event_queue_.StopFling(), which can synchronously destroy *this via a re-entrant call chain during overscroll completion. However, after the call to StopFling(), the function continues to access member variables of this and invokes further methods without any liveness checks:
// components/input/input_router_impl.cc:150-160
void InputRouterImpl::SendKeyboardEvent(
const NativeWebKeyboardEventWithLatencyInfo& key_event,
KeyboardEventCallback event_result_callback,
DispatchToRendererCallback& dispatch_callback) {
gesture_event_queue_.StopFling(); // L154 — Re-entrant; can free *this
blink::mojom::WidgetInputHandler::DispatchEventCallback callback =
base::BindOnce(&InputRouterImpl::KeyboardEventHandled, weak_this_, // L156 — UAF read of this->weak_this_
key_event, std::move(event_result_callback));
FilterAndSendWebInputEvent(key_event.event, key_event.latency, // L158 — UAF call
std::move(callback), dispatch_callback);
}
InputRouterImpl is heap-owned via a std::unique_ptr<InputRouter> within RenderInputRouter which is in turn owned by RenderWidgetHostImpl. If RenderWidgetHostImpl is synchronously destroyed, the destructor of InputRouterImpl runs immediately, freeing its backing memory.
While sibling entry points such as InputRouterImpl::OnTouchEventAck protect against this re-entrant destruction pattern using local base::WeakPtr checks:
// components/input/input_router_impl.cc:518-522
auto weak_this = weak_ptr_factory_.GetWeakPtr();
disposition_handler_->OnTouchEventAck(event, ack_source, ack_result);
if (!weak_this) {
return;
}
SendKeyboardEvent is missing this critical guard.
Potential Attack Path and Sequence
Below are the suggested/potential steps an attacker could follow to trigger this vulnerability. Note that these are analytical steps based on static code tracing, as our tooling agent does not have the capability to run code or compile a functional proof of concept:
- Setup: The attacker serves a webpage that has at least one entry in its back-navigation history and does not consume horizontal scroll gestures (
overscroll-behavior-x: auto). - Overscroll Initialization: The user performs a horizontal touchscreen swipe past the overscroll completion threshold. This transitions
OverscrollControllerinto an active overscroll mode (OVERSCROLL_EASTorOVERSCROLL_WEST). - Fling Arming: The user lifts their finger with horizontal velocity, prompting the OS to generate a
kGestureFlingStart(GFS) event. The browser’sFlingControllerconsumes the GFS, arms itsfling_curve_, and setsfling_in_progress()totruewhile theOverscrollControllerpreserves the active overscroll state. - Event Stalling: A compromised renderer can stall the
WidgetInputHandler::DispatchEventACK reply for incoming momentum-phaseGestureScrollUpdateevents, widening the race window indefinitely. - Keyboard Input Dispatch: While the fling is active, the user presses a key. This calls
RenderWidgetHostImpl::ForwardKeyboardEventWithCommands->InputRouterImpl::SendKeyboardEvent->gesture_event_queue_.StopFling(). - Synchronous GSE & ACK:
StopFlingtriggersEndCurrentFling(), which generates a synthetic touchscreenkGestureScrollEnd(GSE) event and forwards it. Since GSE is a non-blocking event, its ACK callback runs synchronously, propagating throughOnGestureEventAck->RenderWidgetHostViewAura::GestureEventAck->OverscrollController::ReceivedEventACK->CompleteAction(). - Tab/Frame Destruction:
CompleteAction()triggersdelegate_->OnOverscrollCompletewhich requests a back/forward navigation (e.g.,controller.GoBack()). A synchronous notification during the navigation request (e.g.DidStartNavigation) triggers an observer or a throttle to synchronously close/destroy theWebContents(and thusRenderWidgetHostImplandInputRouterImpl). - Use-After-Free: The stack unwinds back to
SendKeyboardEvent(line 155). The code attempts to readthis->weak_this_(line 156) and callsFilterAndSendWebInputEvent(line 158) on the freedInputRouterImplmemory block.
Because the free occurs within the context of a base::ScopedSafetyChecksExclusion scope (instantiated during event forwarding), PartitionAlloc’s Scheduler Loop Quarantine is bypassed, allowing the slot to be immediately reallocated. An attacker who has reallocated the heap slot can overwrite the this->client_ pointer to hijack the subsequent virtual call (client_->FilterInputEvent(...)) at line 650, leading to browser-process Remote Code Execution (RCE) / Sandbox Escape.
Suggested Fix
Add a base::WeakPtr check immediately after the call to StopFling() inside InputRouterImpl::SendKeyboardEvent to halt execution if this has been synchronously destroyed:
void InputRouterImpl::SendKeyboardEvent(
const NativeWebKeyboardEventWithLatencyInfo& key_event,
KeyboardEventCallback event_result_callback,
DispatchToRendererCallback& dispatch_callback) {
auto weak_this = weak_ptr_factory_.GetWeakPtr();
gesture_event_queue_.StopFling();
if (!weak_this) {
return;
}
blink::mojom::WidgetInputHandler::DispatchEventCallback callback =
base::BindOnce(&InputRouterImpl::KeyboardEventHandled, weak_this_,
key_event, std::move(event_result_callback));
FilterAndSendWebInputEvent(key_event.event, key_event.latency,
std::move(callback), dispatch_callback);
}
Evaluated with Chrome root at commit: 70c6813870b6701fa16670076bf633ee6c3a439f
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.