Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in USB
DescriptionUse after free in USB
ComponentUSB
Bug ClassUAF
Tracker516999424
Fix commit91cbfc99801d (chromium/src) +20/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • services/device/usb/usb_device_handle_impl.cc
From 91cbfc99801d49d4e7d8a6349552870f95b48cb0 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
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 <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
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 vm...@google.com

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