CVE-2026-6319
Overview
Background
- `PaymentsWindowBridge` (Java)
- the Android/Clank Java object that drives the BNPL (Buy Now Pay Later) ephemeral payment tab and holds a
long mNativePaymentsWindowBridgepointing at its C++ counterpart. - `PaymentsWindowBridge` (C++)
- the native object whose address is stored in the Java field; it owns a reference to the Java object via
java_payments_window_bridge_. - JNI native-pointer pattern
- Chromium passes a raw C++
thispointer into Java as an opaquelong, and Java passes it back on each@CalledByNative/...Jni.get()call so the generated JNI glue canreinterpret_castit back to the C++ object. - `@CalledByNative` / `onNativeDestroyed`
- the newly added Java method the C++ destructor invokes to notify Java that the native object is gone.
Root Cause Analysis
The Java PaymentsWindowBridge stored its native counterpart in a final long mNativePaymentsWindowBridge, so once set it could never be invalidated. The C++ PaymentsWindowBridge::~PaymentsWindowBridge() was defaulted and did not tell Java it had been destroyed, so the Java object retained a stale native pointer after the C++ object was freed. Java methods such as onNavigationFinished, onWebContentsObservationStarted, and onWebContentsDestroyed forwarded that pointer straight into JNI (PaymentsWindowBridgeJni.get().…(mNativePaymentsWindowBridge, …)), which reinterpret_casts and dereferences it — a use-after-free if the native object was already gone.
The fix makes the field non-final, adds @CalledByNative onNativeDestroyed() that zeroes it, has the C++ destructor call Java_PaymentsWindowBridge_onNativeDestroyed (guarded by if (java_payments_window_bridge_)), and adds if (mNativePaymentsWindowBridge == 0) return; to each JNI-forwarding method.
The restored invariant: no JNI call is made on a native pointer that no longer references a live object.
final, never nulled on native destruction), so Java could call into a freed C++ object; the fix adds a destruction notification that zeroes the pointer and a zero-check gate before every JNI call.Attack Path
- Open a BNPL ephemeral payment tab
Reach the flow that constructs both the Java
PaymentsWindowBridgeand its C++ peer, establishing the storedmNativePaymentsWindowBridge. - Destroy the native object while Java survives
Cause the C++
PaymentsWindowBridgeto be destroyed (e.g. the payment window/native lifecycle ends) while the Java object is still referenced and able to receive callbacks. - Trigger a Java->native callback
Drive an event that invokes
onNavigationFinished,onWebContentsObservationStarted, oronWebContentsDestroyed, which (pre-patch) forward the stale pointer through JNI. - Use-after-free in native code
The JNI glue
reinterpret_casts and dereferences the freedmNativePaymentsWindowBridge, operating on released memory; the diff shows the dangling-pointer call but no further primitive, so any escalation is(inferred).
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
PaymentsWindowBridgechrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridge.java |
modified | |
ifchrome/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.javachrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.javachrome/browser/ui/android/autofill/payments/payments_window_bridge.cc
Audit Directions
- Java-held native pointersAudit Android bridge classes storing a native
long; anyfinalnative-pointer field with noonNativeDestroyed-style invalidation is a UAF candidate. - Native destructors that skip Java notificationReview C++
~Destructors of objects referenced by Java to ensure they signal the Java peer before freeing, especially defaulted (= default) destructors. - Unguarded JNI forwarding methodsCheck that every Java method forwarding a native pointer into
...Jni.get()first verifies the pointer is non-zero.
Patch
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,
Original Bug Report
Use-After-Free via dangling JNI native pointer in PaymentsWindowBridge::OnWebContentsDestroyed
Steps to reproduce the problem
Continued from https://issues.chromium.org/issues/498289905.
- Checkout and apply the patch:
git checkout 14e903b7ea5de658e62e89cd56ca758a5a0c2428
git apply patch3.diff
- 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
- Install on a device (connect an ARM64 Android device via adb):
out/Default/bin/chrome_public_apk install
- Start the MITM proxy on the host machine:
pip install mitmproxy
mitmproxy --mode regular --listen-port 8080 -s mitm_bnpl.py
- Host the PoC files (
poc.html,poc_checkout.html,bnpl_page.html):
python3 -m http.server 8888
- 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'
-
Add a test credit card in Chrome Settings > Payment methods (Name: Test User, Number: 4111 1111 1111 1111, Expiry: 12/30).
-
Navigate to
http://<host>:8888/poc.htmland click “Start PoC”. -
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.
-
After the ephemeral tab (bottom sheet) appears, the parent page closes the merchant tab in 10 seconds. The browser process crashes with SIGSEGV at
0xcdcdcdcdcdcdcdcdinPaymentsWindowBridge::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.
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 \
- https://crbug.com/355139788
- 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
- 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
- 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
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc;l=30;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/android/autofill/payments/payments_window_bridge.cc;l=66;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/autofill/payments/android_payments_window_manager.h;l=65;drc=14e903b7ea5de658e62e89cd56ca758a5a0c2428