Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in HID
DescriptionUse after free in HID
ComponentHID
Bug ClassUAF
Tracker495999127
Fix commit907bec059362 (chromium/src) +69/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
if
services/device/hid/hid_connection_win.cc
modified

Files Changed

  • services/device/hid/hid_connection_win.cc
  • services/device/public/cpp/device_features.cc
  • services/device/public/cpp/device_features.h
From 907bec05936296900b17ff38911dff3055ff8aea Mon Sep 17 00:00:00 2001
From: Alvin Ji <[email protected]>
Date: Mon, 04 May 2026 13:03:31 -0700
Subject: [PATCH] hid: Prevent UAF in HidConnectionWin on Close

This change adopts a self-owning pattern for aborted PendingHidTransfer
objects, ensuring that the OVERLAPPED structure and its associated
buffer stay valid until the Windows kernel finishes its asynchronous
read/write.

Change-Id: Id7048b33db6505cd19d02034862102dc69ebc9ca
Bug: 495999127
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7710162
Commit-Queue: Alvin Ji <[email protected]>
Reviewed-by: Matt Reynolds <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1624883}
---

diff --git a/services/device/hid/hid_connection_win.cc b/services/device/hid/hid_connection_win.cc
index ba1a0262..e94b062 100644
--- a/services/device/hid/hid_connection_win.cc
+++ b/services/device/hid/hid_connection_win.cc
@@ -15,6 +15,7 @@
 #include "base/memory/advanced_memory_safety_checks.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/numerics/safe_conversions.h"
+#include "base/task/sequenced_task_runner.h"
 #include "base/win/object_watcher.h"
 #include "components/device_event_log/device_event_log.h"
 #include "services/device/public/cpp/device_features.h"
@@ -65,6 +66,20 @@
 
   void TakeResultFromWindowsAPI(BOOL result);
 
+  void Abort() {
+    if (base::FeatureList::IsEnabled(features::kSafeHidConnectionWinClose)) {
+      is_aborted_ = true;
+    } else {
+      watcher_.StopWatching();
+    }
+
+    if (callback_) {
+      std::move(callback_).Run(this, false);
+    }
+  }
+
+  bool is_aborted() const { return is_aborted_; }
+
   OVERLAPPED* GetOverlapped() { return &overlapped_; }
 
   // Implements base::win::ObjectWatcher::Delegate.
@@ -78,6 +93,7 @@
   OVERLAPPED overlapped_;
   base::win::ScopedHandle event_;
   base::win::ObjectWatcher watcher_;
+  bool is_aborted_ = false;
 };
 
 PendingHidTransfer::PendingHidTransfer(
@@ -107,6 +123,19 @@
 }
 
 void PendingHidTransfer::OnObjectSignaled(HANDLE event_handle) {
+  if (is_aborted_) {
+    // `this` owns itself and will self-destruct now that the OS has signaled
+    // the event. This releases the buffer and OVERLAPPED structure held by
+    // this PendingHidTransfer.
+    base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE, this);
+    return;
+  }
+
+  // PendingHidTransfer holds a reference to the buffer to ensure the kernel has
+  // a valid memory location during the overlapped operation. In the
+  // non-aborted case, we release this reference before running the callback
+  // so that the callback can have exclusive ownership of the buffer.
+  buffer_.reset();
   std::move(callback_).Run(this, true);
 }
 
@@ -144,7 +173,13 @@
     entry->file_handle.Close();
   }
   file_handles_.clear();
