Medium watch ✔︎ verified CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set 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
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495902113
Fix commitf07a64bbbbb1 (chromium/src) +19/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-24 (per vendor advisory)
Fix landed2026-03-25 (public git)
Disclosed2026-05-12
Patch gap48 days — fix public in source before the advisory

Background

`ColorPickerBridge`
the C++ object (components/embedder_support/android/delegate/color_picker_bridge.cc) bridging Chromium’s color chooser to the Android color-picker UI.
`mNativeDialog` / `mNativeColorPicker`
the Java field in ColorPickerBridge.java holding the raw native pointer to the C++ bridge.
`onDialogDismissed`
the Java callback, invoked when the Android dialog is dismissed, that calls back into native via onColorChosen.

Root Cause Analysis

The Java ColorPickerBridge stored the native pointer in private final long mNativeDialog (ColorPickerBridge.java:30), assigned once in the constructor (:44). When the dialog was dismissed, onDialogDismissed unconditionally invoked ColorPickerBridgeJni.get().onColorChosen(mNativeDialog, newColor) (ColorPickerBridge.java:66), dereferencing that stored pointer. On the native side the pre-fix destructor was ColorPickerBridge::~ColorPickerBridge() = default; (color_picker_bridge.cc:55), so destroying the C++ object did nothing to notify Java. Because Android dialog dismissal is asynchronous, the native ColorPickerBridge could be freed (the advisory describes a renderer re-opening the chooser and resetting the previous one) while the Java object still held the now-dangling mNativeDialog, and a later onDialogDismissed would call onColorChosen through that stale pointer into freed memory.

The fix gives the C++ destructor a body that, when j_color_chooser_ is non-null, calls Java_ColorPickerBridge_detach(env, j_color_chooser_); the Java detach() sets mNativeColorPicker = 0; and onDialogDismissed now guards with if (mNativeColorPicker != 0) before the JNI call. This restores the invariant that Java never calls native through a freed pointer.

Key insight
Destroying the native ColorPickerBridge left the Java side’s mNativeDialog pointer dangling yet still callable; the fix clears that pointer from the destructor and null-checks it before every JNI call.

Attack Path

  1. Open a color chooser Content opens the Android color picker, creating paired Java/native ColorPickerBridge objects.
  2. Queue an async dismissal The dialog is dismissed, enqueuing onDialogDismissed for later execution on the UI thread.
  3. Free the native object Before that callback runs, the native ColorPickerBridge is destroyed; pre-fix ~ColorPickerBridge (color_picker_bridge.cc:55) left mNativeDialog dangling.
  4. Dereference freed memory The queued onDialogDismissed calls onColorChosen(mNativeDialog, …) (ColorPickerBridge.java:66) through the freed object. (inferred — exact reclamation and any escalation are the reporter’s claims.)

Impact Assessment

The advisory records a use-after-free in Core, medium severity, not in the wild, on Android. A compromised renderer able to force destruction of the chooser during the dialog-dismissal window could cause the browser process to call through a freed pointer; the reporter posits controlling the freed memory for code execution, which is (inferred) and not demonstrated by the shown code.

Changed Functions

FunctionChangeNotes
if
components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java
modified

Files Changed

  • components/embedder_support/android/delegate/color_picker_bridge.cc
  • components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java

Audit Directions

  • Native pointers held in Java
    Ensure every raw native pointer stored in a Java/JNI bridge is cleared in the native destructor and null-checked before use.
  • Async UI callbacks
    Audit asynchronous dialog/dismissal callbacks that may outlive the native object whose pointer they capture.
From f07a64bbbbb1f4b499ae000b301bc4714e463ea6 Mon Sep 17 00:00:00 2001
From: Ted Choc <[email protected]>
Date: Wed, 25 Mar 2026 09:28:06 -0700
Subject: [PATCH] Ensure native ptr held in Java is cleared when ColorPickerBridge is deleted.

Bug: 495902113
Change-Id: Id320c0b9f7f2b4c3e835a63ccb941d40d6eeab63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7698805
Commit-Queue: Ted Choc <[email protected]>
Reviewed-by: Bo Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1604888}
---

diff --git a/components/embedder_support/android/delegate/color_picker_bridge.cc b/components/embedder_support/android/delegate/color_picker_bridge.cc
index 18d7274..66042cde 100644
--- a/components/embedder_support/android/delegate/color_picker_bridge.cc
+++ b/components/embedder_support/android/delegate/color_picker_bridge.cc
@@ -52,7 +52,12 @@
   Java_ColorPickerBridge_showColorPicker(env, j_color_chooser_, initial_color);
 }
 
