Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in USB
DescriptionUse after free in USB
ComponentUSB
Sub-componentUSB
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.
Tracker516999424
Fix commit91cbfc99801d (chromium/src) +20/-2
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-05-28 (public git)
Disclosed2026-06-30
Patch gap33 days — fix public in source before the advisory

Background

WebUSB
Chrome API letting web content communicate with USB devices through the browser’s device service, where UsbDeviceHandleImpl mediates transfers on macOS.
Isochronous transfer
a time-sensitive USB transfer mode (e.g. audio/video streams) submitted via IsochronousTransferIn/IsochronousTransferOut, distinct from the generic control/bulk path.
Claimed interface
a USB interface that JS has claimed via ClaimInterface; in-flight transfers are associated with it so ReleaseInterface can cancel them before the interface state is torn down.
`endpoint_map_`
the handle’s map of endpoint addresses to their owning claimed interface; membership means the endpoint belongs to a currently-claimed interface and the transfer is trackable/cancellable.

Root Cause Analysis

The generic control/bulk transfer path performed an endpoint_map_ membership lookup before creating a transfer, but IsochronousTransferIn and IsochronousTransferOut omitted this guard.

As a result, an isochronous transfer could be created targeting an endpoint whose claimed interface was NULL, meaning the transfer was never registered under any interface’s cancellation set. When ReleaseInterface ran, it iterated only tracked transfers and therefore did not cancel this orphaned isochronous transfer. The interface/handle state the transfer referenced was then freed while the transfer was still outstanding on macOS, producing a use-after-free in the device service.

The fix adds the endpoint_map_.contains(endpoint_address) check to both isochronous paths so a transfer is only created for an endpoint of a claimed interface, keeping it cancellable.

Key insight
Two code paths that both create cancellable transfers must share the same precondition check: the isochronous path diverged from the generic path by skipping the endpoint_map_ lookup, which silently allowed a transfer to escape the interface-based cancellation lifecycle.

Attack Path

  1. Claim and target Malicious page opens a WebUSB device and identifies an endpoint address whose interface is not claimed (or is in the process of release).
  2. Submit orphaned transfer It calls isochronousTransferIn/isochronousTransferOut on that endpoint, creating a transfer with a NULL claimed interface that no interface tracks.
  3. Release the interface ReleaseInterface runs but skips the untracked isochronous transfer, so it is not cancelled while state is torn down.
  4. Trigger the free The referenced interface/handle state is freed while the isochronous transfer is still outstanding.
  5. UAF The still-live isochronous transfer accesses the freed state on macOS, yielding a use-after-free in the device service.

Impact Assessment

A compromised or malicious renderer with WebUSB access can drive the macOS device service into a use-after-free on freed interface/handle state. This can lead to memory corruption and potential code execution in the device service process, which runs outside the renderer sandbox. Exploitation requires user consent to a WebUSB device, narrowing but not eliminating reach.

Files Changed

  • services/device/usb/usb_device_handle_impl.cc

Audit Directions

  • Divergent preconditions across sibling paths
    When multiple entry points create objects sharing a lifecycle (cancellation, tracking), verify every path enforces the same guard; audit control/bulk vs. isochronous vs. interrupt paths in UsbDeviceHandleImpl for consistency.
  • Untracked resources escaping teardown
    Look for objects created without registration into the structure (endpoint_map_, interface transfer sets) that a teardown routine like ReleaseInterface relies on to cancel or free them.
  • Null-association lifetime assumptions
    Flag code that creates transfers or callbacks against an endpoint/interface without validating a non-null owner, since a NULL claimed interface bypasses ownership-based cleanup.
From 91cbfc99801d49d4e7d8a6349552870f95b48cb0 Mon Sep 17 00:00:00 2001
From: Alvin Ji <[email protected]>
Date: Thu, 28 May 2026 12:39:58 -0700
Subject: [PATCH] usb: prevent UAF in macOS WebUSB isochronous transfers

Add endpoint verification to IsochronousTransferIn and IsochronousTransferOut to ensure the target endpoint is part of a claimed interface before creating the transfer. This prevents transfers from being created with a null claimed interface, which allowed them to bypass cancellation during ReleaseInterface and lead to a Use-After-Free on macOS.

BUG=516999424

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

diff --git a/services/device/usb/usb_device_handle_impl.cc b/services/device/usb/usb_device_handle_impl.cc
index 8a75062f..7e94e425 100644
--- a/services/device/usb/usb_device_handle_impl.cc
+++ b/services/device/usb/usb_device_handle_impl.cc
@@ -783,6 +783,15 @@
 
   uint8_t endpoint_address =
       ConvertTransferDirection(UsbTransferDirection::INBOUND) | endpoint_number;
+  if (!endpoint_map_.contains(endpoint_address)) {
+    USB_LOG(ERROR) << "Failed to submit isochronous transfer because endpoint "
+                   << static_cast<int>(endpoint_address)
+                   << " is not part of a claimed interface.";
+    ReportIsochronousTransferError(std::move(callback), packet_lengths,
+                                   UsbTransferStatus::TRANSFER_ERROR);
+    return;
+  }
+
   size_t length =
       std::accumulate(packet_lengths.begin(), packet_lengths.end(), 0u);
   auto buffer = base::MakeRefCounted<base::RefCountedBytes>(length);
@@ -810,6 +819,15 @@
   uint8_t endpoint_address =
       ConvertTransferDirection(UsbTransferDirection::OUTBOUND) |
       endpoint_number;
