Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker522304853
Fix commitcf330da50ddd (chromium/src) +97/-20
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc
modified
WidgetInputHandlerManagerTest
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
modified
TEST_F
third_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.cc
  • third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.h
  • third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
From cf330da50ddd6940e2a7f47fa21f4063476b71fe Mon Sep 17 00:00:00 2001
From: Vladimir Levin <vmpstr@chromium.org>
Date: Wed, 17 Jun 2026 12:57:28 -0700
Subject: [PATCH] Update WidgetInputHandlerManager to use strong refs

This updates the class to use strong refs in the disconnect
callbacks, and clears it when clearing clients. It also
ensures that destruction is bound to the input task runner
to ensure it's destroyed on the appropriate thread

R=jonross@chromium.org

Bug: 522304853
Change-Id: I10d217c1c031f1a54870ddedd3474b5e99428074
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7951580
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1648530}
---

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 ca2cd83..26fa2b2 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
@@ -353,14 +353,14 @@
         std::move(viz_host), compositor_thread_default_task_runner_);
     viz_host_.set_disconnect_handler(
         base::BindOnce(&WidgetInputHandlerManager::OnVizHostDisconnected,
-                       AsWeakPtr()),
+                       scoped_refptr<WidgetInputHandlerManager>(this)),
         compositor_thread_default_task_runner_);
   } else {
     viz_host_ = mojo::SharedRemote<mojom::blink::WidgetInputHandlerHost>(
         std::move(viz_host));
     viz_host_.set_disconnect_handler(
         base::BindOnce(&WidgetInputHandlerManager::OnVizHostDisconnected,
-                       AsWeakPtr()),
+                       scoped_refptr<WidgetInputHandlerManager>(this)),
         base::SequencedTaskRunner::GetCurrentDefault());
   }
 }
@@ -379,12 +379,16 @@
   uses_input_handler_ = true;
   base::OnceClosure init_closure = base::BindOnce(
       &WidgetInputHandlerManager::InitOnInputHandlingThread,
-      weak_ptr_factory_.GetWeakPtr(),
+      scoped_refptr<WidgetInputHandlerManager>(this),
       widget_->LayerTreeHost()->GetDelegateForInput(), sync_compositing);
   InputThreadTaskRunner()->PostTask(FROM_HERE, std::move(init_closure));
 }
 
