Critical watch CVSS 8.3 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Payments
DescriptionRace in Payments
ComponentPayments
Sub-componentPayments
Bug ClassRace
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker503619813
Fix commit133845186794 (chromium/src) +12/-8
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-17 (per vendor advisory)
Fix landed2026-04-20 (public git)
Disclosed2026-05-12
Patch gap22 days — fix public in source before the advisory

Background

`JniPaymentApp`
a C++ bridge object (components/payments/content/android/jni_payment_app.cc) that wraps a PaymentApp and 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::GetUIThreadTaskRunner helper 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.

Key insight
The single mistake was destroying a UI-thread-owned object inline on an arbitrary JNI-caller thread; the fix defers destruction to the UI thread’s task runner so the create-thread and destroy-thread invariant holds.

Attack Path

  1. Reach the payments flow Trigger a Web Payments request on Android so a JniPaymentApp is created on the UI thread to represent a payment instrument.
  2. Drive Java-side dismissal Cause the Java JniPaymentApp.dismissInstrument() path to fire, which calls into native freeNativeObject from a thread that is not guaranteed to be the UI thread.
  3. Off-thread destruction races UI use The old FreeNativeObject runs delete this inline, 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).
  4. Memory corruption The race can leave UI components referencing freed state, a use-after-free condition (inferred) that the deferred DeleteSoon destruction closes.

Impact Assessment

An attacker exploiting the race could trigger off-UI-thread destruction of a UI-pinned payments object, producing a thread-safety violation and potential use-after-free (inferred) in the browser process. The vendor advisory rates this critical and classifies it as a race in Payments; it is Android-specific and requires exercising the Web Payments instrument-dismissal flow. The diff demonstrates the unsafe cross-thread deletion but does not itself demonstrate any further escalation such as code execution, which is (inferred).

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

Audit Directions

  • Create/destroy thread symmetry
    Audit objects created on the UI thread but freed from Java/JNI callbacks; ensure destruction uses DeleteSoon on the owning thread rather than inline delete.
  • JNI-driven native deletion
    Review freeNativeObject-style native methods invoked from Java for implicit assumptions about which thread the JNI call arrives on.
  • UI-pinned component ownership
    Look for native objects that hold or touch UI-pinned components whose lifetime is controlled from a different thread, a common source of teardown races.
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