-  transfers_.clear();
+
+  // Abort() triggers the completion callback (e.g., OnReadInputReport),
+  // which calls UnlinkTransfer() and removes the entry from |transfers_|.
+  // We use a while loop to safely empty the list as entries are removed.
+  while (!transfers_.empty()) {
+    transfers_.front()->Abort();
+  }
 }
 
 void HidConnectionWin::PlatformWrite(
@@ -243,12 +278,18 @@
     scoped_refptr<base::RefCountedBytes> buffer,
     PendingHidTransfer* transfer_raw,
     bool signaled) {
+  std::unique_ptr<PendingHidTransfer> transfer = UnlinkTransfer(transfer_raw);
+  if (transfer->is_aborted()) {
+    // `transfer` owns itself and will self-destruct when signaled by the OS.
+    transfer.release();
+    return;
+  }
+
   if (!signaled) {
     HID_LOG(DEBUG) << "HID read failed.";
     return;
   }
 
-  auto transfer = UnlinkTransfer(transfer_raw);
   DWORD bytes_transferred;
   if (!GetOverlappedResult(file_handle, transfer->GetOverlapped(),
                            &bytes_transferred, FALSE)) {
@@ -278,13 +319,22 @@
     ReadCallback callback,
     PendingHidTransfer* transfer_raw,
     bool signaled) {
+  std::unique_ptr<PendingHidTransfer> transfer = UnlinkTransfer(transfer_raw);
+  if (transfer->is_aborted()) {
+    // `transfer` owns itself and will self-destruct when signaled by the OS.
+    transfer.release();
+    // Handle the aborted case by signaling failure.
+    std::move(callback).Run(false, nullptr, 0);
+    return;
+  }
+
+  // Handle other erroneous cases.
   if (!signaled) {
     HID_LOG(DEBUG) << "HID read failed.";
     std::move(callback).Run(false, nullptr, 0);
     return;
   }
 
-  auto transfer = UnlinkTransfer(transfer_raw);
   DWORD bytes_transferred;
   if (!GetOverlappedResult(file_handle, transfer->GetOverlapped(),
                            &bytes_transferred, FALSE)) {
@@ -310,13 +360,22 @@
                                        WriteCallback callback,
                                        PendingHidTransfer* transfer_raw,
                                        bool signaled) {
+  std::unique_ptr<PendingHidTransfer> transfer = UnlinkTransfer(transfer_raw);
+  if (transfer->is_aborted()) {
+    // `transfer` owns itself and will self-destruct when signaled by the OS.
+    transfer.release();
+    // Handle the aborted case by signaling failure.
+    std::move(callback).Run(false);
+    return;
+  }
+
+  // Handle other erroneous cases.
   if (!signaled) {
     HID_LOG(DEBUG) << "HID write failed.";
     std::move(callback).Run(false);
     return;
   }
 
-  auto transfer = UnlinkTransfer(transfer_raw);
   DWORD bytes_transferred;
   if (!GetOverlappedResult(file_handle, transfer->GetOverlapped(),
                            &bytes_transferred, FALSE)) {
diff --git a/services/device/public/cpp/device_features.cc b/services/device/public/cpp/device_features.cc
index 92bc1d0..f4baf7b 100644
--- a/services/device/public/cpp/device_features.cc
+++ b/services/device/public/cpp/device_features.cc
@@ -62,6 +62,11 @@
 // handle is closed.
 BASE_FEATURE(kSafeUsbDeviceHandleWinClose, base::FEATURE_ENABLED_BY_DEFAULT);
 
+// When enabled, HidConnectionWin will ensure that pending OVERLAPPED requests
+// are not deleted until the kernel has signaled completion, even if the
+// connection is closed.
+BASE_FEATURE(kSafeHidConnectionWinClose, base::FEATURE_ENABLED_BY_DEFAULT);
+
 // Defines a feature parameter for the `kWinSystemLocationPermission` feature.
 // This parameter controls the polling interval (in milliseconds) for checking
 // the permission status. The default polling interval is set to 500
diff --git a/services/device/public/cpp/device_features.h b/services/device/public/cpp/device_features.h
index ecb77e18..b9cd547 100644
--- a/services/device/public/cpp/device_features.h
+++ b/services/device/public/cpp/device_features.h
@@ -34,6 +34,7 @@
     kWinSystemLocationPermissionEventBased);
 DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kHidGetFeatureReportFix);
 DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kSafeUsbDeviceHandleWinClose);
+DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kSafeHidConnectionWinClose);
 
 extern const DEVICE_FEATURES_EXPORT base::FeatureParam<int>
     kWinSystemLocationPermissionPollingParam;
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in HidConnectionWin::PlatformClose due to async CancelIo without wait

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential use-after-free vulnerability exists in HidConnectionWin::PlatformClose where pending asynchronous I/O is canceled using CancelIo without waiting for kernel cancellation to finish. The associated OVERLAPPED structures are immediately freed, allowing the Windows kernel to perform a write-after-free into the browser process’s heap when it writes the delayed I/O completion status. This kernel-originated memory corruption bypasses MiraclePtr protections and could lead to sandbox escape and remote code execution.

