CVE-2026-6315
Overview
Background
- `PermissionDialogDelegate`
- The C++ side of an Android permission prompt that owns a
PermissionDialogJavaDelegate(held injava_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::DeleteSoonposts 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.
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
- Open a permission prompt
A page triggers an Android permission request, creating a
PermissionDialogDelegatethat owns aPermissionDialogJavaDelegate. Ground: the class under patch and itsjava_delegate_member. - Invoke a Java delegate method
A dialog event causes a
PermissionDialogJavaDelegatemethod to begin executing (JNI into C++). Ground: the commit states the UaF occurs “while a PermissionDialogJavaDelegate method is running.” - Re-enter `Dismissed` during that method
Before that method returns,
Dismissed()runs and, pre-patch, calledDestroyJavaDelegate()→java_delegate_.reset(), freeing the delegate whose method is still on the stack. Ground: the removedDestroyJavaDelegate()call inDismissed. - 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
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.cccomponents/permissions/android/permission_prompt/permission_dialog_delegate.h
Audit Directions
- Synchronous self-destruction in callbacksSearch for objects that
reset()/deletea member (orthis) inside dismiss/close/complete callbacks that may be entered while that member’s own method is running; preferDeleteSoon. - JNI/bridge delegate lifetimesReview 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 dispatchAudit permission/dialog/prompt code paths where
Dismiss/Dismissedcan fire during an in-progress operation, confirming owned resources are freed only after the stack unwinds.
Patch
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;
Original Bug Report
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.cccomponents/permissions/android/permission_prompt/permission_dialog_delegate.hcomponents/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogController.javacomponents/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogDelegate.javacomponents/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
// ...
}
}
- JNI Execution: The code calls
PermissionDialogController::CreateDialogin Java. - Synchronous Dismissal: In Java, if
PermissionDialogCoordinator.showDialogdetermines that theModalDialogManageris null (which can happen in certain embedders like WebEngine where layout isn’t inflated, or during navigation/backgrounding races), it immediately callsonDismiss(DismissalType.AUTODISMISS_NO_DIALOG_MANAGER). - Native Re-entry: The
onDismisscall synchronously invokes the native methodPermissionDialogDelegate::Dismissed(). - Destruction: Inside
Dismissed(), the code checksif (!permission_prompt_->IsShowing()). Because the prompt factory has not yet returned,PermissionRequestManagerhas not assigned the new prompt to itsview_member. Therefore,IsShowing()evaluates tofalse, and the code callsDestroyJavaDelegate(), which callsjava_delegate_.reset(). - Use-After-Free: The
reset()completely frees thePermissionDialogJavaDelegateobject. The stack unwinds back toPermissionDialogJavaDelegate::CreateDialog(). The code proceeds to dereferencepermission_prompt_from the now-deletedthisobject.
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:
- Trigger a permission request (e.g.,
navigator.permissions.request({name: 'geolocation'})) on an Android device. - Race the prompt creation with an action that invalidates the
WindowAndroidorModalDialogManager(e.g., navigating away, backgrounding the tab), or target an embedder where this is deterministically null. - During the synchronous
permission_prompt_->Dismiss()cascade triggered byDestroyJavaDelegate(), use the extensive observer notifications to spray the heap and reallocate the freedPermissionDialogJavaDelegatechunk with attacker-controlled bytes. - When execution returns to
CreateDialog(), the virtual methodShouldUseRequestingOriginFavicon()is called on the attacker’s fake object. - 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.