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

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Gamepad
DescriptionUse after free in Gamepad
ComponentGamepad
Sub-componentGamepad
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.
Tracker516975148
Fix commita5e41fa34b2c (chromium/src) +6/-12
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-06-02 (public git)
Disclosed2026-06-08
Patch gap6 days — fix public in source before the advisory

Background

`HidWriterWin`
the Windows gamepad backend that writes HID output reports (e.g. rumble/force-feedback) to a device via an overlapped (asynchronous) file handle.
`OVERLAPPED` structure
a Win32 struct the kernel/driver writes completion status into; here it is stack-allocated inside WriteOutputReport, so its lifetime ends when the function returns.
`CancelIo` / `GetOverlappedResult`
CancelIo requests cancellation of pending I/O on a handle; GetOverlappedResult(..., bWait=true) blocks until that specific overlapped operation is actually retired (completed or aborted).
`WaitForMultipleObjects(bWaitAll=false)`
returns as soon as any one of the supplied handles is signaled, which may be a stale/previous signal rather than the current operation’s completion.

Root Cause Analysis

HidWriterWin::WriteOutputReport performs an overlapped write using a stack-allocated OVERLAPPED. On timeout it called CancelIo and then WaitForMultipleObjects(2, {hid_handle_, overlapped.hEvent}, false, INFINITE) with bWaitAll=false, intending to block until cancellation finished. Because the file handle could already be in a signaled state from a prior operation, WaitForMultipleObjects could return immediately — before the current write’s cancellation was retired — so the function returned and tore down the stack frame while the driver still held the address of the now-dead OVERLAPPED. When the driver later completed the cancellation it wrote status into that invalid stack address, corrupting memory (a use-after-return).

The fix removes the WaitForMultipleObjects path and, after optionally CancelIo-ing on non-WAIT_OBJECT_0, calls GetOverlappedResult(hid_handle_, &overlapped, &bytes_written, true), which blocks on the specific operation until it is fully retired; the OVERLAPPED therefore outlives all driver writes to it.

Key insight
The old code used WaitForMultipleObjects with bWaitAll=false on a handle that could be pre-signaled, so it stopped waiting before the cancelled write finished; replacing it with GetOverlappedResult(..., bWait=true) ties the wait to the exact operation and guarantees the stack OVERLAPPED stays valid until the driver is done with it.

Attack Path

  1. Trigger an output write A gamepad with force-feedback/rumble is present and the write path in WriteOutputReport issues an overlapped write whose completion is delayed past the 100 ms WaitForSingleObject timeout.
  2. Cancel and mis-wait On timeout the old code calls CancelIo then WaitForMultipleObjects(bWaitAll=false), which returns immediately because the file handle is already signaled from a previous operation, before the current cancellation completes.
  3. Return and free the frame WriteOutputReport returns, destroying the stack frame containing the OVERLAPPED.
  4. Driver writes to dead stack The driver finishes the cancellation and writes completion status into the freed OVERLAPPED address, corrupting the stack (inferred that this is attacker-usable beyond corruption; the patch demonstrates only the invalid write).

Impact Assessment

An attacker who can drive gamepad output (web content via the Gamepad API causing rumble, or a malicious/colluding HID device) on Windows can race the write-timeout path to cause a driver write into a freed stack frame of the browser/device process. The vendor advisory rates this critical and does not report in-the-wild exploitation; the bug is Windows-specific and timing-dependent. Escalation from the memory corruption to control of execution is (inferred) and not shown by the diff.

Changed Functions

FunctionChangeNotes
if
device/gamepad/hid_writer_win.cc
modified

Files Changed

  • device/gamepad/hid_writer_win.cc

