High watch ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
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 — revised by independent review by AI
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.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Overstates attacker control. In the old DisplayDevice, requesting_frame_ is dereferenced only when kUnrestrictedUsb is enabled. OpenHelpCenterUrl needs the user to click the chooser's help link. The key insight also reverses lifetimes: the controller outlives the frame.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker405292639
Fix commit0333ecde9142 (chromium/src) +25/-8
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited@retsew0x01
Reported2025-03-21 (per vendor advisory)
Fix landed2025-04-08 (public git)
Disclosed2025-04-15
Patch gap7 days — fix public in source before the advisory

Background

`UsbChooserController`
The browser-process controller (chrome/browser/usb/usb_chooser_controller.cc) backing the WebUSB device-picker dialog for a given requesting frame.
`RenderFrameHost`
The browser-process object representing a single frame in the renderer, whose lifetime is tied to that frame and can end while a chooser dialog is still open.
`GlobalRenderFrameHostId`
A stable, non-owning identifier for a frame that can be re-resolved to a live RenderFrameHost* via content::RenderFrameHost::FromID, returning null once the frame is gone.
`AcrossTasksDanglingUntriaged`
A raw_ptr trait that suppresses dangling-pointer enforcement across task boundaries, so a stale RenderFrameHost* held under it is not caught at destruction time.

Root Cause Analysis

UsbChooserController cached the requesting frame as a raw requesting_frame_ pointer (const raw_ptr<content::RenderFrameHost, AcrossTasksDanglingUntriaged>), which it later dereferenced in OpenHelpCenterUrl and DisplayDevice. Because the chooser dialog and its device-enumeration callbacks outlive a task boundary, the requesting RenderFrameHost could be destroyed (frame navigation, tab close, or frame detach) while the controller was still live, violating the invariant that requesting_frame_ points at a valid object at each use. The AcrossTasksDanglingUntriaged trait meant the dangling pointer was not flagged, so a subsequent call such as WebContents::FromRenderFrameHost(requesting_frame_) or requesting_frame_->IsFeatureEnabled(...) dereferenced freed memory.

The fix stores the frame’s identity as a GlobalRenderFrameHostId (render_frame_host_id_) and re-resolves it through content::RenderFrameHost::FromID at each access, returning early (no help URL, DisplayDevice returns false) when the lookup yields null. This works because FromID reflects current frame liveness rather than trusting a possibly-stale pointer.

Key insight
The single mistake was retaining an owning-lifetime assumption over a non-owned RenderFrameHost* that could outlive the controller; the fix replaces the raw pointer with a re-resolvable GlobalRenderFrameHostId and gates every access on a fresh, null-checked FromID lookup.

Attack Path

  1. Open the chooser A page in a compromised or attacker-influenced frame calls the WebUSB requestDevice API, constructing a UsbChooserController that captures the requesting RenderFrameHost.
  2. Destroy the frame While the picker dialog and asynchronous device enumeration are pending, the attacker tears down the requesting frame (navigate the frame, close the tab, or detach an iframe), freeing its RenderFrameHost.
  3. Trigger a stale access A queued device event drives DisplayDevice, or the user activates the help link driving OpenHelpCenterUrl, causing a dereference of the now-dangling requesting_frame_.
  4. Use freed memory The controller reads or calls through the freed RenderFrameHost (e.g., WebContents::FromRenderFrameHost, IsFeatureEnabled), producing a use-after-free that may be groomed for corruption or control-flow influence in the browser process.

Impact Assessment

An attacker who can open a WebUSB chooser and then destroy the requesting frame gains a use-after-free of a RenderFrameHost in the privileged browser process, which can lead to memory corruption and potentially remote code execution. The precondition is that a chooser dialog or its pending device callbacks survive the frame’s destruction, a race the attacker controls via navigation or frame teardown. The controller runs in the browser process, so successful exploitation escalates beyond the renderer sandbox.

Changed Functions

FunctionChangeNotes
requesting_frame_
chrome/browser/usb/usb_chooser_controller.cc
modified
if
chrome/browser/usb/usb_chooser_controller.cc
modified

Files Changed

  • chrome/browser/usb/usb_chooser_controller.cc
  • chrome/browser/usb/usb_chooser_controller.h

Audit Directions

  • Missing liveness re-check
    Audit call sites that dereference a stored frame or web-contents pointer without a preceding RenderFrameHost::FromID null check, especially in permission choosers (WebBluetooth, WebSerial, WebHID) that mirror the WebUSB pattern.
  • Async device/permission callbacks
    Review observer and enumeration callbacks (e.g., DeviceObserver, GetPermissionCallback) that fire after the originating frame may have navigated or detached, ensuring each validates frame liveness before use.
From 0333ecde91425e518cd898614c7b018209a18511 Mon Sep 17 00:00:00 2001
From: Alvin Ji <[email protected]>
Date: Tue, 08 Apr 2025 10:46:18 -0700
Subject: [PATCH] usb: Use GlobalRenderFrameHostId in UsbChooserController

The UsbChooserController currently holds a raw pointer to the requesting
RenderFrameHost. This can lead to use-after-free issues if the
RenderFrameHost is destroyed before the chooser controller. This CL
replaces the raw pointer with a `GlobalRenderFrameHostId`. This ID can
be used to retrieve the RenderFrameHost when needed, and checks are
added to ensure the RenderFrameHost is still valid before accessing it.

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

diff --git a/chrome/browser/usb/usb_chooser_controller.cc b/chrome/browser/usb/usb_chooser_controller.cc
index 2654ad4f..4c64fc2d 100644
--- a/chrome/browser/usb/usb_chooser_controller.cc
+++ b/chrome/browser/usb/usb_chooser_controller.cc
@@ -100,8 +100,8 @@
           CreateChooserTitle(render_frame_host, IDS_USB_DEVICE_CHOOSER_PROMPT)),
       options_(std::move(options)),
       callback_(std::move(callback)),
