Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebAppInstalls
DescriptionUse after free in WebAppInstalls
ComponentWebAppInstalls
Bug ClassUAF
Tracker499423683
Fix commiteda39112c6c1 (chromium/src) +33/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
GetPermissionCallbacks
chrome/browser/webapps/installable/installed_webapp_bridge.cc
modified
if
chrome/browser/webapps/installable/installed_webapp_bridge.cc
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
  • chrome/browser/webapps/installable/installed_webapp_bridge.cc
From eda39112c6c198796f0ba5a37ca0802f2235ee7f Mon Sep 17 00:00:00 2001
From: Adriana Ixba <aixba@chromium.org>
Date: Fri, 24 Apr 2026 10:04:09 -0700
Subject: [PATCH] [WebAPK] Clean up permission callback

This helps solve a UAF vulnerability in WebAPK permissions where the
permission callback gets called more than once, causing an invalid ptr
to be dereferenced

- Track if the callback has already been executed on the Java side
- Use a permissions_callback map to avoid passing raw callback ptrs and
dereference invalid ones

Bug: 499423683
Change-Id: I409806a0e91ef22f10cfb1bead8422baf4ff6925
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7784189
Reviewed-by: Glenn Hartmann <hartmanng@chromium.org>
Commit-Queue: Adriana Ixba <aixba@chromium.org>
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620287}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
index 6dc07fd8..d697cf1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
@@ -43,6 +43,8 @@
 import org.chromium.webapk.lib.client.WebApkServiceConnectionManager;
 import org.chromium.webapk.lib.runtime_library.IWebApkApi;
 
+import java.util.concurrent.atomic.AtomicBoolean;
+
 /**
  * Provides APIs for browsers to communicate with WebAPK services. Each WebAPK has its own "WebAPK
  * service".
@@ -115,9 +117,11 @@
     }
 
     private static Handler createPermissionHandler(Callback<Integer> permissionCallback) {
+        final AtomicBoolean called = new AtomicBoolean(false);
         return new Handler(
                 Looper.getMainLooper(),
                 message -> {
+                    if (called.getAndSet(true)) return true;
                     @ContentSetting
                     int settingValue =
                             toContentSettingValue(
diff --git a/chrome/browser/webapps/installable/installed_webapp_bridge.cc b/chrome/browser/webapps/installable/installed_webapp_bridge.cc
index f7d8b93..0ae847a6 100644
--- a/chrome/browser/webapps/installable/installed_webapp_bridge.cc
+++ b/chrome/browser/webapps/installable/installed_webapp_bridge.cc
@@ -4,13 +4,16 @@
 
 #include "chrome/browser/webapps/installable/installed_webapp_bridge.h"
 
+#include <memory>
 #include <utility>
 #include <variant>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
 #include "base/android/jni_utils.h"
+#include "base/containers/id_map.h"
 #include "base/memory/safety_checks.h"
+#include "base/no_destructor.h"
 #include "components/content_settings/core/common/content_settings.h"
 #include "components/content_settings/core/common/content_settings_types.h"
 #include "components/content_settings/core/common/content_settings_utils.h"
@@ -41,6 +44,14 @@
   ADVANCED_MEMORY_SAFETY_CHECKS();
 };
 
+base::IDMap<std::unique_ptr<PermissionCallbackWithAMSC>, int64_t>&
+GetPermissionCallbacks() {
+  static base::NoDestructor<
+      base::IDMap<std::unique_ptr<PermissionCallbackWithAMSC>, int64_t>>
+      permission_callbacks;
+  return *permission_callbacks;
+}
+
 }  // namespace
 
 static void JNI_InstalledWebappBridge_NotifyPermissionsChange(
@@ -50,20 +61,21 @@
   ContentSettingsType type = static_cast<ContentSettingsType>(type_int);
   DCHECK(IsKnownEnumValue(type));
   InstalledWebappProvider* provider =
-    reinterpret_cast<InstalledWebappProvider*>(j_provider);
+      reinterpret_cast<InstalledWebappProvider*>(j_provider);
   provider->Notify(type);
 }
 
-static void JNI_InstalledWebappBridge_RunPermissionCallback(
-    JNIEnv* env,
-    int64_t callback_ptr,
-    int setting) {
+static void JNI_InstalledWebappBridge_RunPermissionCallback(JNIEnv* env,
+                                                            int64_t callback_id,
+                                                            int setting) {
   DCHECK_LE(setting, static_cast<int>(PermissionDecision::kMaxValue));
-  auto* callback = reinterpret_cast<PermissionCallbackWithAMSC*>(callback_ptr);
-  std::move(*callback).Run(
-      static_cast<PermissionDecision>(static_cast<PermissionDecision>(setting)),
-      /*is_final_decision=*/true);
-  delete callback;
+  auto* callback = GetPermissionCallbacks().Lookup(callback_id);
+  if (!callback) {
+    return;
+  }
+  std::move(*callback).Run(static_cast<PermissionDecision>(setting),
+                           /*is_final_decision=*/true);
+  GetPermissionCallbacks().Remove(callback_id);
 }
 
 InstalledWebappProvider::RuleList
