CVE-2026-15113
Overview
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.javachrome/android/junit/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridgeTest.java
Patch
From 2a4a6bfdb0f89d2da863599e637e6dc1ed7e78df Mon Sep 17 00:00:00 2001
From: Luis Antunes <luisantunes@google.com>
Date: Fri, 03 Jul 2026 08:13:20 -0700
Subject: [PATCH] [Autofill] Clear native pointer in name fix-flow bridge
AutofillNameFixFlowBridge held its native pointer in a final field, so
it could not be cleared once PromptDismissed deleted the C++
counterpart. A late delegate callback (e.g. via the soft-keyboard Done
action that calls onClick directly) would then forward a stale pointer
to JNI.
Mirror AutofillExpirationDateFixFlowBridge / CardUnmaskBridge: drop
final, zero the pointer in onPromptDismissed and skip
onUserAcceptCardholderName / onUserDismiss / onPromptDismissed when it
is 0. Relax the constructor to @VisibleForTesting package visibility and
add Robolectric coverage.
Fixed: 520540744
Change-Id: I8d4fc04946ecc8f798f72213a4c83c57f41fddbf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8029643
Commit-Queue: Luis Antunes <luisantunes@google.com>
Reviewed-by: Luchen Peng <luchenpeng@google.com>
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656587}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java
index 8c8ae3f..95db3046 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java
@@ -8,6 +8,8 @@
import android.app.Activity;
+import androidx.annotation.VisibleForTesting;
+
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
@@ -25,7 +27,7 @@
@JNINamespace("autofill")
@NullMarked
final class AutofillNameFixFlowBridge implements AutofillNameFixFlowPromptDelegate {
- private final long mNativeCardNameFixFlowViewAndroid;
+ private long mNativeCardNameFixFlowViewAndroid;
private final @Nullable Activity mActivity;
private final String mTitle;
private final String mInferredName;
@@ -33,7 +35,8 @@
private final int mIconId;
private @Nullable AutofillNameFixFlowPrompt mNameFixFlowPrompt;
- private AutofillNameFixFlowBridge(
+ @VisibleForTesting
+ AutofillNameFixFlowBridge(
long nativeCardNameFixFlowViewAndroid,
String title,
String inferredName,
@@ -74,16 +77,25 @@
@Override
public void onPromptDismissed() {
- AutofillNameFixFlowBridgeJni.get().promptDismissed(mNativeCardNameFixFlowViewAndroid);
+ if (mNativeCardNameFixFlowViewAndroid == 0) return;
+ long nativePtr = mNativeCardNameFixFlowViewAndroid;
+ // The native pointer is zeroed out here before calling promptDismissed to ensure
+ // that any subsequent UI events triggered during the dismissal flow (like focus
+ // changes or text watcher events) are dropped instead of attempting to call
+ // JNI methods on a dangling pointer.
+ mNativeCardNameFixFlowViewAndroid = 0;
+ AutofillNameFixFlowBridgeJni.get().promptDismissed(nativePtr);
}
@Override
public void onUserDismiss() {
+ if (mNativeCardNameFixFlowViewAndroid == 0) return;
AutofillNameFixFlowBridgeJni.get().onUserDismiss(mNativeCardNameFixFlowViewAndroid);
}
@Override
public void onUserAcceptCardholderName(String name) {
+ if (mNativeCardNameFixFlowViewAndroid == 0) return;
AutofillNameFixFlowBridgeJni.get().onUserAccept(mNativeCardNameFixFlowViewAndroid, name);
}
diff --git a/chrome/android/junit/BUILD.gn b/chrome/android/junit/BUILD.gn
index 5f565ac4..57751957 100644
--- a/chrome/android/junit/BUILD.gn
+++ b/chrome/android/junit/BUILD.gn
@@ -506,6 +506,7 @@
"src/org/chromium/chrome/browser/autofill/AutofillFallbackSurfaceLauncherTest.java",
"src/org/chromium/chrome/browser/autofill/AutofillImageFetcherTest.java",
"src/org/chromium/chrome/browser/autofill/AutofillImageFetcherUtilsTest.java",
+ "src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridgeTest.java",
"src/org/chromium/chrome/browser/autofill/AutofillUiUtilsTest.java",
"src/org/chromium/chrome/browser/autofill/CardUnmaskBridgeTest.java",
"src/org/chromium/chrome/browser/autofill/GoogleWalletLauncherTest.java",
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridgeTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridgeTest.java
new file mode 100644
index 0000000..32c1c2a
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridgeTest.java
@@ -0,0 +1,82 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.autofill;
+
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.ui.base.WindowAndroid;
+
+/** Unit tests for {@link AutofillNameFixFlowBridge}. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class AutofillNameFixFlowBridgeTest {
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private AutofillNameFixFlowBridge.Natives mNativeMock;
+
+ private WindowAndroid mWindowAndroid;
+ private AutofillNameFixFlowBridge mBridge;
+ private static final long NATIVE_POINTER = 12345L;
+
+ @Before
+ public void setUp() {
+ AutofillNameFixFlowBridgeJni.setInstanceForTesting(mNativeMock);
+
+ Activity activity = Robolectric.buildActivity(Activity.class).create().get();
+ mWindowAndroid = new WindowAndroid(activity, false);
+
+ mBridge =
+ new AutofillNameFixFlowBridge(
+ NATIVE_POINTER,
+ /* title= */ "Title",
+ /* inferredName= */ "Inferred Name",
+ /* confirmButtonLabel= */ "Confirm",
+ /* iconId= */ 0,
+ mWindowAndroid);
+ }
+
+ @After
+ public void tearDown() {
+ mWindowAndroid.destroy();
+ }
+
+ @Test
+ public void testOnUserDismiss_CallsJni() {
+ mBridge.onUserDismiss();
+ verify(mNativeMock).onUserDismiss(NATIVE_POINTER);
+ }
+
+ @Test
+ public void testOnUserAcceptCardholderName_CallsJni() {
+ mBridge.onUserAcceptCardholderName("New Name");
+ verify(mNativeMock).onUserAccept(NATIVE_POINTER, "New Name");
+ }
+
+ @Test
+ public void testOnPromptDismissed_CallsJniAndClearsPointer() {
+ mBridge.onPromptDismissed();
+ verify(mNativeMock).promptDismissed(NATIVE_POINTER);
+
+ // Subsequent calls should not reach JNI.
+ mBridge.onUserDismiss();
+ mBridge.onUserAcceptCardholderName("New Name");
+ mBridge.onPromptDismissed();
+
+ // Verify no other calls were made to JNI after the first dismiss.
+ org.mockito.Mockito.verifyNoMoreInteractions(mNativeMock);
+ }
+}
Original Bug Report
Potential Browser Process UAF in CardNameFixFlowViewAndroid via Stale JNI Pointer
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability in the Android browser process occurs when CardNameFixFlowViewAndroid is deleted but its final JNI pointer remains in AutofillNameFixFlowBridge. A queued IME action or event on the detached EditText can invoke native methods on this stale pointer, potentially leading to control-flow hijacking. The vulnerability is caused by the JNI pointer being declared ‘final’ and never cleared upon prompt dismissal.
Affected files:
chrome/browser/ui/android/autofill/card_name_fix_flow_view_android.ccchrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.javachrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowPrompt.java
Estimated timestamp from git blame: 2018-11-08
Potential Use-After-Free in CardNameFixFlowViewAndroid via Stale JNI Pointer and IME Action Bypass
1. Code Defect Analysis
In chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java (line 28), the native pointer reference is declared as:
private final long mNativeCardNameFixFlowViewAndroid;
Because this field is marked final, it cannot be modified or zeroed out once initialized. When onPromptDismissed() (line 76-78) calls the native PromptDismissed() JNI method, the native object executes delete this (chrome/browser/ui/android/autofill/card_name_fix_flow_view_android.cc:37), freeing the native memory chunk. However, the Java AutofillNameFixFlowBridge instance remains alive on the Java heap and continues to hold the stale pointer.
In contrast, other similar JNI implementations such as AutofillExpirationDateFixFlowBridge.java (line 27) declare the native reference as mutable and correctly clear it upon dismissal:
private long mNativeCardExpirationDateFixFlowViewAndroid;
...
@Override
public void onPromptDismissed() {
AutofillExpirationDateFixFlowBridgeJni.get()
.promptDismissed(mNativeCardExpirationDateFixFlowViewAndroid);
mNativeCardExpirationDateFixFlowViewAndroid = 0;
}
2. Potential Teardown & Race Flow
When a tab containing a payment prompt is closed (e.g., via a compromised renderer), the following synchronous teardown chain is initiated:
~ChromePaymentsAutofillClient->~CardNameFixFlowControllerImpl()->MaybeDestroyCardNameFixFlowView(true)CardNameFixFlowViewAndroid::ControllerGone()sets itscontroller_pointer tonullptrand calls the Java bridge’sdismiss()method.- In Java,
mNameFixFlowPrompt.dismiss(DialogDismissalCause.DISMISSED_BY_NATIVE)executes. ModalDialogManager.dismissDialogtriggersonDismiss()on the prompt’s controller, which calls the bridge’sonPromptDismissed().- JNI
promptDismissed()is called withmNativeCardNameFixFlowViewAndroid. - Native
CardNameFixFlowViewAndroid::PromptDismissedexecutesdelete this, freeing the C++ object. AppModalPresenter.javadetaches themDialogViewandEditText.
Although the view is detached, Garbage Collection in Android is asynchronous, meaning the Java objects and listeners remain in memory. If an IME_ACTION_DONE event is already queued in the UI thread’s Looper behind the close-tab task, the editor action listener on the detached EditText can be executed:
mUserNameInput.setOnEditorActionListener(
(view, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
if (mUserNameInput.getText().toString().trim().length() != 0) {
onClick(mDialogModel, ModalDialogProperties.ButtonType.POSITIVE);
}
return true;
}
return false;
});
This listener calls onClick(mDialogModel, POSITIVE) directly, which bypasses the ModalDialogManager state checks and calls mDelegate.onUserAcceptCardholderName(...). This method immediately invokes JNI onUserAccept with the stale, non-zeroed mNativeCardNameFixFlowViewAndroid pointer:
@Override
public void onUserAcceptCardholderName(String name) {
AutofillNameFixFlowBridgeJni.get().onUserAccept(mNativeCardNameFixFlowViewAndroid, name);
}
3. Security Impact
On the native side, the JNI zero-generated wrapper performs a raw reinterpret_cast on the stale pointer and invokes OnUserAccept(). This method dereferences this->controller_ (chrome/browser/ui/android/autofill/card_name_fix_flow_view_android.cc:29):
void CardNameFixFlowViewAndroid::OnUserAccept(JNIEnv* env,
const std::u16string& name) {
controller_->OnNameAccepted(name);
}
If the freed memory of CardNameFixFlowViewAndroid has been reclaimed or sprayed in the browser process, controller_ will contain attacker-controlled bytes. Because CardNameFixFlowController is an abstract interface, calling OnNameAccepted triggers an indirect virtual function call based on a controlled vtable, leading to arbitrary Remote Code Execution (RCE) in the privileged, unsandboxed browser process (complete Sandbox Escape).
4. Potential Steps to Reproduce
Note: These are theoretical steps as our tooling lacks the capability to execute code.
- A compromised renderer opens a popup window and navigates it to an attacker page hosting a credit-card form lacking a cardholder-name field.
- The renderer auto-submits the form to trigger the upload-save offer dialog.
- The user interacts with the dialog, typing a cardholder name and tapping the IME ‘Done’ key (queues
IME_ACTION_DONEin the UI thread’s Looper). - The renderer immediately sends a
window.close()IPC, placing the tab teardown task in the queue ahead of the IME event. - Tab teardown executes, synchronously deleting the native
CardNameFixFlowViewAndroidobject. - The renderer sprays the browser-process heap to reclaim the freed view memory chunk.
- The Looper processes the queued
IME_ACTION_DONEevent, dispatching it to the detachedEditText. - The listener triggers the JNI call with the stale
finalpointer, dereferencing the sprayed memory and executing the hijacked virtual call.
5. Suggested Fix
Modify chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillNameFixFlowBridge.java to:
- Remove the
finalmodifier frommNativeCardNameFixFlowViewAndroid. - Clear the pointer to
0inside theonPromptDismissed()callback. - Ensure that all JNI invocations (
onUserDismiss,onUserAcceptCardholderName, etc.) perform a null/zero check onmNativeCardNameFixFlowViewAndroidbefore calling the JNI methods.
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.