CVE-2025-3620
Overview
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*viacontent::RenderFrameHost::FromID, returning null once the frame is gone. - `AcrossTasksDanglingUntriaged`
- A
raw_ptrtrait that suppresses dangling-pointer enforcement across task boundaries, so a staleRenderFrameHost*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.
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
- Open the chooser
A page in a compromised or attacker-influenced frame calls the WebUSB
requestDeviceAPI, constructing aUsbChooserControllerthat captures the requestingRenderFrameHost. - 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. - Trigger a stale access
A queued device event drives
DisplayDevice, or the user activates the help link drivingOpenHelpCenterUrl, causing a dereference of the now-danglingrequesting_frame_. - 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
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
| Function | Change | Notes |
|---|---|---|
requesting_frame_chrome/browser/usb/usb_chooser_controller.cc |
modified | |
ifchrome/browser/usb/usb_chooser_controller.cc |
modified |
Files Changed
chrome/browser/usb/usb_chooser_controller.ccchrome/browser/usb/usb_chooser_controller.h
Audit Directions
- Missing liveness re-checkAudit call sites that dereference a stored frame or web-contents pointer without a preceding
RenderFrameHost::FromIDnull check, especially in permission choosers (WebBluetooth,WebSerial,WebHID) that mirror the WebUSB pattern. - Async device/permission callbacksReview 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.
Patch
From 0333ecde91425e518cd898614c7b018209a18511 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
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 <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
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};
Original Bug Report
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:
- open the
poc.htmlin 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>
- click button
click meand select a device, then clickconnect - 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
Is the security issue related to a crash?
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