-      requesting_frame_(render_frame_host) {
-  RenderFrameHost* main_frame = requesting_frame_->GetMainFrame();
+      render_frame_host_id_(render_frame_host->GetGlobalId()) {
+  RenderFrameHost* main_frame = render_frame_host->GetMainFrame();
   origin_ = main_frame->GetLastCommittedOrigin();
   Profile* profile =
       Profile::FromBrowserContext(main_frame->GetBrowserContext());
@@ -203,7 +203,15 @@
 void UsbChooserController::Close() {}
 
 void UsbChooserController::OpenHelpCenterUrl() const {
-  WebContents::FromRenderFrameHost(requesting_frame_)
+  content::RenderFrameHost* render_frame_host =
+      content::RenderFrameHost::FromID(render_frame_host_id_);
+  if (!render_frame_host) {
+    // When |render_frame_host| is not valid anymore we don't want to open help
+    // center url.
+    return;
+  }
+
+  WebContents::FromRenderFrameHost(render_frame_host)
       ->OpenURL(content::OpenURLParams(
                     GURL(chrome::kChooserUsbOverviewURL), content::Referrer(),
                     WindowOpenDisposition::NEW_FOREGROUND_TAB,
@@ -267,6 +275,14 @@
 
 bool UsbChooserController::DisplayDevice(
     const device::mojom::UsbDeviceInfo& device_info) const {
+  content::RenderFrameHost* render_frame_host =
+      content::RenderFrameHost::FromID(render_frame_host_id_);
+  if (!render_frame_host) {
+    // When |render_frame_host| is not valid anymore we don't want to display
+    // any device information.
+    return false;
+  }
+
   if (!device::UsbDeviceFilterMatchesAny(options_->filters, device_info)) {
     return false;
   }
@@ -281,10 +297,9 @@
   bool is_usb_unrestricted = false;
   if (base::FeatureList::IsEnabled(blink::features::kUnrestrictedUsb)) {
     is_usb_unrestricted =
-        requesting_frame_ &&
-        requesting_frame_->IsFeatureEnabled(
+        render_frame_host->IsFeatureEnabled(
             network::mojom::PermissionsPolicyFeature::kUsbUnrestricted) &&
-        content::HasIsolatedContextCapability(requesting_frame_);
+        content::HasIsolatedContextCapability(render_frame_host);
   }
   // Isolated context with permission to access the policy-controlled feature
   // "usb-unrestricted" can bypass the USB blocklist.
diff --git a/chrome/browser/usb/usb_chooser_controller.h b/chrome/browser/usb/usb_chooser_controller.h
index de5d7fb..8e1bb06 100644
--- a/chrome/browser/usb/usb_chooser_controller.h
+++ b/chrome/browser/usb/usb_chooser_controller.h
@@ -15,6 +15,7 @@
 #include "base/scoped_observation.h"
 #include "chrome/browser/usb/usb_chooser_context.h"
 #include "components/permissions/chooser_controller.h"
+#include "content/public/browser/global_routing_id.h"
 #include "services/device/public/mojom/usb_device.mojom.h"
 #include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
 #include "url/origin.h"
@@ -64,8 +65,9 @@
   blink::mojom::WebUsbService::GetPermissionCallback callback_;
   url::Origin origin_;
 
-  const raw_ptr<content::RenderFrameHost, AcrossTasksDanglingUntriaged>
-      requesting_frame_;
+  // Hold the GlobalRenderFrameHostId for requesting frame so we can always
+  // check whether the frame host is still valid before we access it.
+  const content::GlobalRenderFrameHostId render_frame_host_id_;
   base::WeakPtr<UsbChooserContext> chooser_context_;
   base::ScopedObservation<UsbChooserContext, UsbChooserContext::DeviceObserver>
       observation_{this};
Loading diff…

Original Bug Report

reported by [email protected]

AddressSanitizer: heap-use-after-free on address 0x7da147715900 at pc 0x55baa6985542 bp 0x7ffe146adfd0 sp 0x7ffe146adfc8


Report description

AddressSanitizer: heap-use-after-free on address 0x7da147715900 at pc 0x55baa6985542 bp 0x7ffe146adfd0 sp 0x7ffe146adfc8


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

This crash was found while reproducing https://issues.chromium.org/issues/40074794.

AddressSanitizer: heap-use-after-free on address 0x7da147715900 at pc 0x55baa6985542 bp 0x7ffe146adfd0 sp 0x7ffe146adfc8

Crash type

browser crash

ENV

ubuntu 22.04.1

Reproduce steps:

  1. open the poc.html in chromium-asan
<html>
<head>
  <script>
    async function main() {
	
      dev = null;
      await navigator.usb
        .requestDevice({ filters: [] })
        .then((usbDevice) => {
          dev = usbDevice;
          console.log(`Product name: ${usbDevice.productName}`);
		  for (var i=1;i<10000;i++){
				navigator.usb
				.requestDevice({ filters: [] })
				.then((usbDevice) => {
			});
		}
    });
      
	}
  </script>
</head>
<body>
  <button onclick="main()">click me</button>
</body>
</html>
  1. click button click me and select a device, then click connect
  2. Try close the tab, then UAF

Please briefly explain who can exploit the vulnerability, and what they gain when doing so

I’m not sure if we can exploit it.


The cause

What version of Chrome have you found the security issue in?

136.0.7079.0 chromium-asan-linux

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed processs)

How would you like to be publicly acknowledged for your report?

retsew0x01

View on issue tracker