-WidgetInputHandlerManager::~WidgetInputHandlerManager() = default;
+WidgetInputHandlerManager::~WidgetInputHandlerManager() {
+  if (destruction_callback_for_testing_) {
+    std::move(destruction_callback_for_testing_).Run();
+  }
+}
 
 void WidgetInputHandlerManager::AddInterface(
     mojo::PendingReceiver<mojom::blink::WidgetInputHandler> receiver) {
@@ -572,13 +576,13 @@
 void WidgetInputHandlerManager::PostHandwritingRadiusToInputThread(
     int handwriting_radius) {
   base::OnceClosure init_closure = base::BindOnce(
-      [](base::WeakPtr<WidgetInputHandlerManager> weak_ptr, int radius) {
-        if (weak_ptr && weak_ptr->input_handler_proxy_) {
-          weak_ptr->input_handler_proxy_->SetHandwritingRadiusOnInputThread(
+      [](scoped_refptr<WidgetInputHandlerManager> manager, int radius) {
+        if (manager->input_handler_proxy_) {
+          manager->input_handler_proxy_->SetHandwritingRadiusOnInputThread(
               radius);
         }
       },
-      weak_ptr_factory_.GetWeakPtr(), handwriting_radius);
+      scoped_refptr<WidgetInputHandlerManager>(this), handwriting_radius);
   InputThreadTaskRunner()->PostTask(FROM_HERE, std::move(init_closure));
 }
 
@@ -788,7 +792,7 @@
 
   auto redraw_complete_callback =
       base::BindOnce(&WidgetInputHandlerManager::InvokeInputProcessedCallback,
-                     manager->AsWeakPtr());
+                     scoped_refptr<WidgetInputHandlerManager>(manager));
 
   // Since wheel-events can kick off animations, we can not consider
   // all observable effects of an input gesture to be processed
@@ -1207,6 +1211,14 @@
 
 void WidgetInputHandlerManager::ClearClient() {
   input_event_queue_->ClearClient();
+  {
+    base::AutoLock lock(viz_host_lock_);
+    if (viz_host_) {
+      viz_host_.Disconnect();
+    }
+    viz_host_.reset();
+  }
+  host_.reset();
 }
 
 void WidgetInputHandlerManager::UpdateBrowserControlsState(
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 0d59973..66d7507a 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
@@ -211,8 +211,8 @@
   void SetInputHandlerProxyForTesting(
       std::unique_ptr<InputHandlerProxy> input_handler_proxy);
 
-  base::WeakPtr<WidgetInputHandlerManager> AsWeakPtr() {
-    return weak_ptr_factory_.GetWeakPtr();
+  void set_destruction_callback_for_testing(base::OnceClosure callback) {
+    destruction_callback_for_testing_ = std::move(callback);
   }
 
   uint16_t suppressing_input_events_state() const {
@@ -229,6 +229,7 @@
   ~WidgetInputHandlerManager() override;
 
   void InitInputHandler();
+
   void InitOnInputHandlingThread(
       const base::WeakPtr<cc::CompositorDelegateForInput>& compositor_delegate,
       bool sync_compositing);
@@ -404,7 +405,7 @@
 
   std::atomic<bool> dev_tools_session_attached_ = false;
 
-  base::WeakPtrFactory<WidgetInputHandlerManager> weak_ptr_factory_{this};
+  base::OnceClosure destruction_callback_for_testing_;
 };
 
 }  // namespace blink
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 734498d..56f6111a 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
@@ -12,9 +12,11 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/task/thread_pool.h"
 #include "base/test/bind.h"
+#include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/thread_restrictions.h"
@@ -37,8 +39,7 @@
 #include "third_party/blink/renderer/platform/widget/input/mock_input_handler_proxy_client.h"
 #include "third_party/blink/renderer/platform/widget/widget_base.h"
 #include "third_party/blink/renderer/platform/wtf/functional.h"
-
-namespace blink ::test {
+namespace blink::test {
 
 class WidgetInputHandlerManagerTest : public testing::Test {
  public:
@@ -137,14 +138,14 @@
           /*input_event_queue=*/nullptr),
       std::move(frame_widget_input_handler_receiver));
 
-  // WidgetBase isn't setup with full mocks. Were we to call this with
-  // `uses_input_hanlder_=true` we'd attempt to access `LayerHostTree` which
-  // we can't mock in WidgetBase. So we set to false, and use test override
-  // to enable the desired dispach mode.
+  // WidgetBase isn't setup with full mocks. Were we to call `InitInputHandler`
+  // we'd attempt to access `LayerHostTree` which we can't mock in WidgetBase.
+  // So we do not call it, but we set needs_input_handler=true to satisfy
+  // DCHECKs.
   widget_input_handler_manager_ = WidgetInputHandlerManager::Create(
       widget_base_->GetWeakPtr(), frame_widget_input_handler_, never_composited,
       /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
-      /*needs_input_handler=*/false,
+      /*uses_input_handler=*/false,
       /*allow_scroll_resampling=*/false,
       /*io_thread_id=*/base::kInvalidThreadId,
       /*main_thread_id=*/base::PlatformThread::CurrentId());
@@ -167,7 +168,7 @@
           widget_base_->GetWeakPtr(), frame_widget_input_handler_,
           /*never_composited=*/false,
           /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
-          /*needs_input_handler=*/false,
+          /*uses_input_handler=*/false,
           /*allow_scroll_resampling=*/false,
           /*io_thread_id=*/base::kInvalidThreadId,
           /*main_thread_id=*/base::PlatformThread::CurrentId());
@@ -215,4 +216,67 @@
   }
 }
 
+TEST_F(WidgetInputHandlerManagerTest, NoLeakWithoutDisconnect) {
+  scoped_refptr<WidgetInputHandlerManager> manager =
+      WidgetInputHandlerManager::Create(
+          widget_base_->GetWeakPtr(), frame_widget_input_handler_,
+          /*never_composited=*/false,
+          /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
+          /*uses_input_handler=*/false,
Loading diff…

Regression Test / PoC

shipped with the fix
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 734498d..56f6111a 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
@@ -12,9 +12,11 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/task/thread_pool.h"
 #include "base/test/bind.h"
+#include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/thread_restrictions.h"
@@ -37,8 +39,7 @@
 #include "third_party/blink/renderer/platform/widget/input/mock_input_handler_proxy_client.h"
 #include "third_party/blink/renderer/platform/widget/widget_base.h"
 #include "third_party/blink/renderer/platform/wtf/functional.h"
-
-namespace blink ::test {
+namespace blink::test {
 
 class WidgetInputHandlerManagerTest : public testing::Test {
  public:
@@ -137,14 +138,14 @@
           /*input_event_queue=*/nullptr),
       std::move(frame_widget_input_handler_receiver));
 
-  // WidgetBase isn't setup with full mocks. Were we to call this with
-  // `uses_input_hanlder_=true` we'd attempt to access `LayerHostTree` which
-  // we can't mock in WidgetBase. So we set to false, and use test override
-  // to enable the desired dispach mode.
+  // WidgetBase isn't setup with full mocks. Were we to call `InitInputHandler`
+  // we'd attempt to access `LayerHostTree` which we can't mock in WidgetBase.
+  // So we do not call it, but we set needs_input_handler=true to satisfy
+  // DCHECKs.
   widget_input_handler_manager_ = WidgetInputHandlerManager::Create(
       widget_base_->GetWeakPtr(), frame_widget_input_handler_, never_composited,
       /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
-      /*needs_input_handler=*/false,
+      /*uses_input_handler=*/false,
       /*allow_scroll_resampling=*/false,
       /*io_thread_id=*/base::kInvalidThreadId,
       /*main_thread_id=*/base::PlatformThread::CurrentId());
@@ -167,7 +168,7 @@
           widget_base_->GetWeakPtr(), frame_widget_input_handler_,
           /*never_composited=*/false,
           /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
-          /*needs_input_handler=*/false,
+          /*uses_input_handler=*/false,
           /*allow_scroll_resampling=*/false,
           /*io_thread_id=*/base::kInvalidThreadId,
           /*main_thread_id=*/base::PlatformThread::CurrentId());
@@ -215,4 +216,67 @@
   }
 }
 
+TEST_F(WidgetInputHandlerManagerTest, NoLeakWithoutDisconnect) {
+  scoped_refptr<WidgetInputHandlerManager> manager =
+      WidgetInputHandlerManager::Create(
+          widget_base_->GetWeakPtr(), frame_widget_input_handler_,
+          /*never_composited=*/false,
+          /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
+          /*uses_input_handler=*/false,
+          /*allow_scroll_resampling=*/false,
+          /*io_thread_id=*/base::kInvalidThreadId,
+          /*main_thread_id=*/base::PlatformThread::CurrentId());
+
+  mojo::PendingRemote<mojom::blink::WidgetInputHandlerHost> viz_host_remote;
+  auto receiver = viz_host_remote.InitWithNewPipeAndPassReceiver();
+
+  manager->SetVizHost(std::move(viz_host_remote));
+
+  base::RunLoop run_loop;
+  manager->set_destruction_callback_for_testing(run_loop.QuitClosure());
+
+  // Clear client (simulating shutdown) which should break the ref cycle.
+  manager->ClearClient();
+
+  // Drop the main reference.
+  manager = nullptr;
+
+  // Wait until the manager is destroyed.
+  run_loop.Run();
+}
+
+TEST_F(WidgetInputHandlerManagerTest, ClearClientBreaksCycleEvenIfCopiesExist) {
+  scoped_refptr<WidgetInputHandlerManager> manager =
+      WidgetInputHandlerManager::Create(
+          widget_base_->GetWeakPtr(), frame_widget_input_handler_,
+          /*never_composited=*/false,
+          /*compositor_thread_scheduler=*/nullptr, widget_scheduler_,
+          /*uses_input_handler=*/false,
+          /*allow_scroll_resampling=*/false,
+          /*io_thread_id=*/base::kInvalidThreadId,
+          /*main_thread_id=*/base::PlatformThread::CurrentId());
+
+  mojo::PendingRemote<mojom::blink::WidgetInputHandlerHost> viz_host_remote;
+  auto receiver = viz_host_remote.InitWithNewPipeAndPassReceiver();
+
+  manager->SetVizHost(std::move(viz_host_remote));
+
+  // Get a copy of the SharedRemote and keep it alive in the test.
+  auto viz_host_copy = manager->GetVizWidgetInputHandlerHost();
+
+  base::RunLoop run_loop;
+  manager->set_destruction_callback_for_testing(run_loop.QuitClosure());
+
+  // Call ClearClient(). If the fix is active, this should call
+  // viz_host_.Disconnect() and break the cycle even though viz_host_copy is
+  // still alive.
+  manager->ClearClient();
+
+  // Drop the main reference.
+  manager = nullptr;
+
+  // The manager should be destroyed.
+  run_loop.Run();
+}
+
 }  // namespace blink::test
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in WidgetInputHandlerManager due to cross-thread WeakPtr race