+  if (!endpoint_map_.contains(endpoint_address)) {
+    USB_LOG(ERROR) << "Failed to submit isochronous transfer because endpoint "
+                   << static_cast<int>(endpoint_address)
+                   << " is not part of a claimed interface.";
+    ReportIsochronousTransferError(std::move(callback), packet_lengths,
+                                   UsbTransferStatus::TRANSFER_ERROR);
+    return;
+  }
+
   size_t length =
       std::accumulate(packet_lengths.begin(), packet_lengths.end(), 0u);
   std::unique_ptr<Transfer> transfer = Transfer::CreateIsochronousTransfer(
@@ -838,9 +856,9 @@
       ConvertTransferDirection(direction) | endpoint_number;
   const auto endpoint_it = endpoint_map_.find(endpoint_address);
   if (endpoint_it == endpoint_map_.end()) {
-    USB_LOG(DEBUG) << "Failed to submit transfer because endpoint "
+    USB_LOG(ERROR) << "Failed to submit transfer because endpoint "
                    << static_cast<int>(endpoint_address)
-                   << " not part of a claimed interface.";
+                   << " is not part of a claimed interface.";
     task_runner_->PostTask(
         FROM_HERE,
         base::BindOnce(std::move(callback), UsbTransferStatus::TRANSFER_ERROR,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in macOS WebUSB via race in ClaimInterface and IsochronousTransfer

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: A potential race condition exists in the macOS WebUSB implementation during the asynchronous window of interface claiming. An isochronous transfer issued during this gap is created with a null claimed interface, bypassing cancellation filters during ReleaseInterface. This can lead to a Use-After-Free of an IOKit COM proxy inside the Device Service on macOS.

Affected files:

  • services/device/usb/usb_device_handle_impl.cc
  • third_party/libusb/src/libusb/os/darwin_usb.c

Estimated timestamp from git blame: 2016-02-04

Potential Security Issue: Use-After-Free in macOS WebUSB

There is a potential Use-After-Free (UAF) vulnerability in the macOS WebUSB implementation. This issue stems from a combination of incomplete state checking when initializing isochronous transfers and a lack of thread synchronization inside the underlying libusb Darwin backend during concurrent transfer cancellation and interface release.

Root Cause Analysis

  1. Unchecked Transfer Initialization: Unlike GenericTransfer, which explicitly verifies that the target endpoint is present in endpoint_map_ before proceeding (see services/device/usb/usb_device_handle_impl.cc line 831), IsochronousTransferIn and IsochronousTransferOut do not check endpoint_map_. Instead, they immediately call Transfer::CreateIsochronousTransfer (lines 781, 807).

    If an isochronous transfer is requested immediately after ClaimInterface() but before ClaimInterfaceComplete() has executed on the sequence thread, endpoint_map_ is empty. As a result, GetClaimedInterfaceForEndpoint (line 1072) returns nullptr. The transfer is then constructed with its claimed_interface_ member set to nullptr (line 355).

  2. Bypassing Cancellation Filters: When ReleaseInterface() is called, it attempts to cancel all active transfers associated with the target interface claimer (line 642). Because the isochronous transfer’s claimed_interface_ is nullptr, it bypasses this filter and remains active and in-flight.

  3. Unsynchronized Thread Concurrency in libusb on Darwin: When Close() is called, it attempts to cancel the active transfer synchronously on the sequence thread via libusb_cancel_transfer() (line 543). Concurrently, the released InterfaceClaimer destructor runs on the blocking thread pool sequence and executes libusb_release_interface() (line 162).

    These two operations run concurrently without adequate synchronization:

    • libusb_release_interface() acquires the device-level lock dev->lock (core.c line 1581) and calls the Darwin backend’s darwin_release_interface() (darwin_usb.c line 1246), which closes and releases the IOKit IOUSBInterfaceInterface COM proxy.
    • libusb_cancel_transfer() only acquires the transfer lock itransfer->lock (io.c line 1493) and does not block on dev->lock.
    • In darwin_abort_transfers(), the sequence thread calls ep_to_pipeRef(), which sees that the interface bit is still set inside dev->claimed_interfaces (as the release thread has not yet returned to clear it in core.c). It retrieves cInterface and executes a blocking Mach IPC call AbortPipe (line 1691).
    • While the sequence thread is blocked, the blocking thread completes Release(), freeing the underlying COM proxy.
    • Upon returning from the IPC, the sequence thread attempts to call ClearPipeStallBothEnds (line 1696) on the freed interface proxy pointer, resulting in a potential Use-After-Free (UAF).

Potential Step-by-Step Reproduction Scenario

Note: These steps are based on static code analysis and represent a potential exploit path, as our tooling does not currently have the capability to execute code.

  1. A compromised renderer process obtains permission for a WebUSB device with an isochronous endpoint.
  2. The renderer calls ClaimInterface(0) and immediately invokes IsochronousTransferIn() before the Mojo callback for ClaimInterface is resolved.
  3. This creates the transfer with a nullptr associated claimed_interface_.
  4. Once ClaimInterface completes, the renderer immediately calls ReleaseInterface(0) followed by Close() on the Mojo interface.
  5. If the concurrent threads align, the Release() call on the blocking thread pool deallocates the COM proxy while the sequence thread is blocked on the Mach IPC of AbortPipe during cancellation, leading to a UAF upon IPC completion.

Suggested Fix

To remediate this issue, add an explicit check to IsochronousTransferIn and IsochronousTransferOut to verify that the target endpoint exists in endpoint_map_ before attempting to create the transfer, matching the logic in GenericTransfer:

const auto endpoint_it = endpoint_map_.find(endpoint_address);
if (endpoint_it == endpoint_map_.end()) {
  USB_LOG(DEBUG) << "Failed to submit isochronous transfer because endpoint "
                 << static_cast<int>(endpoint_address)
                 << " not part of a claimed interface.";
  ReportIsochronousTransferError(std::move(callback), packet_lengths,
                                 UsbTransferStatus::TRANSFER_ERROR);
  return;
}

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
Uploaded PoC files