CVE-2026-10959
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc |
modified | |
GetVizWidgetInputHandlerHostthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc |
modified | |
TEST_Pthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc |
modified | |
forthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc |
modified |
Files Changed
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.ccthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager.hthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
Patch
From b76a9f8c73221ee3421ce26e5ef6a006e34aa7b8 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Wed, 20 May 2026 09:38:33 -0700 Subject: [PATCH] Fix UAF in WidgetInputHandlerManager due to data race on viz_host_ A data race existed in WidgetInputHandlerManager involving viz_host_, a mojo::SharedRemote. viz_host_ was mutated on the main thread and compositor thread, and read from both. GetVizWidgetInputHandlerHost() returned a raw pointer to the underlying proxy. If viz_host_ was reset on another thread while a caller held this raw pointer, a Use-After-Free (UAF) could occur. This CL fixes the issue by: - Guarding viz_host_ with a base::Lock. - Changing GetVizWidgetInputHandlerHost() to return a copy of the mojo::SharedRemote instead of a raw pointer. A unit test is added to reproduce the race condition. Bug: 507258648 Change-Id: I694de5a32d800d098783af0246557cdc27e4cbd3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849436 Reviewed-by: Kartar Singh <[email protected]> Commit-Queue: Jonathan Ross <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633648} --- diff --git a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc index 8416f1e77..b88e410 100644 --- a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc +++ b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc @@ -351,6 +351,7 @@ void WidgetInputHandlerManager::SetVizHost( mojo::PendingRemote<mojom::blink::WidgetInputHandlerHost> viz_host) { + base::AutoLock lock(viz_host_lock_); if (viz_host_) { DLOG(WARNING) << "Resetting an existing viz_host. This may indicate a " << "missed disconnect notification during GPU restart."; @@ -523,8 +524,7 @@ host->DidStartScrollingViewport(); } - if (mojom::blink::WidgetInputHandlerHost* viz_host = - GetVizWidgetInputHandlerHost()) { + if (auto viz_host = GetVizWidgetInputHandlerHost()) { viz_host->DidStartScrollingViewport(); } } @@ -541,8 +541,7 @@ host->SetTouchActionFromMain(touch_action); } - if (mojom::blink::WidgetInputHandlerHost* viz_host = - GetVizWidgetInputHandlerHost()) { + if (auto viz_host = GetVizWidgetInputHandlerHost()) { viz_host->SetTouchActionFromMain(touch_action); } } @@ -554,12 +553,14 @@ return nullptr; } -mojom::blink::WidgetInputHandlerHost* +mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost> WidgetInputHandlerManager::GetVizWidgetInputHandlerHost() { - if (viz_host_) { - return viz_host_.get(); - } - return nullptr; + base::AutoLock lock(viz_host_lock_); + // Returning a copy of the SharedRemote increments the refcount of the + // underlying state while under the lock, ensuring it remains valid for the + // caller even if viz_host_ is reset on another thread after the lock is + // released. + return viz_host_; } #if BUILDFLAG(IS_ANDROID) diff --git a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h index 4b222cc..2c5fc4af 100644 --- a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h +++ b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h @@ -9,7 +9,9 @@ #include <memory> #include <optional> +#include "base/synchronization/lock.h" #include "base/task/single_thread_task_runner.h" +#include "base/thread_annotations.h" #include "base/types/optional_ref.h" #include "base/types/pass_key.h" #include "build/build_config.h" @@ -140,7 +142,8 @@ void ProcessTouchAction(cc::TouchAction touch_action); mojom::blink::WidgetInputHandlerHost* GetWidgetInputHandlerHost(); - mojom::blink::WidgetInputHandlerHost* GetVizWidgetInputHandlerHost(); + mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost> + GetVizWidgetInputHandlerHost(); #if BUILDFLAG(IS_ANDROID) void AttachSynchronousCompositor( @@ -316,7 +319,10 @@ void FlushCompositorQueueForTesting(); void FlushMainThreadQueueForTesting(base::OnceClosure done); - void OnVizHostDisconnected() { viz_host_.reset(); } + void OnVizHostDisconnected() { + base::AutoLock lock(viz_host_lock_); + viz_host_.reset(); + } // Only valid to be called on the main thread. base::WeakPtr<WidgetBase> widget_; @@ -331,7 +337,9 @@ // The WidgetInputHandlerHost is bound on the compositor task runner // but class can be called on the compositor and main thread. mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost> host_; - mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost> viz_host_; + base::Lock viz_host_lock_; + mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost> viz_host_ + GUARDED_BY(viz_host_lock_); // Any thread can access these variables. scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_; diff --git a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc index 7a01509..3f1c386 100644 --- a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc +++ b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc @@ -4,13 +4,20 @@ #include "third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h" +#include <atomic> #include <string> +#include <thread> +#include <vector> #include "base/memory/raw_ptr.h" #include "base/memory/scoped_refptr.h" #include "base/memory/weak_ptr.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/thread_pool.h" +#include "base/test/bind.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" +#include "base/threading/thread_restrictions.h" #include "cc/test/fake_impl_task_runner_provider.h" #include "cc/test/fake_layer_tree_host_impl.h" #include "cc/test/mock_input_handler.h" @@ -58,8 +65,8 @@ // testing::Test: void SetUp() override; - private: - base::test::SingleThreadTaskEnvironment task_environment_; + protected: + base::test::TaskEnvironment task_environment_; scoped_refptr<WidgetInputHandlerManager> widget_input_handler_manager_; StubWidgetBaseClient client_; @@ -219,6 +226,85 @@ } } +TEST_P(WidgetInputHandlerManagerTest, VizHostRace) { + std::atomic<bool> start_flag{false}; + std::atomic<int> threads_ready{0}; + std::atomic<int> threads_finished{0}; + const int kOpsPerThread = 1000; + + scoped_refptr<WidgetInputHandlerManager> manager; + + auto reader_runner = base::ThreadPool::CreateSequencedTaskRunner({}); + auto writer_runner = base::ThreadPool::CreateSequencedTaskRunner({}); + + base::WaitableEvent manager_created; + + auto writer_worker = [&]() { + manager = WidgetInputHandlerManager::Create( + widget_base_->GetWeakPtr(), frame_widget_input_handler_, + /*never_composited=*/false, + /*compositor_thread_scheduler=*/nullptr, widget_scheduler_, + /*needs_input_handler=*/false, + /*allow_scroll_resampling=*/false, + /*io_thread_id=*/base::kInvalidThreadId, + /*main_thread_id=*/base::PlatformThread::CurrentId()); + manager_created.Signal(); + + threads_ready++; + while (!start_flag.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + + std::vector<mojo::PendingReceiver<mojom::blink::WidgetInputHandlerHost>> + receivers; + for (int i = 0; i < kOpsPerThread; ++i) { + mojo::PendingRemote<mojom::blink::WidgetInputHandlerHost> viz_host_remote; + auto receiver = viz_host_remote.InitWithNewPipeAndPassReceiver();
Regression Test / PoC
diff --git a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
index 7a01509..3f1c386 100644
--- a/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
+++ b/third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
@@ -4,13 +4,20 @@
#include "third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h"
+#include <atomic>
#include <string>
+#include <thread>
+#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/task/thread_pool.h"
+#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
+#include "base/threading/thread_restrictions.h"
#include "cc/test/fake_impl_task_runner_provider.h"
#include "cc/test/fake_layer_tree_host_impl.h"
#include "cc/test/mock_input_handler.h"
@@ -58,8 +65,8 @@
// testing::Test:
void SetUp() override;
- private:
- base::test::SingleThreadTaskEnvironment task_environment_;
+ protected:
+ base::test::TaskEnvironment task_environment_;
scoped_refptr<WidgetInputHandlerManager> widget_input_handler_manager_;
StubWidgetBaseClient client_;
@@ -219,6 +226,85 @@
}
}
+TEST_P(WidgetInputHandlerManagerTest, VizHostRace) {
+ std::atomic<bool> start_flag{false};
+ std::atomic<int> threads_ready{0};
+ std::atomic<int> threads_finished{0};
+ const int kOpsPerThread = 1000;
+
+ scoped_refptr<WidgetInputHandlerManager> manager;
+
+ auto reader_runner = base::ThreadPool::CreateSequencedTaskRunner({});
+ auto writer_runner = base::ThreadPool::CreateSequencedTaskRunner({});
+
+ base::WaitableEvent manager_created;
+
+ auto writer_worker = [&]() {
+ manager = WidgetInputHandlerManager::Create(
+ widget_base_->GetWeakPtr(), frame_widget_input_handler_,
+ /*never_composited=*/false,
+ /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
+ /*needs_input_handler=*/false,
+ /*allow_scroll_resampling=*/false,
+ /*io_thread_id=*/base::kInvalidThreadId,
+ /*main_thread_id=*/base::PlatformThread::CurrentId());
+ manager_created.Signal();
+
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ std::vector<mojo::PendingReceiver<mojom::blink::WidgetInputHandlerHost>>
+ receivers;
+ for (int i = 0; i < kOpsPerThread; ++i) {
+ mojo::PendingRemote<mojom::blink::WidgetInputHandlerHost> viz_host_remote;
+ auto receiver = viz_host_remote.InitWithNewPipeAndPassReceiver();
+ receivers.push_back(std::move(receiver));
+ manager->SetVizHost(std::move(viz_host_remote));
+ }
+ threads_finished++;
+ };
+
+ auto reader_worker = [&]() {
+ base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait;
+ // Wait for manager to be created by writer thread.
+ manager_created.Wait();
+
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ for (int i = 0; i < kOpsPerThread; ++i) {
+ manager->GetVizWidgetInputHandlerHost();
+ }
+ threads_finished++;
+ };
+
+ writer_runner->PostTask(FROM_HERE, base::BindLambdaForTesting(writer_worker));
+ reader_runner->PostTask(FROM_HERE, base::BindLambdaForTesting(reader_worker));
+
+ // Wait until both threads are ready at the starting line.
+ while (threads_ready.load(std::memory_order_relaxed) < 2) {
+ std::this_thread::yield();
+ }
+
+ // Signal the starting flag to allow both threads to race.
+ start_flag.store(true, std::memory_order_release);
+
+ // Wait until both threads are finished.
+ while (threads_finished.load(std::memory_order_relaxed) < 2) {
+ std::this_thread::yield();
+ }
+
+ // Ensure destruction on writer thread.
+ writer_runner->PostTask(
+ FROM_HERE,
+ base::BindOnce([](scoped_refptr<WidgetInputHandlerManager> m) {},
+ std::move(manager)));
+}
+
INSTANTIATE_TEST_SUITE_P(,
WidgetInputHandlerManagerTest,
testing::Bool(),
Original Bug Report
Potential Use-After-Free in WidgetInputHandlerManager via Data Race
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A data race in WidgetInputHandlerManager allows unsynchronized access to a mojo::SharedRemote across the main and compositor threads. This race can lead to the compositor thread obtaining and using a dangling raw pointer while the main thread concurrently resets the connection. An attacker could potentially exploit this Use-After-Free to achieve remote code execution in the sandboxed renderer process.
Affected files:
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.ccthird_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h
Estimated timestamp from git blame: 2025-06-27
Description
A data race exists in third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc involving the viz_host_ member variable, which is a mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost>.
While mojo::SharedRemote is safe to call from multiple threads, its underlying state is managed via a scoped_refptr, making concurrent reassignment and access of the same SharedRemote instance a data race. viz_host_ is mutated on the main thread (via SetVizHost) and the compositor thread (via OnVizHostDisconnected), and it is read from both threads (via GetVizWidgetInputHandlerHost()).
The most severe consequence occurs because GetVizWidgetInputHandlerHost() returns a raw pointer directly to the proxy object owned by the SharedRemote (return viz_host_.get();). If the SharedRemote is reset on another thread while a caller is holding this raw pointer, the underlying object is destroyed, leading to a Use-After-Free (UAF) upon virtual method dispatch.
BackupRefPtr (MiraclePtr) does not mitigate this vulnerability because SharedRemoteBase’s internal scoped_refptr uses RAW_PTR_EXCLUSION, and the pointer returned to the caller is held as a standard C++ raw pointer.
Potential Attacker Steps
Note: These are suggested steps based on code analysis, as our tooling agent cannot execute a working proof of concept.
- The attacker loads a malicious webpage on a platform where
input::features::kInputOnVizis enabled (e.g., Android). - The attacker’s JavaScript intentionally triggers a GPU process crash (e.g., via a WebGL resource exhaustion technique).
- The browser process recovers the GPU process and sends a new
viz_hostIPC to the renderer, resulting in the main thread executingWidgetInputHandlerManager::SetVizHost(). - Concurrently, the attacker synthesizes a gesture scroll event, which is processed by the renderer’s compositor thread.
- The compositor thread executes
WidgetInputHandlerManager::DidStartScrollingViewport()and callsGetVizWidgetInputHandlerHost(). - The compositor thread extracts the raw pointer (
viz_host_.get()) just before being preempted by the OS scheduler. - The main thread executes
viz_host_.reset()insideSetVizHost(), destroying the underlying proxy object and freeing the memory. - The attacker’s script, actively spraying the renderer heap, reclaims the freed proxy object’s memory and overwrites it with a fake object containing a controlled vtable pointer.
- The compositor thread resumes and executes
viz_host->DidStartScrollingViewport(). The virtual dispatch uses the attacker’s fake vtable, hijacking control flow and resulting in Remote Code Execution (RCE) in the renderer process.
Suggested Fix
- Introduce a
base::Lock(e.g.,viz_host_lock_) inWidgetInputHandlerManagerto synchronize all reads, assignments, and resets ofviz_host_. - Modify
GetVizWidgetInputHandlerHost()to return a copy of themojo::SharedRemoterather than a raw pointer to the underlying proxy. By returning a copy while under the lock, the reference count of the underlying state is safely incremented, ensuring the proxy object remains alive as long as the caller holds the copy.
Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.