Audit Directions

  • Stack-allocated `OVERLAPPED` lifetimes
    audit every overlapped-I/O site for stack or short-lived OVERLAPPED/buffer structures and confirm the function blocks on the specific operation (GetOverlappedResult(..., TRUE)) before returning.
  • `WaitForMultipleObjects(bWaitAll=false)` on reusable handles
    flag waits that key on a file/device handle that can carry stale signals from prior operations; such waits do not prove the current operation finished.
  • Cancellation-then-return paths
    review CancelIo/CancelIoEx call sites to ensure cancellation is awaited to completion rather than fire-and-return while the kernel still owns caller-provided memory.
From a5e41fa34b2c37843ba3e82b51b7323f32b3763a Mon Sep 17 00:00:00 2001
From: Rob Pitkin <[email protected]>
Date: Tue, 02 Jun 2026 12:46:44 -0700
Subject: [PATCH] gamepad: Fix Use-After-Return in HidWriterWin

HidWriterWin::WriteOutputReport uses a stack-allocated OVERLAPPED
structure for asynchronous writes. When a write operation times out, the
function calls CancelIo and then waits for either the file handle or the
event handle to be signaled using WaitForMultipleObjects with bWaitAll =
false.

If the file handle was signaled by a previous operation,
WaitForMultipleObjects returns immediately, before the current operation
is fully cancelled. The function then returns, destroying the stack
frame and the OVERLAPPED structure. When the driver eventually completes
the cancellation, it writes status information back to the now-invalid
OVERLAPPED address, causing memory corruption.

This CL fixes the issue by removing the WaitForMultipleObjects call and
instead calling GetOverlappedResult with `bWait = true` after
potentially cancelling the I/O. This ensures that the operation is fully
retired before the function returns and the stack frame is destroyed.

Bug: 516975148
Change-Id: I92116cef7454e35de8d40ccb55b15819eddff79b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7892686
Reviewed-by: Matt Reynolds <[email protected]>
Commit-Queue: Rob Pitkin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1640392}
---

diff --git a/device/gamepad/hid_writer_win.cc b/device/gamepad/hid_writer_win.cc
index 420f637..7dc61c0 100644
--- a/device/gamepad/hid_writer_win.cc
+++ b/device/gamepad/hid_writer_win.cc
@@ -57,19 +57,13 @@
       // Wait for the write to complete. This causes WriteOutputReport to behave
       // synchronously.
       DWORD wait_object = ::WaitForSingleObject(overlapped.hEvent, 100);
-      if (wait_object == WAIT_OBJECT_0) {
-        ::GetOverlappedResult(hid_handle_.Get(), &overlapped, &bytes_written,
-                              true);
-      } else {
-        // Wait failed, or the timeout was exceeded before the write completed.
-        // Cancel the write request.
-        if (::CancelIo(hid_handle_.Get())) {
-          HANDLE handles[2];
-          handles[0] = hid_handle_.Get();
-          handles[1] = overlapped.hEvent;
-          ::WaitForMultipleObjects(2, handles, false, INFINITE);
-        }
+      if (wait_object != WAIT_OBJECT_0) {
+        ::CancelIo(hid_handle_.Get());
       }
+      // This blocks until the specific overlapped operation completes or
+      // aborts.
+      write_success = ::GetOverlappedResult(hid_handle_.Get(), &overlapped,
+                                            &bytes_written, true);
     }
   }
   return write_success ? bytes_written : 0;
Loading diff…

Original Bug Report

reported by [email protected]

Potential stack Use-After-Return in HidWriterWin::WriteOutputReport during I/O cancellation

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An asynchronous write operation in HidWriterWin::WriteOutputReport utilizes a stack-allocated OVERLAPPED structure. If the write times out and is cancelled, a flawed WaitForMultipleObjects call can return prematurely before the I/O cancellation is complete. This allows the kernel to write status information into a stale stack frame, potentially leading to memory corruption in the unsandboxed browser process.

Affected files:

  • device/gamepad/hid_writer_win.cc

Estimated timestamp from git blame: 2018-02-09

Root Cause Analysis

