Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Permissions
DescriptionUse after free in Permissions
ComponentPermissions
Bug ClassUAF
Tracker499247910
Fix commit5457c6e7e7cb (chromium/src) +5/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Files Changed

  • components/permissions/android/permission_prompt/permission_dialog_delegate.cc
  • components/permissions/android/permission_prompt/permission_dialog_delegate.h
From 5457c6e7e7cb051f87fe90545cfd68c5640432c4 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <[email protected]>
Date: Tue, 07 Apr 2026 01:46:27 -0700
Subject: [PATCH] [permissions] Destroy PermissionDialogJavaDelegate async

This CL makes permissionDialogDelegate::Dismissed destroy the
PermissionDialogJavaDelegate asynchronously. This prevents UaF issues in
case Dismissed() is called while a PermissionDialogJavaDelegate method
is running.

Bug: 499247910
Change-Id: Iac8a46b4f4fc3fc83dc989c54d0fea41d3a3e6ac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7734121
Commit-Queue: Antonio Sartori <[email protected]>
Reviewed-by: Balazs Engedy <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610616}
---

diff --git a/components/permissions/android/permission_prompt/permission_dialog_delegate.cc b/components/permissions/android/permission_prompt/permission_dialog_delegate.cc
index e570092..4c671a1 100644
--- a/components/permissions/android/permission_prompt/permission_dialog_delegate.cc
+++ b/components/permissions/android/permission_prompt/permission_dialog_delegate.cc
@@ -224,13 +224,16 @@
     // But, all the underlying data associated with it will get wiped.
     // So, we destroy the Java delegate and use the `IsJavaDelegateDestroyed`
     // signal as a way to tell if the `PermissionPrompt` creation failed.
