CVE-2026-13283
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.java |
modified |
Files Changed
components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.javacomponents/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialogTest.java
Patch
From 4157c6eaf49a5997aaa91f20e30e9d500281b910 Mon Sep 17 00:00:00 2001
From: Josh Karlin <jkarlin@chromium.org>
Date: Mon, 15 Jun 2026 06:59:59 -0700
Subject: [PATCH] Properly dismiss pending AdsBlockedDialog
If an AdsBlockedDialog is asynchronously waiting to be shown and is
dismissed, properly remove the callback from the dialog handler so that
it isn't referenced after dismissal.
Also fix a reentrancy bug where native dismissal in java wound up
calling back to native.
Bug: 522561151
Change-Id: Iba17be04c23bd7db2222f9d0be2b4cfb766ee818
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7926527
Reviewed-by: Charles Harrison <csharrison@chromium.org>
Commit-Queue: Josh Karlin <jkarlin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1646774}
---
diff --git a/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.java b/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.java
index 76a06d7..3e766acd 100644
--- a/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.java
+++ b/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.java
@@ -43,6 +43,7 @@
private @Nullable PropertyModel mDialogModel;
private @Nullable ClickableSpan mClickableSpan;
private final Handler mDialogHandler;
+ private boolean mDismissedByNative;
@CalledByNative
static AdsBlockedDialog create(long nativeDialog, WindowAndroid windowAndroid) {
@@ -135,7 +136,15 @@
@CalledByNative
void dismiss() {
- mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+ mDismissedByNative = true;
+ if (mDialogModel != null) {
+ mModalDialogManager.dismissDialog(
+ mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+ }
+ if (mNativeDialog != 0) {
+ mDialogHandler.removeCallbacksAndMessages(null);
+ mNativeDialog = 0;
+ }
}
// Returns link-formatted message text for the ads blocked dialog.
@@ -167,7 +176,9 @@
@Override
public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCause) {
mDialogHandler.removeCallbacksAndMessages(null);
- AdsBlockedDialogJni.get().onDismissed(mNativeDialog);
+ if (!mDismissedByNative && mNativeDialog != 0) {
+ AdsBlockedDialogJni.get().onDismissed(mNativeDialog);
+ }
mNativeDialog = 0;
}
diff --git a/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialogTest.java b/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialogTest.java
index 5a5fba8..3e243b8 100644
--- a/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialogTest.java
+++ b/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialogTest.java
@@ -133,6 +133,49 @@
Mockito.verify(mNativeMock).onLearnMoreClicked(anyLong());
}
+ /**
+ * Tests that dismissing a pending dialog (not yet shown by ModalDialogManager) correctly runs
+ * the cleanup path manually.
+ */
+ @Test
+ public void testDismissPendingDialogCleansUp() {
+ createAndShowDialog(true);
+ mDialog.dismiss();
+ Mockito.verify(mDialogHandler).removeCallbacksAndMessages(null);
+ Mockito.verify(mNativeMock, never()).onDismissed(anyLong());
+ }
+
+ /**
+ * Tests that dismissing a showing dialog delegates dismissal to ModalDialogManager and does not
+ * perform manual cleanup twice.
+ */
+ @Test
+ public void testDismissShowingDialogDelegatesToManager() {
+ createAndShowDialog(false);
+ // Stub dismissDialog to simulate the manager calling onDismiss synchronously
+ Mockito.doAnswer(
+ invocation -> {
+ ModalDialogProperties.Controller dialogController =
+ mModalDialogModel.get(ModalDialogProperties.CONTROLLER);
+ dialogController.onDismiss(
+ mModalDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+ return null;
+ })
+ .when(mModalDialogManagerMock)
+ .dismissDialog(Mockito.any(), Mockito.anyInt());
+
+ mDialog.dismiss();
+
+ // Verify that the manager's dismissDialog was called
+ Mockito.verify(mModalDialogManagerMock)
+ .dismissDialog(mModalDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+
+ // Verify that Java doesn't call back to native's onDismissed when handling a native
+ // dismissal.
+ Mockito.verify(mNativeMock, never()).onDismissed(anyLong());
+ Mockito.verify(mDialogHandler, Mockito.times(1)).removeCallbacksAndMessages(null);
+ }
+
/** Tests that the dialog is shown using Handler#post when shouldPostDialog is true. */
@Test
public void testPostDialog() {
Original Bug Report
Potential Use-After-Free in AdsBlockedDialog via uncancelled UI task
Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A race condition in the Android AdsBlockedDialog can lead to a potential Use-After-Free in the browser process. If a tab is closed while an asynchronous UI task to show the dialog is pending, the native C++ object is destroyed but the Java object retains a dangling pointer. Subsequent user interaction with the delayed dialog triggers a JNI call on the freed memory.
Affected files:
components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.javacomponents/subresource_filter/android/ads_blocked_dialog.cccomponents/subresource_filter/content/browser/ads_blocked_message_delegate.cc
Estimated timestamp from git blame: 2021-11-20
Summary
A potential Use-After-Free (UAF) vulnerability exists in the Android AdsBlockedDialog component. The issue stems from an asynchronous UI task queuing mechanism combined with an incomplete cleanup path in ModalDialogManager. Under specific conditions, an attacker can trigger the destruction of a WebContents while a dialog show task is pending on the UI thread, causing the C++ object to be freed without safely unregistering the Java counterpart.
Technical Details
When a user clicks “Learn more” on an ads-blocked dialog, AdsBlockedMessageDelegate sets a reprompt_required_ = true flag and navigates the user to a support page. Upon returning to the original tab, AdsBlockedMessageDelegate::OnWebContentsFocused calls ShowDialog(true), which invokes the Java method AdsBlockedDialog.show(true) via JNI.
Because the dialog is being restored, the Java implementation avoids displaying it immediately and instead posts a task to a Handler:
// AdsBlockedDialog.java
if (shouldPostDialog) {
mDialogHandler.post(() ->
mModalDialogManager.showDialog(assumeNonNull(mDialogModel), ModalDialogType.TAB));
}
If the underlying WebContents is destroyed while this Runnable is still pending in the queue, the C++ AdsBlockedMessageDelegate is destroyed. This triggers the destructor of the native AdsBlockedDialog object, which calls the Java dismiss() method:
// AdsBlockedDialog.java
@CalledByNative
void dismiss() {
mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
}
Crucially, inside ModalDialogManager.dismissDialog(), there is an early return if the dialog is not currently showing and not in the pending queue:
// ModalDialogManager.java
if (mCurrentPresenter == null || model != mCurrentPresenter.getDialogModel()) {
if (mPendingDialogContainer.remove(model)) { ... return; }
return; // Early return without triggering onDismiss!
}
Since the Runnable hasn’t executed yet, the dialog is in neither state, meaning dismissDialog returns early. Because of this, AdsBlockedDialog.onDismiss() is completely bypassed.
As a result:
mDialogHandler.removeCallbacksAndMessages(null)is not called, leaving theRunnableactive.mNativeDialogis not set to0, leaving a dangling pointer to the freed C++ object.
When the UI thread processes the Runnable, the dialog appears. Any user interaction (such as clicking the positive button) leads to AdsBlockedDialogJni.get().onAllowAdsClicked(mNativeDialog), executing a base::OnceClosure inside the freed C++ object and potentially granting Arbitrary Code Execution in the browser process.
Potential Exploitation Steps
Note: Our tooling agent cannot run live code, so these are potential steps derived from static analysis.
- An attacker’s site triggers an Ads Blocked message.
- The user interacts with the UI (e.g., clicking “Manage” and then “Learn more”), which opens a new tab and sets
reprompt_required_ = true. - The user switches back to the attacker’s tab.
- The attacker’s page detects the user returning (e.g., via the
visibilitychangeevent) and immediately closes the tab context (e.g., usingwindow.close()if it was a popup). - This synchronously destroys the
WebContentsand the nativeAdsBlockedDialog, but the Java show task is already queued on the UI thread. - The UI thread executes the queued task, presenting the “Ads blocked” dialog to the user over the active tab.
- The user clicks a button on the dialog, triggering a JNI call on the freed native pointer.
Proposed Fix
To fix this issue, AdsBlockedDialog.dismiss() must proactively clean up its own state regardless of whether ModalDialogManager considers the dialog active.
Modify AdsBlockedDialog.java:
@CalledByNative
void dismiss() {
// Immediately cancel pending tasks and clear the pointer
mDialogHandler.removeCallbacksAndMessages(null);
mNativeDialog = 0;
// Proceed with standard dismissal
mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
}
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.