Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Payments
DescriptionRace in Payments
ComponentPayments
Bug ClassRace
Tracker503619813
Fix commit133845186794 (chromium/src) +12/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Files Changed

  • components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java
  • components/payments/content/android/jni_payment_app.cc
  • components/payments/content/android/jni_payment_app.h
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_;
Loading diff…

Original Bug Report

reported by [email protected]

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.java
  • components/payments/content/android/java/src/org/chromium/components/payments/JniPaymentApp.java
  • components/payments/content/secure_payment_confirmation_app.h
  • components/payments/content/service_worker_payment_app.h
  • components/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

  1. In PaymentAppService.java, the deduplicatePaymentApps method filters out redundant payment apps (for example, hiding a Service Worker payment app if a preferred native Android app is present).
  2. When an app is filtered out, it is removed from the local collections (e.g., uniquePaymentApps) and the temporary mPossiblyDuplicatePaymentApps list is cleared. The references are dropped without invoking dismissInstrument().
  3. The unreferenced JniPaymentApp instances become eligible for Java Garbage Collection.
  4. When the GC runs, the Android FinalizerDaemon thread executes JniPaymentApp.finalize(), which calls dismissInstrument() and crosses the JNI boundary to invoke JniPaymentApp::FreeNativeObject(env).
  5. FreeNativeObject executes delete this;, which destroys the underlying C++ PaymentApp object (such as SecurePaymentConfirmationApp or ServiceWorkerPaymentApp) entirely on the background FinalizerDaemon thread.
  6. SecurePaymentConfirmationApp inherits from content::WebContentsObserver. Its destructor implicitly calls ~WebContentsObserver(), which invokes WebContentsImpl::RemoveObserver(this).
  7. WebContentsImpl::RemoveObserver calls RemoveObserver on its base::ObserverList.
  8. If base::ObserverList currently has no active iterators (live_iterators_.empty()), it bypasses its internal sequence checker and directly calls observers_.erase(it) on its underlying std::vector.
  9. This concurrent std::vector::erase() execution from a background thread races against the Browser UI thread, which frequently reads, iterates, and mutates the same WebContents observer 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.

  1. An attacker hosts a malicious website that registers a Service Worker payment handler.
  2. The Service Worker’s web-app manifest includes "prefer_related_applications": true and specifies a widely installed Android native payment app as a related application.
  3. The attacker’s site invokes a PaymentRequest with methods that match both the Service Worker and the Android native app.
  4. During the payment app discovery phase, both apps are instantiated in C++ and Java.
  5. PaymentAppService.deduplicatePaymentApps drops the Java reference to the Service Worker app in favor of the native app.
  6. The attacker uses memory-intensive JavaScript to apply memory pressure, forcing an Android Garbage Collection.
  7. The FinalizerDaemon triggers the C++ destructor off-thread. Concurrent UI actions (like navigating the WebContents or registering a new observer) cause a data race in the ObserverList, crashing or corrupting the Browser process.

Suggested Fix

  1. Explicit Cleanup: Update PaymentAppService.java to explicitly call dismissInstrument() on any PaymentApp instances that are discarded during deduplication or when a PaymentRequest is aborted/closed, ensuring resources are freed deterministically on the UI thread.
  2. Thread Enforcement: Update JniPaymentApp::FreeNativeObject to ensure the C++ object is always deleted on the UI thread, regardless of which thread initiates it. For example, use content::GetUIThreadTaskRunner({})->DeleteSoon(...) instead of a direct delete 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.

View on issue tracker