Affected files:

  • services/device/hid/hid_connection_win.cc

Estimated timestamp from git blame: 2020-12-29

Description

A potential use-after-free (UAF) vulnerability exists in HidConnectionWin::PlatformClose within services/device/hid/hid_connection_win.cc. The implementation calls the Windows CancelIo API to stop pending asynchronous I/O but immediately proceeds to free the underlying OVERLAPPED structures and data buffers without ensuring that the Windows kernel has finished using them.

Technical Details

In HidConnectionWin::PlatformClose, the code iterates through open file handles, calls CancelIo, closes the handle, and then immediately clears the transfers_ list:

void HidConnectionWin::PlatformClose() {
  for (auto& entry : file_handles_) {
    CancelIo(entry->file_handle.Get());
    entry->file_handle.Close();
  }
  file_handles_.clear();
  transfers_.clear();
}

On Windows, CancelIo is an asynchronous operation. It marks pending I/O requests for cancellation and returns immediately; it does not synchronously wait for the kernel to finish canceling the I/O Request Packet (IRP). The kernel must still complete the IRP, which involves asynchronously writing the final status (e.g., STATUS_CANCELLED) and the number of bytes transferred to the Internal and InternalHigh fields of the provided OVERLAPPED structure.

The transfers_.clear() call immediately destroys the PendingHidTransfer objects. These objects contain inline OVERLAPPED structures and hold references to the RefCountedBytes buffers used for the I/O. Since CancelIo does not wait, the kernel continues to hold the raw user-mode virtual address of the OVERLAPPED structure and the buffer. When the kernel completes the cancellation, it will attempt to write the final status to these memory locations after they have been freed and potentially reallocated by PartitionAlloc.

Impact and Severity

This issue results in a highly reliable, kernel-originated heap write-after-free in the browser process (the Device Service is hosted in the browser process on Windows). Because the Windows kernel writes directly to the raw user-mode virtual address of the OVERLAPPED structure, Chrome’s MiraclePtr (BackupRefPtr) protection is entirely bypassed as it only protects C++ raw_ptr<T> types, not raw memory addresses held by the OS.

An attacker who has obtained WebHID permissions (granted via a user prompt) can trigger this vulnerability by closing the HID connection while I/O is pending. By grooming the heap, an attacker could place sensitive objects (e.g., objects with vtables) in the reclaimed memory slots before the kernel write occurs, potentially leading to a full sandbox escape and remote code execution.

Potential Attacker Steps (Unverified)

(Note: These are potential steps as a working proof-of-concept has not been run.)

  1. A malicious webpage requests and receives WebHID access via navigator.hid.requestDevice(), which prompts the user.
  2. The attacker opens a connection to the device using device.open(), creating a HidConnectionWin in the browser process which automatically initiates asynchronous reads.
  3. The attacker deliberately closes the connection (e.g., via device.close() or by destroying the Mojo remote) while read operations are still pending.
  4. HidConnectionWin::PlatformClose() calls CancelIo and immediately frees the PendingHidTransfer objects.
  5. The attacker rapidly allocates objects of similar size in the browser process to reclaim the freed memory slots.
  6. The Windows kernel finishes canceling the I/O and writes STATUS_CANCELLED to the freed OVERLAPPED structure, corrupting the attacker’s newly allocated object.

The implementation must ensure that all pending I/O operations have reached a terminal state before the associated OVERLAPPED structures and buffers are destroyed. This can be achieved by iterating through the pending transfers and synchronously waiting for their completion after calling CancelIo.

For example, calling GetOverlappedResult with bWait=TRUE for all pending transfers, or waiting for the associated event handle (overlapped_.hEvent) to become signaled, will ensure the kernel is completely finished with the memory before it is freed.

Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker