CVE-2026-10923
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
GetPermissionCallbackschrome/browser/webapps/installable/installed_webapp_bridge.cc |
modified | |
ifchrome/browser/webapps/installable/installed_webapp_bridge.cc |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.javachrome/browser/webapps/installable/installed_webapp_bridge.cc
Patch
From eda39112c6c198796f0ba5a37ca0802f2235ee7f Mon Sep 17 00:00:00 2001 From: Adriana Ixba <[email protected]> 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 <[email protected]> Commit-Queue: Adriana Ixba <[email protected]> Reviewed-by: Dibyajyoti Pal <[email protected]> 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)
Original Bug Report
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.ccchrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/NotificationPermissionUpdater.javachrome/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.)
- Attacker installs a malicious Android app using the
com.google.android.apps.mapslitepackage name and matchingSTART_URL/SCOPEmetadata to bypass signature validation. - The app registers an intent filter for an attacker-controlled origin (e.g.,
https://attacker.com/) and implementsorg.webapk.IDENTITY_SERVICE_APIto return Chrome’s package name. - The user visits
https://attacker.com/, which triggers a notification permission request. - Chrome sends a
PendingIntentwith theMessengerto the malicious app. - The app sends two rapid messages to the
Messenger. - Chrome processes the first message, executing
Run()and callingdelete callback;. Because the object usesADVANCED_MEMORY_SAFETY_CHECKS, it is temporarily placed in a global quarantine. - The attacker uses JS to create/destroy many iframes, triggering sufficient browser-process
ADVANCED_MEMORY_SAFETY_CHECKSallocations (e.g.,RenderFrameHost) to exceed the 512KB quarantine limit, flushing the callback chunk back to the main PartitionAlloc free list. - The attacker sends many small IPC payloads (e.g., via Mojo) to reclaim the 8-byte chunk, forging the
bind_state_pointer of theOnceCallback. - Chrome processes the second message. The JNI method uses the same raw pointer, dereferences the forged
bind_state_, and jumps to an attacker-controlledpolymorphic_invoke_pointer, achieving Remote Code Execution in the browser process.
Suggested Fix
- Enforce single execution in
createPermissionHandlerinsideWebApkServiceClient.javaby tracking whether the callback has already been fired (similar tomCalledelsewhere). - Instead of passing a raw
int64_tpointer across JNI, use an ID-based lookup mechanism (e.g., a map of integers tostd::unique_ptr<PermissionCallbackWithAMSC>) inInstalledWebappBridgeso that subsequent lookup attempts safely fail. - Review and consider removing the
Maps Litesignature validation bypass inWebApkValidator.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.