Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Payments
DescriptionUse after free in Payments
ComponentPayments
Bug ClassUAF
Tracker499018889
Fix commitdd2818b02583 (chromium/src) +31/-2
CISA KEVNot listed
Creditedpwn2addr
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
PaymentsWindowBridge
chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java
modified
if
chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
modified

Files Changed

  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java
  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java
  • chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
From dd2818b02583be0a2569e2a53997f08a7b48dc87 Mon Sep 17 00:00:00 2001
From: viplavkadam <viplavkadam@google.com>
Date: Tue, 07 Apr 2026 09:51:43 -0700
Subject: [PATCH] [BNPL][Clank] PaymentWindowBridge native pointer fix for Use-After-free

Making the native pointer in Java non-final and setting it zero when
the C++ object is destroyed. Guarding all JNI calls from Java to C++
with a zero-pointer check

Bug: 499018889
Change-Id: I5a2956160d15236dc96598ae132339a1eeb2d3ba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7731228
Reviewed-by: Timofey Chudakov <tchudakov@google.com>
Reviewed-by: Vinny Persky <vinnypersky@google.com>
Commit-Queue: Viplav Kadam <viplavkadam@google.com>
Cr-Commit-Position: refs/heads/main@{#1610834}
---

diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java
index 818265c..2db1435 100644
--- a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java
+++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java
@@ -16,7 +16,7 @@
 @JNINamespace("autofill::payments")
 @NullMarked
 class PaymentsWindowBridge {
-    private final long mNativePaymentsWindowBridge;
+    private long mNativePaymentsWindowBridge;
     private PaymentsWindowCoordinator mPaymentsWindowCoordinator;
 
     @CalledByNative
@@ -35,6 +35,11 @@
         mPaymentsWindowCoordinator.closeEphemeralTab();
     }
 
+    @CalledByNative
+    public void onNativeDestroyed() {
+        mNativePaymentsWindowBridge = 0;
+    }
+
     PaymentsWindowCoordinator getPaymentsWindowCoordinatorForTesting() {
         return mPaymentsWindowCoordinator;
     }
@@ -50,6 +55,7 @@
      * @param clickedUrl The URL that the user initiated the navigation to.
      */
     void onNavigationFinished(GURL clickedUrl) {
+        if (mNativePaymentsWindowBridge == 0) return;
         PaymentsWindowBridgeJni.get().onNavigationFinished(mNativePaymentsWindowBridge, clickedUrl);
     }
 
@@ -59,6 +65,7 @@
      * @param webContents The WebContents that is being observed.
      */
     public void onWebContentsObservationStarted(WebContents webContents) {
+        if (mNativePaymentsWindowBridge == 0) return;
         PaymentsWindowBridgeJni.get()
                 .onWebContentsObservationStarted(mNativePaymentsWindowBridge, webContents);
     }
@@ -70,6 +77,7 @@
      * imminently destroyed.
      */
     void onWebContentsDestroyed() {
+        if (mNativePaymentsWindowBridge == 0) return;
         PaymentsWindowBridgeJni.get().onWebContentsDestroyed(mNativePaymentsWindowBridge);
     }
 
diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java
index a58457d..2355101 100644
--- a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java
+++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java
@@ -6,6 +6,7 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -81,4 +82,19 @@
 
         verify(mNativeMock).onWebContentsDestroyed(AUTOFILL_PAYMENTS_WINDOW_BRIDGE_NATIVE_POINTER);
     }
+
+    @Test
+    public void testNativeMethodsNotCalledAfterNativeDestroyed() {
+        PaymentsWindowBridgeJni.setInstanceForTesting(mNativeMock);
+
+        // Simulate native object destruction.
+        mPaymentsWindowBridge.onNativeDestroyed();
+
+        // Native method calls.
+        mPaymentsWindowBridge.onNavigationFinished(ISSUER_URL);
+        mPaymentsWindowBridge.onWebContentsObservationStarted(mMerchantWebContents);
+        mPaymentsWindowBridge.onWebContentsDestroyed();
+
+        verifyNoInteractions(mNativeMock);
+    }
 }
diff --git a/chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc b/chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
index f10e3000..d18a6bf 100644
--- a/chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
+++ b/chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
@@ -27,7 +27,12 @@
       base::android::AttachCurrentThread(), reinterpret_cast<int64_t>(this));
 }
 
