CVE-2026-7352
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
modified | |
synchronizedmedia/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
modified | |
whilemedia/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
modified |
Files Changed
media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
Patch
From 5625d2ec93730e93927ffad958fc3a0f90073218 Mon Sep 17 00:00:00 2001
From: Feras Aldahlawi <frs@chromium.org>
Date: Wed, 15 Apr 2026 17:59:47 -0700
Subject: [PATCH] cdm: Synchronize calls to MediaDrmBridge
Introduce a synchronization lock and a pending call counter
to ensure that MediaDrmBridge's destroy() method waits for all in-flight
JNI callbacks to complete before proceeding with destruction. This
prevents race conditions where a callback might attempt to use the
native pointer after it has been invalidated.
Bug: 499023054
Change-Id: I50838e4d0592a0b2c6ac6851d5f199c76a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737728
Reviewed-by: Frank Liberato <liberato@chromium.org>
Reviewed-by: Vikram Pasupathy <vpasupathy@chromium.org>
Commit-Queue: Feras Aldahlawi <frs@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1615537}
---
diff --git a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
index 6a706db7..698fe564 100644
--- a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
+++ b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
@@ -102,6 +102,10 @@
// Reference to the C++ media::MediaDrmBridge object.
private long mNativeMediaDrmBridge;
+ private int mPendingNativeCalls;
+
+ private final Object mNativeMediaDrmBridgeLock = new Object();
+
private final UUID mKeySystemUuid;
private final boolean mRequiresMediaCrypto;
@@ -642,7 +646,7 @@
// Provision only works for origin isolated storage.
if (!mOriginSet) {
Log.e(TAG, "Calling provision() without an origin.");
- MediaDrmBridgeJni.get().onProvisioningComplete(mNativeMediaDrmBridge, false);
+ onProvisioningComplete(false);
return;
}
@@ -663,12 +667,12 @@
}
// Indicate that provisioning succeeded.
- MediaDrmBridgeJni.get().onProvisioningComplete(mNativeMediaDrmBridge, true);
+ onProvisioningComplete(true);
} catch (android.media.NotProvisionedException e) {
if (!startProvisioning()) {
// Indicate that provisioning failed.
- MediaDrmBridgeJni.get().onProvisioningComplete(mNativeMediaDrmBridge, false);
+ onProvisioningComplete(false);
}
}
}
@@ -692,7 +696,18 @@
@CalledByNative
private void destroy() {
Log.i(TAG, "Destroying MediaDrmBridge for origin %s", mOrigin);
- mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+ synchronized (mNativeMediaDrmBridgeLock) {
+ mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+ // If mPendingNativeCalls is greater than 0, that means there are threads still
+ // accessing this object.
+ while (mPendingNativeCalls > 0) {
+ try {
+ mNativeMediaDrmBridgeLock.wait();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Interrupted while waiting for pending native calls", e);
+ }
+ }
+ }
if (mMediaDrm != null) {
try {
release();
@@ -1399,9 +1414,7 @@
}
Log.i(TAG, "Provisioning origin ID %s", mOriginSet ? mOrigin : "<none>");
- MediaDrmBridgeJni.get()
- .onProvisionRequest(
- mNativeMediaDrmBridge, request.getDefaultUrl(), request.getData());
+ onProvisionRequest(request.getDefaultUrl(), request.getData());
return true;
}
@@ -1487,7 +1500,7 @@
void onProvisioned(boolean success) {
if (!mRequiresMediaCrypto) {
// No MediaCrypto required, so notify provisioning complete.
- MediaDrmBridgeJni.get().onProvisioningComplete(mNativeMediaDrmBridge, success);
+ onProvisioningComplete(success);
if (!success) {
release();
}
@@ -1538,50 +1551,89 @@
// Helper functions to make native calls.
- private void onMediaCryptoReady(@Nullable MediaCrypto mediaCrypto) {
- if (isNativeMediaDrmBridgeValid()) {
- MediaDrmBridgeJni.get().onMediaCryptoReady(mNativeMediaDrmBridge, mediaCrypto);
+ private interface NativeCall {
+ void run(long nativeMediaDrmBridge);
+ }
+
+ /**
+ * Safely executes a JNI call, ensuring the native pointer remains valid for the duration of the
+ * call and blocking destroy() until it completes.
+ */
+ private void runNativeCallback(NativeCall call) {
+ long nativePtr;
+ synchronized (mNativeMediaDrmBridgeLock) {
+ if (mNativeMediaDrmBridge == INVALID_NATIVE_MEDIA_DRM_BRIDGE) return;
+ nativePtr = mNativeMediaDrmBridge;
+ mPendingNativeCalls++;
}
+
+ try {
+ call.run(nativePtr);
+ } finally {
+ synchronized (mNativeMediaDrmBridgeLock) {
+ mPendingNativeCalls--;
+ if (mPendingNativeCalls == 0
+ && mNativeMediaDrmBridge == INVALID_NATIVE_MEDIA_DRM_BRIDGE) {
+ mNativeMediaDrmBridgeLock.notifyAll();
+ }
+ }
+ }
+ }
+
+ private void onMediaCryptoReady(@Nullable MediaCrypto mediaCrypto) {
+ runNativeCallback(
+ (nativePtr) -> MediaDrmBridgeJni.get().onMediaCryptoReady(nativePtr, mediaCrypto));
+ }
+
+ private void onProvisioningComplete(boolean success) {
+ runNativeCallback(
+ (nativePtr) -> MediaDrmBridgeJni.get().onProvisioningComplete(nativePtr, success));
+ }
+
+ private void onProvisionRequest(String defaultUrl, byte[] requestData) {
+ runNativeCallback(
+ (nativePtr) ->
+ MediaDrmBridgeJni.get()
+ .onProvisionRequest(nativePtr, defaultUrl, requestData));
}
private void onPromiseResolved(final long promiseId) {
- if (isNativeMediaDrmBridgeValid()) {
- MediaDrmBridgeJni.get().onPromiseResolved(mNativeMediaDrmBridge, promiseId);
- }
+ runNativeCallback(
+ (nativePtr) -> MediaDrmBridgeJni.get().onPromiseResolved(nativePtr, promiseId));
}
private void onPromiseResolvedWithSession(final long promiseId, final SessionId sessionId) {
- if (isNativeMediaDrmBridgeValid()) {
- MediaDrmBridgeJni.get()
- .onPromiseResolvedWithSession(
- mNativeMediaDrmBridge, promiseId, sessionId.emeId());
- }
+ byte[] emeId = sessionId.emeId();
+ runNativeCallback(
+ (nativePtr) ->
+ MediaDrmBridgeJni.get()
+ .onPromiseResolvedWithSession(nativePtr, promiseId, emeId));
}
private void onPromiseRejected(
final long promiseId, final long systemCode, final String errorMessage) {
Log.e(TAG, "onPromiseRejected: %s", errorMessage);
- if (isNativeMediaDrmBridgeValid()) {
- MediaDrmBridgeJni.get()
- .onPromiseRejected(mNativeMediaDrmBridge, promiseId, systemCode, errorMessage);
- }
+ runNativeCallback(
+ (nativePtr) ->
+ MediaDrmBridgeJni.get()
+ .onPromiseRejected(nativePtr, promiseId, systemCode, errorMessage));
}
private void onSessionMessage(final SessionId sessionId, final MediaDrm.KeyRequest request) {
- if (!isNativeMediaDrmBridgeValid()) return;
-
int requestType = request.getRequestType();
- MediaDrmBridgeJni.get()
- .onSessionMessage(
- mNativeMediaDrmBridge, sessionId.emeId(), requestType, request.getData());
+ byte[] requestData = request.getData();
+ byte[] emeId = sessionId.emeId();
+ runNativeCallback(
+ (nativePtr) ->
+ MediaDrmBridgeJni.get()
+ .onSessionMessage(nativePtr, emeId, requestType, requestData));
}
Original Bug Report
Potential Cross-thread UAF in MediaDrmBridge on Android via Data Race
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 without the security team.
Overview: A potential Use-After-Free (UAF) vulnerability exists in MediaDrmBridge on Android due to missing synchronization on the mNativeMediaDrmBridge field. Mismatched threading between Chrome’s dedicated media thread and Android’s MediaDrm callbacks allows the GPU main thread to invoke JNI methods using a dangling pointer, potentially leading to Remote Code Execution (RCE) in the GPU process.
Affected files:
media/base/android/java/src/org/chromium/media/MediaDrmBridge.javamedia/base/android/media_drm_bridge.ccmedia/base/android/media_drm_bridge.h
Estimated timestamp from git blame: 2024-05-28
Description
A potential Use-After-Free (UAF) race condition exists in the Android MediaDrmBridge implementation. On Android, Chrome’s GPU process creates the MediaDrmBridge on a dedicated base::ThreadPool task runner. Because this native thread lacks a Java Looper, Android’s MediaDrm listener APIs (e.g., OnKeyStatusChangeListener) fall back to dispatching callbacks on the application’s Main Looper, which corresponds to the GPU Main Thread.
When the DRM session is destroyed, the C++ MediaDrmBridge is deleted on the ThreadPool thread, which calls into Java’s MediaDrmBridge.destroy() to set mNativeMediaDrmBridge = 0. However, the mNativeMediaDrmBridge field is a primitive long and is accessed without volatile or synchronized modifiers.
If a DRM callback fires concurrently on the GPU Main Thread, it reads the stale, non-zero mNativeMediaDrmBridge pointer. Due to the lack of synchronization, this introduces a Time-of-Check to Time-of-Use (TOCTOU) and data visibility race. The GPU Main Thread then passes the dangling pointer back to C++ via JNI. The C++ code (e.g., MediaDrmBridge::OnSessionKeysChange) attempts to access members of the freed object (like task_runner_->PostTask), leading to a virtual call on a freed object.
Potential Attacker Steps
Note: Our tooling agent does not have the ability to run code, so these are suggested steps based on static analysis.
- Setup: An attacker compromises a Renderer process and sends a Mojo IPC request to the GPU process to create a Widevine DRM session.
- Heap Grooming: The attacker uses GPU IPCs (e.g., WebGL/WebGPU) to groom the GPU process heap, preparing to reclaim the memory block of the
MediaDrmBridgeonce freed. - Trigger: The attacker triggers an asynchronous DRM event (such as a key status change) via the DRM session.
- Race: Concurrently, the attacker drops the Mojo connection to the
MediaService, initiating the teardown of theMediaDrmBridgeon the ThreadPool thread. - UAF Execution: The ThreadPool thread frees the C++ object and clears the Java pointer. Simultaneously, the GPU Main Thread processes the DRM callback, reads the stale native pointer due to the missing synchronization, and calls into C++.
- RCE: The JNI invocation lands in
MediaDrmBridge::OnSessionKeysChangeon the freed object. The attacker-controlled reclaimed memory allows them to hijack the virtual method calltask_runner_->PostTask(), achieving Remote Code Execution in the GPU process.
Suggested Fix
- Java-Side Synchronization: Implement
synchronizedblocks around all accesses tomNativeMediaDrmBridge(similar to howMediaCodecBridgehandlesmNativeMediaCodecBridge). This ensures the pointer check and JNI invocation are atomic relative todestroy(). - C++ Side WeakPtr: Consider changing the JNI architecture to pass an ID or handle that can be safely resolved on the C++ side using a thread-safe mechanism, or pass a raw pointer to an object that strictly vends
base::WeakPtrs, rather than passing a raw pointer to theMediaDrmBridgeitself.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.