High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Permissions
DescriptionUse after free in Permissions
ComponentPermissions
Sub-componentPermissions
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker499247910
Fix commit5457c6e7e7cb (chromium/src) +5/-4
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-03 (per vendor advisory)
Fix landed2026-04-07 (public git)
Disclosed2026-04-15
Patch gap8 days — fix public in source before the advisory

Background

`PermissionDialogDelegate`
The C++ side of an Android permission prompt that owns a PermissionDialogJavaDelegate (held in java_delegate_) bridging to the Java dialog UI.
`PermissionDialogJavaDelegate`
The Java-facing delegate object whose methods are invoked (via JNI) in response to dialog events; destroying it while one of its methods is on the stack is a re-entrancy hazard.
`DeleteSoon`
base::SequencedTaskRunner::DeleteSoon posts the object’s destruction to run later on the current sequence, so the delete happens after the current call stack unwinds.
Re-entrancy / use-after-free
Destroying an object synchronously from inside a callback that the object itself (or its sibling) is still executing can leave a live frame operating on freed memory.

Root Cause Analysis

In PermissionDialogDelegate::Dismissed (the enclosing function is indicated by the hunk’s comment about PermissionPrompt creation failing, consistent with the commit title), the code synchronously destroyed the Java delegate by calling DestroyJavaDelegate(), which immediately did java_delegate_.reset(). Per the commit, Dismissed() can be invoked while a PermissionDialogJavaDelegate method is still running; freeing java_delegate_ right then tears down an object whose method frame is still on the stack, i.e. a use-after-free via re-entrancy.

The fix changes Dismissed to post the destruction asynchronously: base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE, std::move(java_delegate_)), so the delegate is destroyed only after the current call stack unwinds and any in-progress delegate method has returned. The helper DestroyJavaDelegate is removed from the header, and PermissionDialogDelegate::Destroy(JNIEnv*) now inlines java_delegate_.reset() directly (an explicit Java-initiated teardown where synchronous reset is safe). The invariant restored is that java_delegate_ is not freed while one of its own methods is executing.

Key insight
Dismissed() could run re-entrantly during a live PermissionDialogJavaDelegate method and synchronously freed that delegate, causing a use-after-free; the fix defers the deletion with DeleteSoon so it happens only after the stack unwinds.

Attack Path

  1. Open a permission prompt A page triggers an Android permission request, creating a PermissionDialogDelegate that owns a PermissionDialogJavaDelegate. Ground: the class under patch and its java_delegate_ member.
  2. Invoke a Java delegate method A dialog event causes a PermissionDialogJavaDelegate method to begin executing (JNI into C++). Ground: the commit states the UaF occurs “while a PermissionDialogJavaDelegate method is running.”
  3. Re-enter `Dismissed` during that method Before that method returns, Dismissed() runs and, pre-patch, called DestroyJavaDelegate() → java_delegate_.reset(), freeing the delegate whose method is still on the stack. Ground: the removed DestroyJavaDelegate() call in Dismissed.
  4. Use freed delegate The still-executing delegate method (or subsequent access) dereferences the now-freed object, a use-after-free. Ground: the commit’s stated UaF; a concrete exploitation primitive beyond the crash is (inferred).

Impact Assessment

On Android, a web page able to drive the permission-prompt lifecycle can provoke a use-after-free of the PermissionDialogJavaDelegate by causing Dismissed() to run re-entrantly while a delegate method is executing, which the advisory rates High severity (“Use after free in Permissions”). The precondition is the specific re-entrant timing during a live delegate call; the advisory does not report in-the-wild exploitation. Any escalation from the UaF to controlled memory corruption or code execution is (inferred); the patch demonstrates only the lifetime bug.

Files Changed

  • components/permissions/android/permission_prompt/permission_dialog_delegate.cc
  • components/permissions/android/permission_prompt/permission_dialog_delegate.h

Audit Directions

  • Synchronous self-destruction in callbacks
    Search for objects that reset()/delete a member (or this) inside dismiss/close/complete callbacks that may be entered while that member’s own method is running; prefer DeleteSoon.
  • JNI/bridge delegate lifetimes
    Review Android bridge delegates where Java-side events re-enter C++ and can trigger teardown of the very object servicing the event.
  • Re-entrancy via event dispatch
    Audit permission/dialog/prompt code paths where Dismiss/Dismissed can fire during an in-progress operation, confirming owned resources are freed only after the stack unwinds.
From 5457c6e7e7cb051f87fe90545cfd68c5640432c4 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
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 <antoniosartori@chromium.org>
Reviewed-by: Balazs Engedy <engedy@chromium.org>
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 vm...@google.com

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