Flapjack, 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 data race exists in WidgetInputHandlerManager when a base::WeakPtr bound on the main thread is evaluated on the compositor thread. In release builds, this bypasses sequence validation and creates a Time-of-Check to Time-of-Use (TOCTOU) race condition during object destruction, leading to a potential Use-After-Free (UAF) that is not protected by MiraclePtr.

Affected files:

  • third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc

Estimated timestamp from git blame: 2025-06-25

Description

A potential Use-After-Free (UAF) vulnerability exists in WidgetInputHandlerManager due to the unsafe cross-thread usage of base::WeakPtr. WidgetInputHandlerManager manages input handling between the main thread and the compositor thread in the renderer process.

base::WeakPtr and its factory are thread-affine: they must be created, checked, and invalidated on the same sequence. WidgetInputHandlerManager creates its weak_ptr_factory_ on the main thread but passes WeakPtrs to tasks that execute on the compositor thread. Specifically, SetVizHost binds OnVizHostDisconnected to a disconnect handler using AsWeakPtr() and posts it to the compositor thread.

In release builds, DCHECK_CALLED_ON_VALID_SEQUENCE is compiled out. When a WeakPtr is evaluated, validity is determined solely by reading an atomic invalidated_ flag. If the WidgetInputHandlerManager is destructed on the main thread concurrently with the compositor thread evaluating the WeakPtr, a Time-of-Check to Time-of-Use (TOCTOU) data race occurs. The compositor thread can read invalidated_ as false just microseconds before the main thread destructor sets it to true and frees the object.

