Critical watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in HID
DescriptionUse after free in HID
ComponentHID
Sub-componentHID
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker495999127
Fix commit907bec059362 (chromium/src) +69/-4
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-05-04 (public git)
Disclosed2026-05-12
Patch gap8 days — fix public in source before the advisory

Background

`HidConnectionWin`
the Windows implementation of a WebHID device connection, which issues overlapped (asynchronous) read/write I/O to the device handle.
`PendingHidTransfer`
a per-I/O object owning the OVERLAPPED structure and the data buffer handed to the Windows kernel for an in-flight asynchronous read/write, and watching the overlapped event for completion.
`OVERLAPPED` / overlapped I/O
a Windows mechanism where the kernel continues writing to the caller-supplied buffer and OVERLAPPED struct after the API returns, signaling an event when done; that memory must stay valid until completion.
`base::win::ObjectWatcher` / `DeleteSoon`
a helper that invokes OnObjectSignaled when a kernel event fires, and a task-runner method that defers an object’s destruction to a later task on the current sequence.

Root Cause Analysis

While an overlapped read or write was in flight, the kernel retained pointers into the PendingHidTransfer’s OVERLAPPED structure and buffer and would continue to write to them until it signaled completion. On connection close, the pre-patch PlatformClose path simply called transfers_.clear(), which synchronously destroyed every PendingHidTransfer — and thus freed the still-registered OVERLAPPED and buffer — even though the kernel had not yet finished the asynchronous operation, so the kernel would later write into freed memory (a use-after-free).

The fix adopts a self-owning pattern: Abort() sets is_aborted_ (under the kSafeHidConnectionWinClose feature) and runs the completion callback, and close now drains transfers_ by calling Abort() on each in a while (!transfers_.empty()) loop rather than destroying them outright. An aborted transfer is detached (transfer.release()) by the completion handlers so it outlives the connection, and it only self-destructs via DeleteSoon(this) inside OnObjectSignaled once the kernel actually signals the event — guaranteeing the OVERLAPPED and buffer stay valid for the full duration of the kernel operation.

Key insight
The connection destroyed PendingHidTransfer objects (freeing their OVERLAPPED and buffer) while the Windows kernel was still writing into them; the fix makes aborted transfers self-owning so they are deleted only after the kernel signals completion, rather than at close time.

Attack Path

  1. Open a HID connection and start I/O Web content (via WebHID) opens a device and issues a read/write, creating a PendingHidTransfer whose OVERLAPPED and buffer are registered with the kernel for an asynchronous operation.
  2. Close the connection mid-transfer The connection is closed while that operation is still outstanding, invoking the platform close path.
  3. Premature free (pre-patch) transfers_.clear() destroys the PendingHidTransfer, freeing the OVERLAPPED structure and buffer that the kernel still holds references to.
  4. Kernel writes into freed memory When the asynchronous read/write later completes, the kernel writes transferred bytes/status into the now-freed OVERLAPPED/buffer, corrupting freed heap memory (any further exploitation primitive is inferred; the diff shows only the premature free).

Impact Assessment

An attacker gains a use-after-free in the browser-process device service on Windows, where the OS kernel writes into memory freed when a WebHID connection is closed during an in-flight transfer; the vendor advisory rates it critical and labels it a use-after-free in HID. Preconditions include the Windows platform, the WebHID API, and a device the user has granted the page access to, plus a close that races an outstanding overlapped operation; any escalation beyond the freed-memory write is (inferred). The fix ships enabled by default behind kSafeHidConnectionWinClose.

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

Audit Directions

  • Overlapped buffers freed at teardown
    Audit every Windows overlapped-I/O caller that owns the OVERLAPPED/buffer for code paths (close, cancel, destructor) that free those objects without first confirming the kernel has completed or been told to cancel the operation.
  • Synchronous container clears of in-flight objects
    Flag clear()/destruction of containers holding objects with outstanding async operations; prefer draining via an abort that defers destruction to the completion signal.
  • Companion feature-flagged safe-close fixes
    The sibling kSafeUsbDeviceHandleWinClose and this kSafeHidConnectionWinClose suggest reviewing other services/device Windows backends (serial, USB, gamepad) for the same close-during-overlapped lifetime bug.
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