CVE-2026-9888
Overview
Background
- `AwContents`
- The native (C++) peer of the Android WebView
AwContentsJava object; it holds a weakjava_ref_to the Java side, which can be garbage-collected independently. - `AwPermissionRequest`
- A native object (with a paired Java peer) representing a single pending WebView permission prompt, created via
AwPermissionRequest::Create. - `PermissionRequestHandler`
- Browser-side manager that tracks outstanding permission requests in
requests_(a vector ofbase::WeakPtr<AwPermissionRequest>) and notifies its client (AwContents). - `PermissionRequestHandlerClient`
- The interface
AwContentsimplements to receiveOnPermissionRequestcallbacks.
Root Cause Analysis
In the pre-patch flow, the method in permission_request_handler.cc eagerly called AwPermissionRequest::Create to build both the native request and its Java peer, pushed the resulting weak pointer into requests_, and only then invoked client_->OnPermissionRequest(java_peer, weak_request.get()). Inside AwContents::OnPermissionRequest, if the Java AwContents had already been garbage-collected (java_ref_.get(env) returned null), the code called permission_request_handler_->CancelRequest(request->GetOrigin(), request->GetResources()) to tear the just-created request back down on the same stack. This create-then-immediately-cancel dance on the GC’d path meant an AwPermissionRequest was constructed, registered, and destroyed re-entrantly, leaving a dangling pointer/registration that could be used after the object was freed (the exact downstream dereference is (inferred)).
The fix inverts the order so the object is created lazily: OnPermissionRequest now receives a std::unique_ptr<AwPermissionRequestDelegate>, returns early with nullptr when j_ref is null (creating nothing), and only calls AwPermissionRequest::Create after the Java object is confirmed alive; the handler registers the request in requests_ only when a non-null base::WeakPtr comes back.
AwContents still existed, so on the garbage-collected path it had to cancel and free the object it had just created — the fix defers creation until after the liveness check, so nothing is created that must be immediately destroyed.Attack Path
- Trigger a permission prompt
Web content loaded in a WebView requests a permission (e.g. geolocation or media capture), driving
PermissionRequestHandlerto begin a new request. - Race the Java object's lifetime
The embedding Java
AwContentsis garbage-collected so thatjava_ref_is null by the time the native request is being set up. - Eager creation then cancel
Pre-patch,
AwPermissionRequest::Createhas already built the native object and pushed its weak pointer intorequests_, then the null-j_refbranch callsCancelRequest, destroying the object during the same callback. - Use-after-free window
A reference to the just-freed
AwPermissionRequest(registered inrequests_and/or passed asweak_request.get()) is used after destruction(inferred), corrupting browser-process state.
Impact Assessment
AwContents, trigger a use-after-free in the app’s browser-side process where WebView runs; exploitation requires winning this GC/timing race rather than a non-default flag or extension. The vendor advisory rates this critical; any escalation beyond the use-after-free itself is (inferred) and the advisory does not state in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/browser/aw_contents.cc |
modified | |
ifandroid_webview/browser/permission/permission_request_handler.cc |
modified | |
AwPermissionRequestandroid_webview/browser/permission/permission_request_handler_client.h |
modified | |
AwPermissionRequestDelegateandroid_webview/browser/permission/permission_request_handler_client.h |
modified | |
PermissionRequestHandlerClientandroid_webview/browser/permission/permission_request_handler_client.h |
modified | |
ifandroid_webview/browser/permission/permission_request_handler_unittest.cc |
modified | |
TestPermissionRequestHandlerandroid_webview/browser/permission/permission_request_handler_unittest.cc |
modified |
Files Changed
android_webview/browser/aw_contents.ccandroid_webview/browser/aw_contents.handroid_webview/browser/permission/permission_request_handler.ccandroid_webview/browser/permission/permission_request_handler_client.handroid_webview/browser/permission/permission_request_handler_unittest.cc
Audit Directions
- Eager allocation before liveness checksLook for native WebView/browser objects created (especially with a paired Java peer) before verifying that a weak owner reference such as
java_ref_is still valid; defer creation past the guard. - Create-then-cancel on failure pathsFlag callbacks that build an object and then synchronously cancel/destroy it on an early-exit branch, which invites re-entrant use-after-free; prefer returning before construction.
- WeakPtr registries mutated during callbacksAudit containers like
requests_that are populated before invoking a client callback that may destroy or prune their entries on the same stack.
Patch
From 88f9f577ff437c1066b9a1815fde2e30a744484d Mon Sep 17 00:00:00 2001 From: Peter Pakkenberg <[email protected]> Date: Thu, 14 May 2026 04:49:21 -0700 Subject: [PATCH] Avoid creating AwPermissionRequest for destroyed contents There is no need to create an AwPermissionRequest only to immediately cancel and delete it if the Java AwContents object has already been garbage collected. This change only creates the Java object once we've determined that it is actually required. Fixed: 511715166 Change-Id: Ied57deba7c8b92aafd9359d8cf617fe89118d6cd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7845196 Commit-Queue: Peter Beverloo <[email protected]> Auto-Submit: Peter Pakkenberg <[email protected]> Reviewed-by: Peter Beverloo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1630578} --- diff --git a/android_webview/browser/aw_contents.cc b/android_webview/browser/aw_contents.cc index 94e6b752..b632f8b 100644 --- a/android_webview/browser/aw_contents.cc +++ b/android_webview/browser/aw_contents.cc @@ -733,21 +733,23 @@ } } -void AwContents::OnPermissionRequest( - base::android::ScopedJavaLocalRef<jobject> j_request, - AwPermissionRequest* request) { - DCHECK(j_request); - DCHECK(request); +base::WeakPtr<AwPermissionRequest> AwContents::OnPermissionRequest( + std::unique_ptr<AwPermissionRequestDelegate> permission_request) { + DCHECK(permission_request); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef<jobject> j_ref = java_ref_.get(env); if (!j_ref) { - permission_request_handler_->CancelRequest(request->GetOrigin(), - request->GetResources()); - return; + return nullptr; } + base::WeakPtr<AwPermissionRequest> weak_request; + // The Create method also assigns the `weak_request` ptr. + base::android::ScopedJavaLocalRef<jobject> j_request = + AwPermissionRequest::Create(std::move(permission_request), &weak_request); + Java_AwContents_onPermissionRequest(env, j_ref, j_request); + return weak_request; } void AwContents::OnPermissionRequestCanceled(AwPermissionRequest* request) { diff --git a/android_webview/browser/aw_contents.h b/android_webview/browser/aw_contents.h index 71c470d..d178db6 100644 --- a/android_webview/browser/aw_contents.h +++ b/android_webview/browser/aw_contents.h @@ -228,8 +228,8 @@ bool GetViewTreeForceDarkState() { return view_tree_force_dark_state_; } // PermissionRequestHandlerClient implementation. - void OnPermissionRequest(base::android::ScopedJavaLocalRef<jobject> j_request, - AwPermissionRequest* request) override; + base::WeakPtr<AwPermissionRequest> OnPermissionRequest( + std::unique_ptr<AwPermissionRequestDelegate> permission_request) override; void OnPermissionRequestCanceled(AwPermissionRequest* request) override; PermissionRequestHandler* GetPermissionRequestHandler() { diff --git a/android_webview/browser/permission/permission_request_handler.cc b/android_webview/browser/permission/permission_request_handler.cc index cd43127..1f47e53 100644 --- a/android_webview/browser/permission/permission_request_handler.cc +++ b/android_webview/browser/permission/permission_request_handler.cc @@ -52,11 +52,11 @@ return; } - base::WeakPtr<AwPermissionRequest> weak_request; - base::android::ScopedJavaLocalRef<jobject> java_peer = - AwPermissionRequest::Create(std::move(request), &weak_request); - requests_.push_back(weak_request); - client_->OnPermissionRequest(java_peer, weak_request.get()); + base::WeakPtr<AwPermissionRequest> weak_request = + client_->OnPermissionRequest(std::move(request)); + if (weak_request) { + requests_.push_back(weak_request); + } PruneRequests(); } diff --git a/android_webview/browser/permission/permission_request_handler_client.h b/android_webview/browser/permission/permission_request_handler_client.h index 51eb98e..a6a4c04e 100644 --- a/android_webview/browser/permission/permission_request_handler_client.h +++ b/android_webview/browser/permission/permission_request_handler_client.h @@ -5,20 +5,26 @@ #ifndef ANDROID_WEBVIEW_BROWSER_PERMISSION_PERMISSION_REQUEST_HANDLER_CLIENT_H_ #define ANDROID_WEBVIEW_BROWSER_PERMISSION_PERMISSION_REQUEST_HANDLER_CLIENT_H_ +#include <memory> + #include "base/android/scoped_java_ref.h" +#include "base/memory/weak_ptr.h" namespace android_webview { class AwPermissionRequest; +class AwPermissionRequestDelegate; class PermissionRequestHandlerClient { public: PermissionRequestHandlerClient(); virtual ~PermissionRequestHandlerClient(); - virtual void OnPermissionRequest( - base::android::ScopedJavaLocalRef<jobject> java_request, - AwPermissionRequest* request) = 0; + // Handles the permission request. Will return a WeakPtr to the created + // AwPermissionRequest if one was instantiated, or `nullptr` if no Java + // objects were created. + virtual base::WeakPtr<AwPermissionRequest> OnPermissionRequest( + std::unique_ptr<AwPermissionRequestDelegate> permission_request) = 0; virtual void OnPermissionRequestCanceled(AwPermissionRequest* request) = 0; }; diff --git a/android_webview/browser/permission/permission_request_handler_unittest.cc b/android_webview/browser/permission/permission_request_handler_unittest.cc index a0374be..f32d3b6 100644 --- a/android_webview/browser/permission/permission_request_handler_unittest.cc +++ b/android_webview/browser/permission/permission_request_handler_unittest.cc @@ -54,13 +54,19 @@ TestPermissionRequestHandlerClient() : request_(nullptr) {} - void OnPermissionRequest(base::android::ScopedJavaLocalRef<jobject> j_request, - AwPermissionRequest* request) override { - DCHECK(request); - request_ = request; - java_request_ = j_request; - requested_permission_ = - Permission(request->GetOrigin(), request->GetResources()); + base::WeakPtr<AwPermissionRequest> OnPermissionRequest( + std::unique_ptr<AwPermissionRequestDelegate> permission_request) + override { + DCHECK(permission_request); + base::WeakPtr<AwPermissionRequest> weak_request; + java_request_ = AwPermissionRequest::Create(std::move(permission_request), + &weak_request); + request_ = weak_request.get(); + if (request_) { + requested_permission_ = + Permission(request_->GetOrigin(), request_->GetResources()); + } + return weak_request; } void OnPermissionRequestCanceled(AwPermissionRequest* request) override { @@ -110,7 +116,7 @@ class TestPermissionRequestHandler : public PermissionRequestHandler { public: - TestPermissionRequestHandler(PermissionRequestHandlerClient* client) + explicit TestPermissionRequestHandler(PermissionRequestHandlerClient* client) : PermissionRequestHandler(client, nullptr) {} const std::vector<base::WeakPtr<AwPermissionRequest>> requests() {
Regression Test / PoC
diff --git a/android_webview/browser/permission/permission_request_handler_unittest.cc b/android_webview/browser/permission/permission_request_handler_unittest.cc
index a0374be..f32d3b6 100644
--- a/android_webview/browser/permission/permission_request_handler_unittest.cc
+++ b/android_webview/browser/permission/permission_request_handler_unittest.cc
@@ -54,13 +54,19 @@
TestPermissionRequestHandlerClient() : request_(nullptr) {}
- void OnPermissionRequest(base::android::ScopedJavaLocalRef<jobject> j_request,
- AwPermissionRequest* request) override {
- DCHECK(request);
- request_ = request;
- java_request_ = j_request;
- requested_permission_ =
- Permission(request->GetOrigin(), request->GetResources());
+ base::WeakPtr<AwPermissionRequest> OnPermissionRequest(
+ std::unique_ptr<AwPermissionRequestDelegate> permission_request)
+ override {
+ DCHECK(permission_request);
+ base::WeakPtr<AwPermissionRequest> weak_request;
+ java_request_ = AwPermissionRequest::Create(std::move(permission_request),
+ &weak_request);
+ request_ = weak_request.get();
+ if (request_) {
+ requested_permission_ =
+ Permission(request_->GetOrigin(), request_->GetResources());
+ }
+ return weak_request;
}
void OnPermissionRequestCanceled(AwPermissionRequest* request) override {
@@ -110,7 +116,7 @@
class TestPermissionRequestHandler : public PermissionRequestHandler {
public:
- TestPermissionRequestHandler(PermissionRequestHandlerClient* client)
+ explicit TestPermissionRequestHandler(PermissionRequestHandlerClient* client)
: PermissionRequestHandler(client, nullptr) {}
const std::vector<base::WeakPtr<AwPermissionRequest>> requests() {
Original Bug Report
Potential Use-After-Free in PermissionRequestHandler during synchronous CleanupReference queue drain
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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in Android WebView when a synchronous Java CleanupReference execution unexpectedly destroys the native AwContents object. This deletes the currently executing PermissionRequestHandler, leading to a UAF write when the execution stack unwinds.
Affected files:
android_webview/browser/permission/permission_request_handler.ccandroid_webview/browser/aw_contents.ccandroid_webview/java/src/org/chromium/android_webview/CleanupReference.javaandroid_webview/java/src/org/chromium/android_webview/permission/AwPermissionRequest.javaandroid_webview/browser/permission/aw_permission_request.cc
Estimated timestamp from git blame: 2026-01-26
Vulnerability Details
A potential Use-After-Free (UAF) vulnerability exists in the Android WebView browser process (the embedding application process). The issue is caused by an unsafe synchronous reentrancy and destruction pattern involving Java’s CleanupReference mechanism.
In Android WebView, native objects are typically managed by Java peer objects using the CleanupReference class. When a Java object is garbage collected, its corresponding CleanupReference is enqueued in a global queue (sGcQueue) to be processed by a reaper thread.
If an embedder application abandons a WebView without explicitly calling destroy(), the Java AwContents object is eventually garbage collected, and its cleanup task is added to sGcQueue. A race condition occurs if a renderer-initiated permission request (e.g., Geolocation, MIDI) is processed on the UI thread while the AwContents cleanup task is still pending in the queue.
Potential Execution Chain:
PermissionRequestHandler::SendRequestis called on the UI thread to handle a new permission request from the renderer.- It calls
client_->OnPermissionRequest(implemented by the C++AwContents). - Inside
AwContents::OnPermissionRequest, it attempts to fetch the Java peer. Since the JavaAwContentshas already been collected, the JNI weak reference returns null. It then synchronously re-entersPermissionRequestHandler::CancelRequest()to cleanly abort the request. CancelRequestenters awhileloop, retrieves an iteratorito the request, and callsCancelRequestInternal, which invokesAwPermissionRequest::CancelAndDelete.- This triggers a JNI call to
AwPermissionRequest.destroyNative, which explicitly callsmCleanupReference.cleanupNow()in the Java layer. CleanupReference.cleanupNow()dispatches a message to the UI thread. Since execution is already on the UI thread, the message handler runs synchronously.- Crucially, this message handler contains a loop that drains the entire global
sGcQueuesynchronously:while ((ref = (CleanupReference) sGcQueue.poll()) != null) { ref.runCleanupTaskInternal(); } - If the
AwContentscleanup task is still in the queue, it is executed synchronously. This triggers the destruction of the nativeAwContentsobject (delete this). BecauseAwContentsowns thePermissionRequestHandlervia astd::unique_ptr, the handler is also immediately deleted from the heap. - When execution unwinds back to the C++
PermissionRequestHandler::CancelRequestmethod, it resumes thewhileloop and executesrequests_.erase(i). - At this point, the implicit
thispointer (thePermissionRequestHandler) and itsrequests_vector have been deleted, resulting in a UAF read and write on the vector’s freed heap buffer.
(Note: These are potential steps based on code analysis; we do not yet have a working proof of concept.)
Impact
This vulnerability allows a compromised renderer to trigger a Use-After-Free write in the unsandboxed browser process (the embedding Android application). This provides a strong primitive that can lead to Remote Code Execution (RCE) in the context of the application using WebView.
This vulnerability is not protected by MiraclePtr (BRP) because the access occurs through the implicit this pointer held on the call stack and a stack-based iterator (i), both of which bypass BRP protections.
Suggested Fix
To prevent this synchronous destruction race, avoid executing the entire sGcQueue synchronously during an explicit cleanupNow(). Alternatively, in PermissionRequestHandler::CancelRequest or AwContents::OnPermissionRequest, the cancellation callbacks or the destruction of the AwPermissionRequest should be posted asynchronously (e.g., using base::SingleThreadTaskRunner::PostTask) rather than executed synchronously on the current call stack.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
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.