CVE-2026-8520
Overview
Files Changed
components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.javacomponents/payments/content/android/jni_payment_app.cccomponents/payments/content/android/jni_payment_app.h
Patch
From 133845186794aa8058a8c6005f92c9028427cb16 Mon Sep 17 00:00:00 2001 From: Stephen McGruer <[email protected]> Date: Mon, 20 Apr 2026 11:07:12 -0700 Subject: [PATCH] payments: Ensure JniPaymentApp is destroyed on UI thread JniPaymentApp is created on the UI thread, but its destruction (triggered by the Java side) was not guaranteed to happen on the same thread. This could lead to security and stability issues, as it interacts with UI-pinned components. Bug: 503619813 Change-Id: Icb720fe9866ee7af8f22e6171b076f415875e70b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7774970 Reviewed-by: Slobodan Pejic <[email protected]> Commit-Queue: Stephen McGruer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1617619} --- diff --git a/components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java b/components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java index 9d474eb..462b3f4c 100644 --- a/components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java +++ b/components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java @@ -260,7 +260,7 @@ @Override public void dismissInstrument() { if (mNativeObject == 0) return; - JniPaymentAppJni.get().freeNativeObject(mNativeObject); + JniPaymentAppJni.get().freeNativeObjectSoon(mNativeObject); mNativeObject = 0; } @@ -327,7 +327,7 @@ void setPaymentHandlerHost(long nativeJniPaymentApp, PaymentHandlerHost paymentHandlerHost); - void freeNativeObject(long nativeJniPaymentApp); + void freeNativeObjectSoon(long nativeJniPaymentApp); byte[] setAppSpecificResponseFields(long nativeJniPaymentApp, ByteBuffer paymentResponse); } diff --git a/components/payments/content/android/jni_payment_app.cc b/components/payments/content/android/jni_payment_app.cc index ef9e53e..1e79ea5 100644 --- a/components/payments/content/android/jni_payment_app.cc +++ b/components/payments/content/android/jni_payment_app.cc @@ -15,6 +15,7 @@ #include "components/payments/content/android/payment_handler_host.h" #include "components/payments/content/payment_request_converter.h" #include "components/payments/core/payment_method_data.h" +#include "content/public/browser/browser_thread.h" #include "third_party/blink/public/mojom/payments/payment_request.mojom.h" #include "ui/gfx/android/java_bitmap.h" @@ -44,7 +45,7 @@ JNIEnv* env, std::unique_ptr<PaymentApp> payment_app) { // The |app| is owned by JniPaymentApp.java and will be destroyed through a - // JniPaymentApp::FreeNativeObject() call. + // JniPaymentApp::FreeNativeObjectSoon() call. JniPaymentApp* app = new JniPaymentApp(std::move(payment_app)); return Java_JniPaymentApp_Constructor( @@ -183,8 +184,8 @@ env, mojom::PaymentResponse::Serialize(&result)); } -void JniPaymentApp::FreeNativeObject(JNIEnv* env) { - delete this; +void JniPaymentApp::FreeNativeObjectSoon(JNIEnv* env) { + content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, this); } void JniPaymentApp::OnInstrumentDetailsReady( @@ -239,7 +240,9 @@ } JniPaymentApp::JniPaymentApp(std::unique_ptr<PaymentApp> payment_app) - : payment_app_(std::move(payment_app)) {} + : payment_app_(std::move(payment_app)) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); +} JniPaymentApp::~JniPaymentApp() = default; diff --git a/components/payments/content/android/jni_payment_app.h b/components/payments/content/android/jni_payment_app.h index 66ed590..01ef7b5b 100644 --- a/components/payments/content/android/jni_payment_app.h +++ b/components/payments/content/android/jni_payment_app.h @@ -73,7 +73,9 @@ JNIEnv* env, const base::android::JavaRef<jobject>& jpayment_response); - void FreeNativeObject(JNIEnv* env); + void FreeNativeObjectSoon(JNIEnv* env); + + ~JniPaymentApp() override; private: // PaymentApp::Delegate implementation: @@ -84,7 +86,6 @@ const std::string& error_message) override; explicit JniPaymentApp(std::unique_ptr<PaymentApp> payment_app); - ~JniPaymentApp() override; std::unique_ptr<PaymentApp> payment_app_; base::android::ScopedJavaGlobalRef<jobject> invoke_callback_;
Original Bug Report
Potential Browser heap corruption via off-thread PaymentApp destruction
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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: During payment app deduplication, Java JniPaymentApp references are dropped without explicitly cleaning up their native C++ counterparts. When Android’s Garbage Collector later finalizes these objects on a background thread, it synchronously destroys UI-thread-affine C++ objects like SecurePaymentConfirmationApp. This triggers off-thread modification of the WebContents observer list, causing concurrent memory mutation and potential heap corruption in the privileged Browser process.
Affected files:
components/payments/content/android/java/src/org/chromium/components/payments/PaymentAppService.javacomponents/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.javacomponents/payments/content/secure_payment_confirmation_app.hcomponents/payments/content/service_worker_payment_app.hcomponents/webauthn/content/browser/internal_authenticator_impl.h
Estimated timestamp from git blame: 2024-10-01
Summary
There is a potential thread-safety vulnerability in the Android PaymentApp implementation. The Java-side PaymentAppService drops references to native-backed JniPaymentApp objects without calling their explicit cleanup method (dismissInstrument()). Relying on the Java Garbage Collector to finalize these objects results in C++ destructors executing on the Android FinalizerDaemon thread instead of the Browser UI thread. Because these C++ objects observe WebContents, destroying them off-thread leads to a data race in base::ObserverList, allowing an attacker to corrupt the heap in the Browser process.
Vulnerability Details
- In
PaymentAppService.java, thededuplicatePaymentAppsmethod filters out redundant payment apps (for example, hiding a Service Worker payment app if a preferred native Android app is present). - When an app is filtered out, it is removed from the local collections (e.g.,
uniquePaymentApps) and the temporarymPossiblyDuplicatePaymentAppslist is cleared. The references are dropped without invokingdismissInstrument(). - The unreferenced
JniPaymentAppinstances become eligible for Java Garbage Collection. - When the GC runs, the Android
FinalizerDaemonthread executesJniPaymentApp.finalize(), which callsdismissInstrument()and crosses the JNI boundary to invokeJniPaymentApp::FreeNativeObject(env). FreeNativeObjectexecutesdelete this;, which destroys the underlying C++PaymentAppobject (such asSecurePaymentConfirmationApporServiceWorkerPaymentApp) entirely on the backgroundFinalizerDaemonthread.SecurePaymentConfirmationAppinherits fromcontent::WebContentsObserver. Its destructor implicitly calls~WebContentsObserver(), which invokesWebContentsImpl::RemoveObserver(this).WebContentsImpl::RemoveObservercallsRemoveObserveron itsbase::ObserverList.- If
base::ObserverListcurrently has no active iterators (live_iterators_.empty()), it bypasses its internal sequence checker and directly callsobservers_.erase(it)on its underlyingstd::vector. - This concurrent
std::vector::erase()execution from a background thread races against the Browser UI thread, which frequently reads, iterates, and mutates the sameWebContentsobserver list.
Because std::vector is not thread-safe, this data race causes use-after-free or memory corruption inside the highly-privileged Browser process, potentially leading to a Sandbox Escape and Remote Code Execution (RCE).
Potential Reproduction Steps
Note: Our tooling agent cannot run code yet, so these steps are theoretical based on code analysis.
- An attacker hosts a malicious website that registers a Service Worker payment handler.
- The Service Worker’s web-app manifest includes
"prefer_related_applications": trueand specifies a widely installed Android native payment app as a related application. - The attacker’s site invokes a
PaymentRequestwith methods that match both the Service Worker and the Android native app. - During the payment app discovery phase, both apps are instantiated in C++ and Java.
PaymentAppService.deduplicatePaymentAppsdrops the Java reference to the Service Worker app in favor of the native app.- The attacker uses memory-intensive JavaScript to apply memory pressure, forcing an Android Garbage Collection.
- The
FinalizerDaemontriggers the C++ destructor off-thread. Concurrent UI actions (like navigating theWebContentsor registering a new observer) cause a data race in theObserverList, crashing or corrupting the Browser process.
Suggested Fix
- Explicit Cleanup: Update
PaymentAppService.javato explicitly calldismissInstrument()on anyPaymentAppinstances that are discarded during deduplication or when aPaymentRequestis aborted/closed, ensuring resources are freed deterministically on the UI thread. - Thread Enforcement: Update
JniPaymentApp::FreeNativeObjectto ensure the C++ object is always deleted on the UI thread, regardless of which thread initiates it. For example, usecontent::GetUIThreadTaskRunner({})->DeleteSoon(...)instead of a directdelete this;.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
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.