In device/gamepad/hid_writer_win.cc, HidWriterWin::WriteOutputReport manages asynchronous writing to a Windows HID device opened with FILE_FLAG_OVERLAPPED (device/gamepad/hid_writer_win.cc, line 31). It utilizes a stack-allocated OVERLAPPED structure and a stack-scoped event helper object:

base::win::ScopedHandle event_handle(
    ::CreateEvent(nullptr, false, false, L""));
OVERLAPPED overlapped = {0};
overlapped.hEvent = event_handle.Get();

DWORD bytes_written = 0;
BOOL write_success =
    ::WriteFile(hid_handle_.Get(), report.data(), report.size_bytes(),
                &bytes_written, &overlapped);

If the write operation is pending, the function blocks on the event for up to 100 milliseconds. If the timeout is exceeded, the code attempts to cancel the I/O operation via CancelIo and wait for completion:

if (::CancelIo(hid_handle_.Get())) {
  HANDLE handles[2];
  handles[0] = hid_handle_.Get();
  handles[1] = overlapped.hEvent;
  ::WaitForMultipleObjects(2, handles, false, INFINITE); // bWaitAll = false
}

There is a critical race condition in how ::WaitForMultipleObjects is utilized here:

  1. When an overlapped operation is initiated with a valid hEvent in its OVERLAPPED structure, the OS resets the event handle (overlapped.hEvent) to the non-signaled state, but the signaled state of the file handle itself (hid_handle_) remains unchanged.
  2. When a prior overlapped operation completes successfully, the OS signals both the event associated with that operation and the file handle.
  3. On a subsequent write operation (“Call 2”), if a timeout occurs, CancelIo is invoked, and WaitForMultipleObjects is called with bWaitAll = false.
  4. Since the file handle (handles[0]) was signaled by the previous operation and never reset (because Call 2 passed an explicit event handle), WaitForMultipleObjects evaluates immediately as completed and returns.

This premature return occurs before the driver has actually completed the cancellation of Call 2. The stack frame of WriteOutputReport is then unwound, deallocating the local overlapped structure. When the driver eventually cancels/aborts the IRP, the Windows I/O manager writes completion status metadata back to the deallocated OVERLAPPED memory address, corrupting the active stack of the gamepad polling thread in the unsandboxed browser process.

Potential Attacker Steps / Trigger Scenario

Note: The following scenario describes potential steps to trigger this behavior, as our automated security tools do not have the capability to execute code or verify runtime state.

  1. A compromised renderer or standard web page calls the Gamepad Haptics API (navigator.getGamepads()[i].vibrationActuator.playEffect(...)).
  2. The request is bound to the browser process and dispatched to the gamepad polling thread, where HidWriterWin::WriteOutputReport is invoked.
  3. A connected USB or Bluetooth HID device (such as a custom or simulated virtual gamepad) intentionally delays its response on the haptic output endpoint, causing the 100ms timeout to trigger.
  4. A previous successful operation (or another concurrent operation on the handle) ensures that the file handle itself is already signaled in kernel-space.
  5. When CancelIo is called, WaitForMultipleObjects immediately returns due to the signaled file handle, unwinding the stack frame.
  6. The device driver completes the abort/cancellation after the function has exited, causing the kernel to write status flags directly into the modified/reused stack frames of the gamepad polling thread.

Suggested Remediation

To ensure that the stack-allocated OVERLAPPED structure is not deallocated until the I/O operation is guaranteed to have finished or cancelled, avoid waiting on the raw file handle. Instead, use GetOverlappedResult with bWait = TRUE after triggering cancellation, which safely blocks until the specific overlapped transaction has been retired by the kernel:

::CancelIo(hid_handle_.Get());
DWORD bytes = 0;
// This blocks until the specific overlapped operation completes or aborts
::GetOverlappedResult(hid_handle_.Get(), &overlapped, &bytes, TRUE);

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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