Chrome · USB
CVE-2026-11188
UAF in USB
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
UsbServiceAndroidservices/device/usb/usb_service_android.cc |
modified | |
ifservices/device/usb/usb_service_android.cc |
modified |
Files Changed
services/device/usb/android/java/src/org/chromium/device/usb/ChromeUsbService.javaservices/device/usb/usb_service_android.cc
Patch
From 9bb3f8bce630aceaf883ad66729eb2c7444893e4 Mon Sep 17 00:00:00 2001
From: Matt Reynolds <mattreynolds@google.com>
Date: Wed, 29 Apr 2026 18:52:35 -0700
Subject: [PATCH] usb: Hop UsbServiceAndroid JNI callbacks to the service sequence
DeviceAttached, DeviceDetached, and DevicePermissionRequestComplete are
invoked by ChromeUsbService.java from BroadcastReceiver callbacks on the
Android main looper, but mutate sequence-bound state (devices_by_id_,
the UsbService observer list via NotifyDeviceAdded/Removed) without
hopping to the service sequence. This is the same data race pattern
fixed in SerialDeviceEnumeratorAndroid by commit eaffd0c6d1
(crbug.com/497000161).
Add task_runner_, capture SequencedTaskRunner::GetCurrentDefault() in
the constructor, split each JNI entry into a trampoline that
PostTasks to the service sequence, and add DCHECK_CALLED_ON_VALID_SEQUENCE
on the UsbService sequence_checker_ for sequence-bound methods.
This patch is based on an initial proposal by Muhammad Aadil.
Bug: 502959826
Change-Id: Ief6297751fe718dc9bf7f16c582c7dbfe895205c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7788050
Commit-Queue: Matt Reynolds <mattreynolds@chromium.org>
Reviewed-by: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1622887}
---
diff --git a/services/device/usb/android/java/src/org/chromium/device/usb/ChromeUsbService.java b/services/device/usb/android/java/src/org/chromium/device/usb/ChromeUsbService.java
index 218e177..2b69d641 100644
--- a/services/device/usb/android/java/src/org/chromium/device/usb/ChromeUsbService.java
+++ b/services/device/usb/android/java/src/org/chromium/device/usb/ChromeUsbService.java
@@ -17,6 +17,7 @@
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
+import org.jni_zero.NativeClassQualifiedName;
import org.jni_zero.NativeMethods;
import org.chromium.base.ContextUtils;
@@ -38,13 +39,13 @@
private static final String TAG = "Usb";
private static final String ACTION_USB_PERMISSION = "org.chromium.device.ACTION_USB_PERMISSION";
- long mUsbServiceAndroid;
+ long mUsbServiceJniDelegate;
UsbManager mUsbManager;
@Nullable BroadcastReceiver mUsbPermissionReceiver;
@Nullable BroadcastReceiver mUsbDeviceChangeReceiver;
- private ChromeUsbService(long usbServiceAndroid) {
- mUsbServiceAndroid = usbServiceAndroid;
+ private ChromeUsbService(long usbServiceJniDelegate) {
+ mUsbServiceJniDelegate = usbServiceJniDelegate;
mUsbManager =
(UsbManager)
ContextUtils.getApplicationContext().getSystemService(Context.USB_SERVICE);
@@ -53,8 +54,8 @@
}
@CalledByNative
- private static ChromeUsbService create(long usbServiceAndroid) {
- return new ChromeUsbService(usbServiceAndroid);
+ private static ChromeUsbService create(long usbServiceJniDelegate) {
+ return new ChromeUsbService(usbServiceJniDelegate);
}
@CalledByNative
@@ -81,7 +82,7 @@
if (mUsbManager.hasPermission(device)) {
ChromeUsbServiceJni.get()
.devicePermissionRequestComplete(
- mUsbServiceAndroid, device.getDeviceId(), true);
+ mUsbServiceJniDelegate, device.getDeviceId(), true);
} else {
Context context = ContextUtils.getApplicationContext();
Intent intent = new Intent(ACTION_USB_PERMISSION);
@@ -110,7 +111,7 @@
assumeNonNull(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE));
ChromeUsbServiceJni.get()
.devicePermissionRequestComplete(
- mUsbServiceAndroid,
+ mUsbServiceJniDelegate,
device.getDeviceId(),
intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED, false));
@@ -123,11 +124,12 @@
UsbDevice device =
assumeNonNull(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE));
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
- ChromeUsbServiceJni.get().deviceAttached(mUsbServiceAndroid, device);
+ ChromeUsbServiceJni.get()
+ .deviceAttached(mUsbServiceJniDelegate, device);
} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(
intent.getAction())) {
ChromeUsbServiceJni.get()
- .deviceDetached(mUsbServiceAndroid, device.getDeviceId());
+ .deviceDetached(mUsbServiceJniDelegate, device.getDeviceId());
}
}
};
@@ -153,11 +155,13 @@
@NativeMethods
interface Natives {
- void deviceAttached(long nativeUsbServiceAndroid, @Nullable UsbDevice device);
+ @NativeClassQualifiedName("UsbServiceAndroid::JniDelegate")
+ void deviceAttached(long nativePointer, @Nullable UsbDevice device);
- void deviceDetached(long nativeUsbServiceAndroid, int deviceId);
+ @NativeClassQualifiedName("UsbServiceAndroid::JniDelegate")
+ void deviceDetached(long nativePointer, int deviceId);
- void devicePermissionRequestComplete(
- long nativeUsbServiceAndroid, int deviceId, boolean granted);
+ @NativeClassQualifiedName("UsbServiceAndroid::JniDelegate")
+ void devicePermissionRequestComplete(long nativePointer, int deviceId, boolean granted);
}
}
diff --git a/services/device/usb/usb_service_android.cc b/services/device/usb/usb_service_android.cc
index 69f2645..96ebd936 100644
--- a/services/device/usb/usb_service_android.cc
+++ b/services/device/usb/usb_service_android.cc
@@ -22,10 +22,71 @@
namespace device {
-UsbServiceAndroid::UsbServiceAndroid() : UsbService() {
+// Bounces JNI callbacks to `task_runner_` (the service sequence). Holds a weak
+// reference to the service since it may be destroyed. The weak pointer must be
+// checked on the service sequence.
+class UsbServiceAndroid::JniDelegate
+ : public base::RefCountedThreadSafe<JniDelegate> {
+ public:
+ explicit JniDelegate(base::WeakPtr<UsbServiceAndroid> service)
+ : service_(std::move(service)),
+ task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
+
+ void DeviceAttached(JNIEnv* env,
+ const base::android::JavaRef<jobject>& usb_device) {
+ task_runner_->PostTask(
+ FROM_HERE, base::BindOnce(&JniDelegate::DeviceAttachedInternal, this,
+ base::android::ScopedJavaGlobalRef<jobject>(
+ env, usb_device)));
+ }
+
+ void DeviceDetached(int32_t device_id) {
+ task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&JniDelegate::DeviceDetachedInternal, this, device_id));
+ }
+
+ void DevicePermissionRequestComplete(int32_t device_id, bool granted) {
+ task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&JniDelegate::DevicePermissionRequestCompleteInternal,
+ this, device_id, granted));
+ }
+
+ private:
+ friend class base::RefCountedThreadSafe<JniDelegate>;
+ ~JniDelegate() = default;
+
+ void DeviceAttachedInternal(
+ base::android::ScopedJavaGlobalRef<jobject> usb_device) {
+ if (service_) {
+ service_->DeviceAttachedInternal(usb_device);
+ }
+ }
+
+ void DeviceDetachedInternal(int32_t device_id) {
+ if (service_) {
+ service_->DeviceDetachedInternal(device_id);
+ }
+ }
+
+ void DevicePermissionRequestCompleteInternal(int32_t device_id,
+ bool granted) {
+ if (service_) {
+ service_->DevicePermissionRequestCompleteInternal(device_id, granted);
+ }
+ }
+
+ base::WeakPtr<UsbServiceAndroid> service_;
+ scoped_refptr<base::SequencedTaskRunner> task_runner_;
+};
+
+UsbServiceAndroid::UsbServiceAndroid()
+ : task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {
+ jni_delegate_ = base::MakeRefCounted<JniDelegate>(weak_factory_.GetWeakPtr());
JNIEnv* env = AttachCurrentThread();
- j_object_.Reset(
- Java_ChromeUsbService_create(env, reinterpret_cast<int64_t>(this)));
+ j_object_.Reset(Java_ChromeUsbService_create(
+ env, reinterpret_cast<int64_t>(jni_delegate_.get())));
ScopedJavaLocalRef<jobjectArray> devices =
Java_ChromeUsbService_getDevices(env, j_object_);
for (auto usb_device : devices.CreateView(env)) {
@@ -36,20 +97,24 @@
}
Loading diff…
Original Bug Report
reported by ma...@chromium.org
Potential data race and UAF in UsbServiceAndroid
Bug description from https://crrev.com/c/7764302:
> DeviceAttached, DeviceDetached, and DevicePermissionRequestComplete are invoked by ChromeUsbService.java from BroadcastReceiver callbacks on the Android main looper, but mutate sequence-bound state (devices_by_id_, the UsbService observer list via NotifyDeviceAdded/Removed) without hopping to the service sequence. This is the same data race pattern fixed in SerialDeviceEnumeratorAndroid by commit eaffd0c6d1 (bug 497000161).
References
On This Page