CVE-2026-8520
Overview
Background
- `JniPaymentApp`
- a C++ bridge object (
components/payments/content/android/jni_payment_app.cc) that wraps aPaymentAppand is owned from the Java side via a raw native pointer. - UI thread / `BrowserThread::UI`
- the single thread on which UI-pinned browser components must be created and destroyed; touching them from another thread is a thread-safety violation.
- `DeleteSoon`
- a
content::GetUIThreadTaskRunnerhelper that posts an object’s destruction to run later on the UI thread, instead of deleting inline on the calling thread.
Root Cause Analysis
JniPaymentApp is constructed on the UI thread, but its destruction was driven from Java: JniPaymentApp.dismissInstrument() called the native freeNativeObject, whose implementation JniPaymentApp::FreeNativeObject ran delete this inline on whatever thread invoked the JNI call. Because that Java-side trigger was not guaranteed to run on the UI thread, the object (which interacts with UI-pinned components) could be destroyed off-thread, racing against UI-thread use of the same components.
The fix renames the entry point to FreeNativeObjectSoon and replaces the inline delete this with content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, this), so destruction is always marshaled to the UI thread. A DCHECK_CURRENTLY_ON(content::BrowserThread::UI) is also added to the constructor to document and enforce the create-on-UI-thread invariant. The Java interface and the .h declaration are updated to match the new method name, and the destructor is made public so DeleteSoon can invoke it.
Attack Path
- Reach the payments flow
Trigger a Web Payments request on Android so a
JniPaymentAppis created on the UI thread to represent a payment instrument. - Drive Java-side dismissal
Cause the Java
JniPaymentApp.dismissInstrument()path to fire, which calls into nativefreeNativeObjectfrom a thread that is not guaranteed to be the UI thread. - Off-thread destruction races UI use
The old
FreeNativeObjectrunsdelete thisinline, so the object’s teardown races concurrent UI-thread access to the UI-pinned components it interacts with (inferred as the exploitable window; the diff shows only the thread-unsafe deletion, not the specific concurrent access). - Memory corruption
The race can leave UI components referencing freed state, a use-after-free condition (inferred) that the deferred
DeleteSoondestruction closes.
Impact Assessment
(inferred).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
Audit Directions
- Create/destroy thread symmetryAudit objects created on the UI thread but freed from Java/JNI callbacks; ensure destruction uses
DeleteSoonon the owning thread rather than inlinedelete. - JNI-driven native deletionReview
freeNativeObject-style native methods invoked from Java for implicit assumptions about which thread the JNI call arrives on. - UI-pinned component ownershipLook for native objects that hold or touch UI-pinned components whose lifetime is controlled from a different thread, a common source of teardown races.
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.