-    DestroyJavaDelegate();
+
+    // Delete asynchronously to avoid re-entrancy issues.
+    base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
+        FROM_HERE, std::move(java_delegate_));
   }
   permission_prompt_->Dismiss(prompt_options_);
 }
 
 void PermissionDialogDelegate::Destroy(JNIEnv* env) {
-  DestroyJavaDelegate();
+  java_delegate_.reset();
 }
 
 void PermissionDialogDelegate::NotifyPermissionAllowed() {
diff --git a/components/permissions/android/permission_prompt/permission_dialog_delegate.h b/components/permissions/android/permission_prompt/permission_dialog_delegate.h
index f4f760c..0361ee0 100644
--- a/components/permissions/android/permission_prompt/permission_dialog_delegate.h
+++ b/components/permissions/android/permission_prompt/permission_dialog_delegate.h
@@ -120,8 +120,6 @@
   // On navigation or page destruction, hide the dialog.
   void DismissDialog();
 
-  void DestroyJavaDelegate() { java_delegate_.reset(); }
-
   // WebContentsObserver:
   void PrimaryPageChanged(content::Page&) override;
   void WebContentsDestroyed() override;
Loading diff…

Original Bug Report

reported by [email protected]

Potential browser-process UAF in PermissionDialogJavaDelegate due to synchronous JNI re-entry

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 the Android permission prompt logic. A synchronous JNI call during dialog creation can trigger immediate dismissal, destroying the underlying C++ delegate object while its method is still on the stack. Returning from JNI results in a UAF when accessing member variables.

Affected files:

  • components/permissions/android/permission_prompt/permission_dialog_delegate.cc
  • components/permissions/android/permission_prompt/permission_dialog_delegate.h
  • components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogController.java
  • components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.java
  • components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogCoordinator.java

Estimated timestamp from git blame: 2026-01-16

Summary

A potential Use-After-Free (UAF) exists in PermissionDialogJavaDelegate::CreateDialog on Android. When initializing a permission prompt, a synchronous JNI call is made to Java to show the dialog. If the Java side immediately dismisses the dialog (e.g., due to a null ModalDialogManager), a synchronous callback to native code destroys the PermissionDialogJavaDelegate object. When execution unwinds back to CreateDialog, the this pointer is dangling, leading to a UAF and a potential control flow hijack.

Technical Details

The vulnerability is triggered during the creation of a permission prompt via PermissionRequestManager::RecreateView(). This function invokes a factory method that eventually calls the PermissionDialogDelegate constructor, which allocates a PermissionDialogJavaDelegate using a std::unique_ptr and calls CreateDialog() on it.

In components/permissions/android/permission_prompt/permission_dialog_delegate.cc:

void PermissionDialogJavaDelegate::CreateDialog(content::WebContents* web_contents) {
  // ...
  PermissionDialogController::CreateDialog(env, j_delegate_); // Synchronous JNI call

  if (permission_prompt_->ShouldUseRequestingOriginFavicon()) { // UAF occurs here
    // ...
  }
}
  1. JNI Execution: The code calls PermissionDialogController::CreateDialog in Java.
  2. Synchronous Dismissal: In Java, if PermissionDialogCoordinator.showDialog determines that the ModalDialogManager is null (which can happen in certain embedders like WebEngine where layout isn’t inflated, or during navigation/backgrounding races), it immediately calls onDismiss(DismissalType.AUTODISMISS_NO_DIALOG_MANAGER).
  3. Native Re-entry: The onDismiss call synchronously invokes the native method PermissionDialogDelegate::Dismissed().
  4. Destruction: Inside Dismissed(), the code checks if (!permission_prompt_->IsShowing()). Because the prompt factory has not yet returned, PermissionRequestManager has not assigned the new prompt to its view_ member. Therefore, IsShowing() evaluates to false, and the code calls DestroyJavaDelegate(), which calls java_delegate_.reset().
  5. Use-After-Free: The reset() completely frees the PermissionDialogJavaDelegate object. The stack unwinds back to PermissionDialogJavaDelegate::CreateDialog(). The code proceeds to dereference permission_prompt_ from the now-deleted this object.

Note on MiraclePtr (BackupRefPtr): MiraclePtr does not mitigate this issue. The object being freed is the PermissionDialogJavaDelegate itself, which is owned by a std::unique_ptr and accessed via the raw C++ this pointer. Because there are no raw_ptrs pointing to the PermissionDialogJavaDelegate object, PartitionAlloc frees the memory immediately without quarantining or zapping it. The subsequent read of permission_prompt_ reads from freed, potentially reallocated memory.

Potential Exploitation Steps

An attacker could potentially exploit this with the following steps:

  1. Trigger a permission request (e.g., navigator.permissions.request({name: 'geolocation'})) on an Android device.
  2. Race the prompt creation with an action that invalidates the WindowAndroid or ModalDialogManager (e.g., navigating away, backgrounding the tab), or target an embedder where this is deterministically null.
  3. During the synchronous permission_prompt_->Dismiss() cascade triggered by DestroyJavaDelegate(), use the extensive observer notifications to spray the heap and reallocate the freed PermissionDialogJavaDelegate chunk with attacker-controlled bytes.
  4. When execution returns to CreateDialog(), the virtual method ShouldUseRequestingOriginFavicon() is called on the attacker’s fake object.
  5. The attacker controls the vtable, successfully hijacking execution flow to achieve Remote Code Execution (RCE) in the browser process.

Note: Our tooling agent does not have the capability to run code or provide a working PoC, so these steps are theoretical.

Suggested Fix

Do not synchronously destroy the Java delegate while it might be on the stack.

One approach is to have PermissionDialogDelegate::Dismissed() post the destruction of the java_delegate_ to the message loop (e.g., using base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon()), ensuring it is only destroyed after the current stack unwinds.

Alternatively, PermissionDialogJavaDelegate::CreateDialog could check a weak pointer to itself after the JNI call returns, or the JNI call could be made asynchronous so it does not block and re-enter synchronously.

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