-PaymentsWindowBridge::~PaymentsWindowBridge() = default;
+PaymentsWindowBridge::~PaymentsWindowBridge() {
+  if (java_payments_window_bridge_) {
+    Java_PaymentsWindowBridge_onNativeDestroyed(
+        base::android::AttachCurrentThread(), java_payments_window_bridge_);
+  }
+}
 
 void PaymentsWindowBridge::OpenEphemeralTab(
     const GURL& url,
Loading diff…

Original Bug Report

reported by pw...@gmail.com

Use-After-Free via dangling JNI native pointer in PaymentsWindowBridge::OnWebContentsDestroyed

Steps to reproduce the problem

Continued from https://issues.chromium.org/issues/498289905.

  1. Checkout and apply the patch:
git checkout 14e903b7ea5de658e62e89cd56ca758a5a0c2428
git apply patch3.diff
  1. Build Chromium for Android:
gn gen out/Default --args='target_os="android" target_cpu="arm64" is_debug=false use_siso=false android_static_analysis="off"'
autoninja -C out/Default chrome_public_apk
  1. Install on a device (connect an ARM64 Android device via adb):
out/Default/bin/chrome_public_apk install
  1. Start the MITM proxy on the host machine:
pip install mitmproxy
mitmproxy --mode regular --listen-port 8080 -s mitm_bnpl.py
  1. Host the PoC files (poc.html, poc_checkout.html, bnpl_page.html):
python3 -m http.server 8888
  1. Launch Chrome:
out/Default/bin/chrome_public_apk run \
  --args='--enable-features=AutofillEnableBuyNowPayLater,AutofillEnableAmountExtraction,AutofillEnableAmountExtractionTesting,AutofillDisableBnplCountryCheckForTesting,AutofillEnableBuyNowPayLaterSyncing --proxy-server=http://<host>:8080 --ignore-certificate-errors'
  1. Add a test credit card in Chrome Settings > Payment methods (Name: Test User, Number: 4111 1111 1111 1111, Expiry: 12/30).

  2. Navigate to http://<host>:8888/poc.html and click “Start PoC”.

  3. In the new tab, tap the Card Number field, select the saved card from the autofill bar, and then select “Affirm” from the Pay Later options.

  4. After the ephemeral tab (bottom sheet) appears, the parent page closes the merchant tab in 10 seconds. The browser process crashes with SIGSEGV at 0xcdcdcdcdcdcdcdcd in PaymentsWindowBridge::OnWebContentsDestroyed.

Problem Description

PaymentsWindowBridge.java stores the C++ PaymentsWindowBridge pointer as a final long field:

// PaymentsWindowBridge  PaymentsWindowBridge.java:19
class PaymentsWindowBridge {
    private final long mNativePaymentsWindowBridge;    // [1]
    ...
    void onWebContentsDestroyed() {
        PaymentsWindowBridgeJni.get().onWebContentsDestroyed(mNativePaymentsWindowBridge);  // [5]
    }
}

The pointer is set once in the constructor [1] and never cleared. The final keyword makes it impossible to set it to zero.

The PaymentsWindowCoordinator registers itself as an EphemeralTabObserver on the Activity-scoped EphemeralTabCoordinator:

// PaymentsWindowCoordinator.openEphemeralTab  PaymentsWindowCoordinator.java:47
void openEphemeralTab(GURL url, String title, WebContents merchantWebContents) {
    ...
    WindowAndroid windowAndroid = merchantWebContents.getTopLevelNativeWindow();
    ...
    mEphemeralTabCoordinator = supplier.get();
    mEphemeralTabCoordinator.addObserver(this);   // [2]
    ...
}

The EphemeralTabCoordinator is retrieved from WindowAndroid’s unowned user data [2], making it Activity-scoped – it survives individual tab closures.

The C++ ownership chain is:

WebContents (merchant tab)
  -> ContentAutofillClient (WebContentsUserData)
    -> ChromeAutofillClient (extends ContentAutofillClient)
      -> ChromePaymentsAutofillClient (member)
        -> AndroidPaymentsWindowManager (unique_ptr)
          -> PaymentsWindowBridge (unique_ptr)

AndroidPaymentsWindowManager owns PaymentsWindowBridge as a unique_ptr:

// AndroidPaymentsWindowManager  android_payments_window_manager.h:65
class AndroidPaymentsWindowManager : public PaymentsWindowManager,
                                     public PaymentsWindowDelegate {
  ...
  std::unique_ptr<PaymentsWindowBridge> payments_window_bridge_;   // [3]
};

The C++ destructor is defaulted and does not notify the Java side:

// PaymentsWindowBridge::~PaymentsWindowBridge  payments_window_bridge.cc:30
PaymentsWindowBridge::~PaymentsWindowBridge() = default;   // [4]

When the merchant tab is closed, ~WebContentsImpl destroys WebContentsUserData entries, which chains through ~ChromeAutofillClient -> ~ChromePaymentsAutofillClient -> ~AndroidPaymentsWindowManager -> ~PaymentsWindowBridge [3][4]. The defaulted destructor [4] does not call into Java to null out mNativePaymentsWindowBridge, nor does it remove the PaymentsWindowCoordinator from the EphemeralTabCoordinator’s observer list.

Subsequently, when the ephemeral tab’s WebContents is cleaned up, PaymentsWindowCoordinator.onWebContentsDestroyed() fires through the observer registered at [2], which calls PaymentsWindowBridge.onWebContentsDestroyed() [5], passing the dangling mNativePaymentsWindowBridge to JNI. The C++ side then dereferences payments_window_delegate_ on the freed object:

// PaymentsWindowBridge::OnWebContentsDestroyed  payments_window_bridge.cc:66-68
void PaymentsWindowBridge::OnWebContentsDestroyed(JNIEnv* env) {
  payments_window_delegate_->WebContentsDestroyed();   // [6] UAF: `this` is freed
}

The crash occurs at [6] because this was freed at [4], and payments_window_delegate_ (a raw_ref) now contains 0xcdcdcdcdcdcdcdcd.

The same pattern was previously found and fixed in FacilitatedPaymentsPaymentMethodsControllerBridge (crbug.com/355139788). That fix made the native pointer non-final, added an onNativeDestroyed() callback to zero it out, and guarded every JNI callback with a null check. None of these protections were applied to PaymentsWindowBridge.

[1] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java;l=19;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

[2] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowCoordinator.java;l=56;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

[3] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/autofill/payments/android_payments_window_manager.h;l=65;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

[4] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc;l=30;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

[5] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java;l=73;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

[6] https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc;l=66;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428

Additional Comments

If a signed-in Chrome instance with a US-based Google account, a linked BNPL issuer (e.g., Affirm), and a BNPL-eligible merchant page is available, this bug can be triggered from poc.html with no patches. I don’t have such account, so I used patch to reproduce. If you still think this patch inappropriate, please let me know in the comments.

Summary

Use-After-Free via dangling JNI native pointer in PaymentsWindowBridge::OnWebContentsDestroyed

Custom Questions

Type of crash:

browser

Crash state:

Please see the attached tombstone.txt

Reporter credit:

pwn2addr

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker