CVE-2026-11009
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/device/usb/usb_device_handle_win.cc |
modified | |
UsbDeviceHandleWinservices/device/usb/usb_device_handle_win.h |
modified | |
Requestservices/device/usb/usb_device_handle_win.h |
modified |
Files Changed
services/device/public/cpp/device_features.ccservices/device/public/cpp/device_features.hservices/device/usb/usb_device_handle_win.ccservices/device/usb/usb_device_handle_win.h
Patch
From 67170c6099d8e3384de7e05617a7877c293df5a7 Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> Date: Fri, 24 Apr 2026 12:48:42 -0700 Subject: [PATCH] usb: Prevent UAF in UsbDeviceHandleWin on Close Ensures pending WinUSB OVERLAPPED requests and their data buffers remain alive until the kernel signals completion, even if the handle is closed. - Introduces `kSafeUsbDeviceHandleWinClose` feature flag. - Moves aborted requests to a global list until the OS signals completion. - Ensures `Request` objects hold a reference to the transfer buffer to prevent memory corruption. Change-Id: I8ac1dc4ca2a6dabb95c92ef80b9a46cb5bffb57f Bug: 496233132 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7710634 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1620405} --- diff --git a/services/device/public/cpp/device_features.cc b/services/device/public/cpp/device_features.cc index c25a717..92bc1d0 100644 --- a/services/device/public/cpp/device_features.cc +++ b/services/device/public/cpp/device_features.cc @@ -57,6 +57,11 @@ // start of the report and truncate the last byte of the report. BASE_FEATURE(kHidGetFeatureReportFix, base::FEATURE_ENABLED_BY_DEFAULT); +// When enabled, UsbDeviceHandleWin will ensure that pending OVERLAPPED requests +// are not deleted until the kernel has signaled completion, even if the +// handle is closed. +BASE_FEATURE(kSafeUsbDeviceHandleWinClose, 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 e358bac..ecb77e18 100644 --- a/services/device/public/cpp/device_features.h +++ b/services/device/public/cpp/device_features.h @@ -33,6 +33,7 @@ DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE( kWinSystemLocationPermissionEventBased); DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kHidGetFeatureReportFix); +DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kSafeUsbDeviceHandleWinClose); extern const DEVICE_FEATURES_EXPORT base::FeatureParam<int> kWinSystemLocationPermissionPollingParam; diff --git a/services/device/usb/usb_device_handle_win.cc b/services/device/usb/usb_device_handle_win.cc index f3c3d7c..25928bf 100644 --- a/services/device/usb/usb_device_handle_win.cc +++ b/services/device/usb/usb_device_handle_win.cc @@ -30,6 +30,7 @@ #include "base/threading/scoped_blocking_call.h" #include "base/win/object_watcher.h" #include "components/device_event_log/device_event_log.h" +#include "services/device/public/cpp/device_features.h" #include "services/device/public/cpp/usb/usb_utils.h" #include "services/device/usb/usb_context.h" #include "services/device/usb/usb_descriptors.h" @@ -155,7 +156,9 @@ void MaybeStartWatching( BOOL success, DWORD last_error, + scoped_refptr<base::RefCountedBytes> buffer, base::OnceCallback<void(Request*, DWORD, size_t)> callback) { + buffer_ = std::move(buffer); callback_ = std::move(callback); if (success) { OnObjectSignaled(event_.Get()); @@ -168,21 +171,45 @@ } void Abort() { + if (base::FeatureList::IsEnabled(features::kSafeUsbDeviceHandleWinClose)) { + is_aborted_ = true; + if (callback_) { + std::move(callback_).Run(this, ERROR_REQUEST_ABORTED, 0); + } + return; + } watcher_.StopWatching(); std::move(callback_).Run(this, ERROR_REQUEST_ABORTED, 0); } OVERLAPPED* overlapped() { return &overlapped_; } int interface_number() const { return interface_number_; } + bool is_aborted() const { return is_aborted_; } // base::win::ObjectWatcher::Delegate void OnObjectSignaled(HANDLE object) override { DCHECK_EQ(object, event_.Get()); + + 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 Request. + base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE, + this); + return; + } + DWORD size; BOOL result = WinUsb_GetOverlappedResult(handle_, &overlapped_, &size, true); DWORD last_error = GetLastError(); + // Request 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 (which also holds a reference) can have exclusive ownership of + // the buffer. + buffer_.reset(); if (result) std::move(callback_).Run(this, ERROR_SUCCESS, size); else @@ -196,6 +223,12 @@ base::win::ScopedHandle event_; base::win::ObjectWatcher watcher_; base::OnceCallback<void(Request*, DWORD, size_t)> callback_; + // This buffer is held to ensure that the memory stays alive until the kernel + // has signaled completion of the overlapped I/O operation. In the abort case + // the Request owns itself until completion, and this will be the only + // reference to the buffer as the transfer callback has already been invoked. + scoped_refptr<base::RefCountedBytes> buffer_; + bool is_aborted_ = false; }; UsbDeviceHandleWin::Interface::Interface() = default; @@ -575,7 +608,7 @@ } DWORD last_error = GetLastError(); request->MaybeStartWatching( - result, last_error, + result, last_error, buffer, base::BindOnce(&UsbDeviceHandleWin::TransferComplete, weak_factory_.GetWeakPtr(), std::move(callback), std::move(buffer))); @@ -989,7 +1022,7 @@ /*LengthTransferred=*/nullptr, control_request->overlapped()); DWORD last_error = GetLastError(); control_request->MaybeStartWatching( - result, last_error, + result, last_error, buffer, base::BindOnce(&UsbDeviceHandleWin::TransferComplete, weak_factory_.GetWeakPtr(), std::move(callback), buffer)); } @@ -1112,6 +1145,11 @@ ReleaseInterfaceReference(&it->second); std::move(callback).Run(status, std::move(buffer), bytes_transferred); + + if (request->is_aborted()) { + // `request` owns itself and will self-destruct when signaled by the OS. + request.release(); + } } void UsbDeviceHandleWin::ReportIsochronousError( diff --git a/services/device/usb/usb_device_handle_win.h b/services/device/usb/usb_device_handle_win.h index 278e407..3125d63 100644 --- a/services/device/usb/usb_device_handle_win.h +++ b/services/device/usb/usb_device_handle_win.h @@ -33,6 +33,8 @@ // UsbDeviceHandle class provides basic I/O related functionalities. class UsbDeviceHandleWin : public UsbDeviceHandle { public: + class Request; + UsbDeviceHandleWin(const UsbDeviceHandleWin&) = delete; UsbDeviceHandleWin& operator=(const UsbDeviceHandleWin&) = delete; @@ -101,7 +103,6 @@ private: struct Interface; - class Request; using OpenInterfaceCallback = base::OnceCallback<void(Interface*)>;
Original Bug Report
Potential Kernel Write-After-Free in UsbDeviceHandleWin via asynchronous CancelIo
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential write-after-free vulnerability exists in the Windows WebUSB implementation. UsbDeviceHandleWin::Close() destroys Request objects immediately after calling CancelIo(), without waiting for the kernel to finish cancelling the IRPs. The Windows kernel can subsequently write the cancellation status into the freed memory, allowing an attacker to corrupt browser heap memory.
Affected files:
services/device/usb/usb_device_handle_win.cc
Estimated timestamp from git blame: 2025-11-10
Summary
There is a potential kernel-mode Write-After-Free (WAF) vulnerability in the Windows implementation of WebUSB (services/device/usb/usb_device_handle_win.cc). When a USB device handle is closed, the code attempts to cancel pending I/O by calling the Windows CancelIo() API, followed immediately by the synchronous destruction of the associated Request objects.
Because CancelIo() is an asynchronous operation that merely requests cancellation, the Windows kernel may still be processing the I/O Request Packet (IRP). When the kernel eventually completes the cancellation, it writes the completion status (STATUS_CANCELLED, 0xC0000120) to the user-space OVERLAPPED structure. Since the browser process has already freed the Request object containing this structure, the kernel writes into freed memory.
Because the write is performed directly by the Windows kernel using a raw virtual address, C++ mitigations like MiraclePtr (BackupRefPtr) are completely bypassed.
Root Cause Analysis
In services/device/usb/usb_device_handle_win.cc, the UsbDeviceHandleWin::Close() method executes the following logic:
- It iterates through interfaces and calls
CancelIo(interface->function_handle.Get()). - It immediately enters a
whileloop to abort all pending requests:while (!requests_.empty()) requests_.front()->Abort();. Request::Abort()callswatcher_.StopWatching(). While this blocks until the wait is unregistered from the thread pool, it does not wait for the underlying OS I/O to complete.Abort()then synchronously fires the completion callback, invokingUsbDeviceHandleWin::TransferComplete().TransferComplete()extracts theRequestviaUnlinkRequest()into a localstd::unique_ptr<Request>. When the function returns, theRequestobject (and its embeddedOVERLAPPEDstructure) is destroyed and freed to the PartitionAlloc heap.- The Windows kernel, running asynchronously, finishes cancelling the IRP and writes
0xC0000120to theOVERLAPPED.Internalfield (offset 24 on x64) and0toOVERLAPPED.InternalHigh(offset 32).
Potential Exploitation Steps
Note: These are potential steps as our setup does not have the ability to run code to produce a fully working Proof-of-Concept.
- A malicious website requests and is granted access to a USB device via
navigator.usb.requestDevice(). - The attacker opens the device and claims an interface.
- The attacker initiates a large asynchronous transfer (e.g.,
device.transferIn()), ensuring the I/O pends in the Windows kernel. - The attacker immediately calls
device.close(). - The browser calls
CancelIo()and frees theRequestobject. The kernel begins cancelling the IRP. - The attacker rapidly performs heap spraying (e.g., via other Web APIs or IPCs) to allocate victim objects of the exact same size as the freed
Requestobject, reclaiming the freed memory slot. - The kernel finishes cancellation and writes
0xC0000120into the attacker’s victim object. - If the victim object is carefully chosen (e.g., placing a capacity or length field at offset 24), this WAF grants the attacker a massive Out-of-Bounds (OOB) read/write primitive.
- The attacker uses the OOB primitive to achieve Remote Code Execution (RCE) in the highly privileged Browser process, escaping the renderer sandbox.
Suggested Fix
The lifetime of the Request object (and crucially, the OVERLAPPED structure) must be extended until the Windows kernel signals that it is entirely finished with the memory.
When Close() or Abort() is called, the code should not immediately destroy the Request. Instead, it should mark the request as aborted, stop the ObjectWatcher, but keep the Request object alive in memory (e.g., moved to a separate “pending destruction” list). The Request should only be safely deleted after the kernel signals the event handle associated with the OVERLAPPED structure, confirming that the OS has completed its final writes.
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.