Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker499023054
Fix commit5625d2ec9373 (chromium/src) +100/-49
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
if
media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
modified
synchronized
media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
modified
while
media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
modified

Files Changed

  • media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
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));
     }
Loading diff…

Original Bug Report

reported by vm...@google.com

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.java
  • media/base/android/media_drm_bridge.cc
  • media/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.

  1. Setup: An attacker compromises a Renderer process and sends a Mojo IPC request to the GPU process to create a Widevine DRM session.
  2. 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 MediaDrmBridge once freed.
  3. Trigger: The attacker triggers an asynchronous DRM event (such as a key status change) via the DRM session.
  4. Race: Concurrently, the attacker drops the Mojo connection to the MediaService, initiating the teardown of the MediaDrmBridge on the ThreadPool thread.
  5. 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++.
  6. RCE: The JNI invocation lands in MediaDrmBridge::OnSessionKeysChange on the freed object. The attacker-controlled reclaimed memory allows them to hijack the virtual method call task_runner_->PostTask(), achieving Remote Code Execution in the GPU process.

Suggested Fix

  1. Java-Side Synchronization: Implement synchronized blocks around all accesses to mNativeMediaDrmBridge (similar to how MediaCodecBridge handles mNativeMediaCodecBridge). This ensures the pointer check and JNI invocation are atomic relative to destroy().
  2. 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 the MediaDrmBridge itself.

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.

View on issue tracker