@@ -91,7 +103,7 @@
 }
 
 void InstalledWebappBridge::SetProviderInstance(
-    InstalledWebappProvider *provider) {
+    InstalledWebappProvider* provider) {
   Java_InstalledWebappBridge_setInstalledWebappProvider(
       base::android::AttachCurrentThread(), (int64_t)provider);
 }
@@ -116,8 +128,8 @@
   // dialog, but as the dialog is modal, the only other thing the user can do
   // is quit Chrome which will also free the pointer. The callback pointer will
   // be destroyed in RunPermissionCallback.
-  PermissionCallbackWithAMSC* callback_ptr =
-      new PermissionCallbackWithAMSC(base::BindOnce(
+  auto callback_with_amsc =
+      std::make_unique<PermissionCallbackWithAMSC>(base::BindOnce(
           [](PermissionCallback callback, const PromptOptions& prompt_options,
              PermissionDecision decision, bool is_final_decision) {
             std::move(callback).Run(permissions::PermissionPromptDecision{
@@ -127,9 +139,12 @@
           },
           std::move(callback), prompt_options));
 
+  int64_t callback_id =
+      GetPermissionCallbacks().Add(std::move(callback_with_amsc));
+
   Java_InstalledWebappBridge_decidePermission(
       env, static_cast<int>(type), origin_url.spec(), last_committed_url.spec(),
-      reinterpret_cast<int64_t>(callback_ptr));
+      callback_id);
 }
 
 DEFINE_JNI(InstalledWebappBridge)
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF in Browser Process via WebAPK Notification Permission JNI Callback

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 without the security team.

Overview: A Use-After-Free exists in InstalledWebappBridge due to an unenforced single-execution assumption in Java handlers. A malicious Android app can bypass WebAPK signature checks by spoofing ‘Maps Lite’ and send multiple permission responses to trigger a double-free/UAF of an 8-byte callback object. This can potentially be exploited for arbitrary code execution in the browser process.

Affected files:

  • chrome/browser/webapps/installable/installed_webapp_bridge.cc
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/NotificationPermissionUpdater.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java

Estimated timestamp from git blame: 2026-01-20

Summary

A memory safety vulnerability (Use-After-Free leading to arbitrary code execution) exists in the JNI callback mechanism used for webapp permission requests on Android. The vulnerability is located in InstalledWebappBridge::DecidePermission and its associated JNI logic, and can be triggered by a malicious Android application.

Technical Details

In InstalledWebappBridge::DecidePermission, a heap-allocated PermissionCallbackWithAMSC (a wrapper for base::OnceCallback) is created, and its raw pointer is passed to the Java layer:

// chrome/browser/webapps/installable/installed_webapp_bridge.cc
PermissionCallbackWithAMSC* callback_ptr = new PermissionCallbackWithAMSC(...);
Java_InstalledWebappBridge_decidePermission(..., reinterpret_cast<int64_t>(callback_ptr));

The Java side is expected to call the JNI method JNI_InstalledWebappBridge_RunPermissionCallback exactly once. When called, the C++ side casts the pointer back, executes the callback, and deletes the object:

static void JNI_InstalledWebappBridge_RunPermissionCallback(
    JNIEnv* env, int64_t callback_ptr, int setting) {
  auto* callback = reinterpret_cast<PermissionCallbackWithAMSC*>(callback_ptr);
  std::move(*callback).Run(...);
  delete callback;
}

When notification permissions are requested via a WebAPK, WebApkServiceClient.requestNotificationPermission creates a Java Handler wrapped in a Messenger to receive the result. Crucially, this Handler does not enforce single execution (unlike TrustedWebActivityClient.PermissionCallback which uses mCalled):

// chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java
private static Handler createPermissionHandler(Callback<Integer> permissionCallback) {
    return new Handler(Looper.getMainLooper(), message -> {
        // ... 
        permissionCallback.onResult(settingValue);
        return true;
    });
}

If the WebAPK sends multiple messages to this Messenger, the Handler processes each one, calling the JNI method multiple times with the exact same callback_ptr.

Bypassing Signature Checks

To trigger this as an attacker, the WebAPK signature check must be bypassed. WebApkValidator.java contains a hardcoded bypass (verifyMapsLite) that skips cryptographic signature validation if the package name is com.google.android.apps.mapslite and certain manifest metadata matches. A malicious app can adopt this package name, spoof the identity service, and successfully masquerade as a valid WebAPK.

Potential Attack Steps

(Note: These are suggested steps based on static analysis.)

  1. Attacker installs a malicious Android app using the com.google.android.apps.mapslite package name and matching START_URL/SCOPE metadata to bypass signature validation.
  2. The app registers an intent filter for an attacker-controlled origin (e.g., https://attacker.com/) and implements org.webapk.IDENTITY_SERVICE_API to return Chrome’s package name.
  3. The user visits https://attacker.com/, which triggers a notification permission request.
  4. Chrome sends a PendingIntent with the Messenger to the malicious app.
  5. The app sends two rapid messages to the Messenger.
  6. Chrome processes the first message, executing Run() and calling delete callback;. Because the object uses ADVANCED_MEMORY_SAFETY_CHECKS, it is temporarily placed in a global quarantine.
  7. The attacker uses JS to create/destroy many iframes, triggering sufficient browser-process ADVANCED_MEMORY_SAFETY_CHECKS allocations (e.g., RenderFrameHost) to exceed the 512KB quarantine limit, flushing the callback chunk back to the main PartitionAlloc free list.
  8. The attacker sends many small IPC payloads (e.g., via Mojo) to reclaim the 8-byte chunk, forging the bind_state_ pointer of the OnceCallback.
  9. Chrome processes the second message. The JNI method uses the same raw pointer, dereferences the forged bind_state_, and jumps to an attacker-controlled polymorphic_invoke_ pointer, achieving Remote Code Execution in the browser process.

Suggested Fix

  1. Enforce single execution in createPermissionHandler inside WebApkServiceClient.java by tracking whether the callback has already been fired (similar to mCalled elsewhere).
  2. Instead of passing a raw int64_t pointer across JNI, use an ID-based lookup mechanism (e.g., a map of integers to std::unique_ptr<PermissionCallbackWithAMSC>) in InstalledWebappBridge so that subsequent lookup attempts safely fail.
  3. Review and consider removing the Maps Lite signature validation bypass in WebApkValidator.java.

Evaluated with Chrome root at commit: 09ec9e7cc4d24823d20b6d37cf3d282734f6bf0f


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
Links in the report