-ColorPickerBridge::~ColorPickerBridge() = default;
+ColorPickerBridge::~ColorPickerBridge() {
+  if (!j_color_chooser_.is_null()) {
+    JNIEnv* env = AttachCurrentThread();
+    Java_ColorPickerBridge_detach(env, j_color_chooser_);
+  }
+}
 
 void ColorPickerBridge::End() {
   if (!j_color_chooser_.is_null()) {
diff --git a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java
index 7df093e0..fa66f0f 100644
--- a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java
+++ b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java
@@ -27,25 +27,30 @@
 @JNINamespace("web_contents_delegate_android")
 @NullMarked
 public class ColorPickerBridge {
-    private final long mNativeDialog;
+    private long mNativeColorPicker;
     private final ColorPickerCoordinator mColorPickerCoordinator;
 
     @CalledByNative
     static @Nullable ColorPickerBridge create(
-            long nativeDialog, @JniType("ui::WindowAndroid*") WindowAndroid windowAndroid) {
+            long nativeColorPicker, @JniType("ui::WindowAndroid*") WindowAndroid windowAndroid) {
         if (windowAndroid == null) return null;
         Context context = windowAndroid.getContext().get();
         if (ContextUtils.activityFromContext(context) == null) return null;
         assumeNonNull(context);
-        return new ColorPickerBridge(nativeDialog, context);
+        return new ColorPickerBridge(nativeColorPicker, context);
     }
 
-    private ColorPickerBridge(long nativeDialog, Context context) {
-        mNativeDialog = nativeDialog;
+    private ColorPickerBridge(long nativeColorPicker, Context context) {
+        mNativeColorPicker = nativeColorPicker;
         mColorPickerCoordinator = ColorPickerCoordinator.create(context, this::onDialogDismissed);
     }
 
     @CalledByNative
+    private void detach() {
+        mNativeColorPicker = 0;
+    }
+
+    @CalledByNative
     void showColorPicker(int initialColor) {
         mColorPickerCoordinator.show(initialColor);
     }
@@ -63,7 +68,9 @@
     }
 
     void onDialogDismissed(int newColor) {
-        ColorPickerBridgeJni.get().onColorChosen(mNativeDialog, newColor);
+        if (mNativeColorPicker != 0) {
+            ColorPickerBridgeJni.get().onColorChosen(mNativeColorPicker, newColor);
+        }
     }
 
     @NativeMethods
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in ColorPickerBridge via asynchronous dialog dismissal race

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the Android Browser process. A race condition between the asynchronous dismissal of the Android color picker dialog and the synchronous destruction of the native ColorPickerBridge object can leave a dangling C++ pointer in the Java implementation, which is subsequently dereferenced.

Affected files:

  • components/embedder_support/android/delegate/color_picker_bridge.cc
  • components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerBridge.java
  • content/browser/web_contents/web_contents_impl.cc
  • components/embedder_support/android/delegate/color_picker_bridge.h

Estimated timestamp from git blame: 2026-01-13

Description

A potential Use-After-Free (UAF) vulnerability exists in the Android Browser process due to a race condition between the asynchronous dismissal of an Android AlertDialog and the renderer-initiated destruction of the ColorPickerBridge object.

In the Android implementation of the color picker, ColorPickerBridge.java stores a native pointer to its corresponding C++ object in a final long mNativeDialog field (line 30). When a user cancels the dialog (e.g., by pressing the Back button), Android’s Dialog.cancel() enqueues an OnCancelListener callback as a message in the main Looper. This callback is processed asynchronously on the UI thread.

Root Cause

A compromised renderer can exploit the window between the dialog dismissal and the execution of the asynchronous callback by triggering the destruction of the ColorPickerBridge.

A renderer can repeatedly call blink::mojom::ColorChooserFactory::OpenColorChooser. In WebContentsImpl::OpenColorChooser, the call to color_chooser_holder_.reset() unconditionally destroys the previous chooser and its associated native ColorPickerBridge object.

When the native ColorPickerBridge is destroyed, its memory is freed. However, the Java-side mNativeDialog pointer is not invalidated and continues to point to the now-freed memory.

Potential Exploitation Scenario

These are suggested steps, as this AI agent does not yet have the ability to run code to confirm a working exploit.

  1. A compromised renderer opens a color picker dialog.
  2. The user dismisses the dialog (e.g., via the Back key). The OnCancelListener message is enqueued to the main Looper.
  3. The renderer immediately spams OpenColorChooser Mojo messages. One of these is processed before the dismissal message, causing WebContentsImpl to reset the current chooser and free the native ColorPickerBridge memory.
  4. The attacker performs a heap spray to reclaim the freed memory (approximately 32 bytes) and places a controlled value at the offset of the web_contents_ field.
  5. The queued Java cancel-listener finally fires, calling onDialogDismissed (line 65), which invokes the JNI method onColorChosen(mNativeDialog, ...).
  6. The native JNI stub calls OnColorChosen using the stale mNativeDialog pointer.
  7. ColorPickerBridge::OnColorChosen reads the attacker-controlled web_contents_ value and performs a virtual call (DidChooseColorInColorChooser) through an attacker-controlled vtable. This could lead to arbitrary code execution (RCE) in the browser process.

Mitigation Analysis

This issue is not mitigated by MiraclePtr (BackupRefPtr). The UAF occurs on the ColorPickerBridge object itself (the “container”). The raw_ptr<WebContents> web_contents_ field resides within the freed memory. Because the attacker-controlled data is loaded as the raw_ptr value before the virtual call dispatch, the internal reference counting and quarantine mechanisms that protect the pointee of a raw_ptr are bypassed if the attacker points the fake pointer outside the BRP pool.

Suggested Fix

Add a clearNativePtr() method to the Java ColorPickerBridge class that sets mNativeDialog to 0. Call this method from the C++ ColorPickerBridge destructor via JNI. Additionally, check if mNativeDialog is non-zero before invoking JNI methods in Java.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker