Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker507258648
Fix commitb76a9f8c7322 (chromium/src) +109/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc
modified
GetVizWidgetInputHandlerHost
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager.cc
modified
TEST_P
third_party/blink/renderer/platform/widget/input/widget_input_handler_manager_unittest.cc
modified
for
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 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();
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 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(),
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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.

  1. The attacker loads a malicious webpage on a platform where input::features::kInputOnViz is enabled (e.g., Android).
  2. The attacker’s JavaScript intentionally triggers a GPU process crash (e.g., via a WebGL resource exhaustion technique).
  3. The browser process recovers the GPU process and sends a new viz_host IPC to the renderer, resulting in the main thread executing WidgetInputHandlerManager::SetVizHost().
  4. Concurrently, the attacker synthesizes a gesture scroll event, which is processed by the renderer’s compositor thread.
  5. The compositor thread executes WidgetInputHandlerManager::DidStartScrollingViewport() and calls GetVizWidgetInputHandlerHost().
  6. The compositor thread extracts the raw pointer (viz_host_.get()) just before being preempted by the OS scheduler.
  7. The main thread executes viz_host_.reset() inside SetVizHost(), destroying the underlying proxy object and freeing the memory.
  8. 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.
  9. 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

  1. Introduce a base::Lock (e.g., viz_host_lock_) in WidgetInputHandlerManager to synchronize all reads, assignments, and resets of viz_host_.
  2. Modify GetVizWidgetInputHandlerHost() to return a copy of the mojo::SharedRemote rather 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.

View on issue tracker