Crucially, base::WeakPtr internally uses RAW_PTR_EXCLUSION for the underlying raw pointer. This means the vulnerable freed pointer is completely invisible to MiraclePtr (BackupRefPtr), offering no mitigation against exploitation.

Potential Exploitation Scenario

While our tooling cannot run code, the following steps outline a theoretical exploit path for an attacker:

  1. Setup: The attacker embeds an iframe in a malicious webpage, initializing a WidgetBase and WidgetInputHandlerManager on the main thread. Mojo connections to the Viz and Browser processes are established.
  2. Trigger: The attacker deliberately causes the GPU/Viz process to crash (e.g., via WebGL), triggering a Mojo disconnect on viz_host_. The Mojo system schedules the bound OnVizHostDisconnected task to run on the renderer’s compositor thread.
  3. Race: Concurrently, the attacker uses JavaScript to remove the iframe (iframe.remove()). This destroys the RenderWidgetHostImpl on the browser side, which closes the WidgetInputHandler pipe, causing the compositor thread to drop its scoped_refptr to the manager.
  4. Destruction: The iframe removal also posts a cleanup task to the main thread, dropping the final scoped_refptr to the manager and invoking its destructor.
  5. TOCTOU: The compositor thread begins evaluating the WeakPtr for the OnVizHostDisconnected task. It reads the invalidated_ flag as valid just before the main thread completes the manager’s destructor and frees the memory.
  6. UAF and Hijack: The attacker uses JavaScript on the main thread to heap spray the freed memory with a fake object payload. The compositor thread proceeds to invoke OnVizHostDisconnected using the attacker-controlled memory as the this pointer.
  7. RCE: Inside OnVizHostDisconnected, calling viz_host_.reset() accesses a fake mojo::SharedRemote pointer from the sprayed memory. Dropping the reference count on this fake pointer triggers an attacker-controlled virtual destructor call, leading to arbitrary Remote Code Execution (RCE) in the sandboxed renderer.

Since WidgetInputHandlerManager inherits from ThreadSafeRefCounted, tasks posted across thread boundaries should capture a scoped_refptr<WidgetInputHandlerManager>(this) rather than a base::WeakPtr. This will ensure the object remains safely alive for the duration of the cross-thread task. Alternatively, CrossThreadWeakHandle should be used if the task must not extend the object’s lifetime.

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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