Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Autofill
DescriptionUse after free in Autofill
ComponentAutofill
Bug ClassUAF
Tracker501561644
Fix commit6ce3ab538b82 (chromium/src) +101/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
OtpVerificationDialogBridge
chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
modified

Files Changed

  • chrome/browser/ui/android/autofill/internal/BUILD.gn
  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java
From 6ce3ab538b82d1a1c6f78fc589d5f94f660bb1a9 Mon Sep 17 00:00:00 2001
From: Matthias Koerber <[email protected]>
Date: Mon, 13 Apr 2026 05:47:07 -0700
Subject: [PATCH] Fix issue in OTP dialog

The native OtpVerificationDialogViewAndroid C++ object is destroyed
synchronously upon dismissal, but the Android UI remains interactive
during a 200ms fade-out animation. Interactions during this window
could invoke JNI methods using a stale pointer.

This CL fixes the issue by:
1. Clearing the native pointer in OtpVerificationDialogBridge when
   dismissal is notified.
2. Adding null checks before calling native methods in the bridge.

Bug: 501561644
Change-Id: I31e2607416056eae23aad6603409cf65e459886b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7754581
Reviewed-by: Piotr Kotynia <[email protected]>
Commit-Queue: Piotr Kotynia <[email protected]>
Auto-Submit: Matthias Körber <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1613646}
---

diff --git a/chrome/browser/ui/android/autofill/internal/BUILD.gn b/chrome/browser/ui/android/autofill/internal/BUILD.gn
index d826e92c..8e09362 100644
--- a/chrome/browser/ui/android/autofill/internal/BUILD.gn
+++ b/chrome/browser/ui/android/autofill/internal/BUILD.gn
@@ -68,6 +68,7 @@
     "java/src/org/chromium/chrome/browser/ui/autofill/AuthenticatorSelectionDialogTest.java",
     "java/src/org/chromium/chrome/browser/ui/autofill/AutofillErrorDialogBridgeTest.java",
     "java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridgeTest.java",
+    "java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java",
     "java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogTest.java",
     "java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java",
     "java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowCoordinatorTest.java",
diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
index 7b0d098f..724d741 100644
--- a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
+++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
@@ -22,7 +22,7 @@
 @JNINamespace("autofill")
 @NullMarked
 class OtpVerificationDialogBridge implements OtpVerificationDialogCoordinator.Delegate {
-    private final long mNativeOtpVerificationDialogView;
+    private long mNativeOtpVerificationDialogView;
     private final OtpVerificationDialogCoordinator mDialogCoordinator;
 
     OtpVerificationDialogBridge(
@@ -55,17 +55,21 @@
 
     @Override
     public void onConfirm(String otp) {
+        if (mNativeOtpVerificationDialogView == 0) return;
         OtpVerificationDialogBridgeJni.get().onConfirm(mNativeOtpVerificationDialogView, otp);
     }
 
     @Override
     public void onNewOtpRequested() {
+        if (mNativeOtpVerificationDialogView == 0) return;
         OtpVerificationDialogBridgeJni.get().onNewOtpRequested(mNativeOtpVerificationDialogView);
     }
 
     @Override
     public void onDialogDismissed() {
+        if (mNativeOtpVerificationDialogView == 0) return;
         OtpVerificationDialogBridgeJni.get().onDialogDismissed(mNativeOtpVerificationDialogView);
+        mNativeOtpVerificationDialogView = 0;
     }
 
     /**
diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java
new file mode 100644
index 0000000..2f17326
--- /dev/null
+++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java
@@ -0,0 +1,95 @@
+// 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.ui.autofill;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+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.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
+import org.chromium.ui.test.util.modaldialog.FakeModalDialogManager;
+
+/** Unit tests for {@link OtpVerificationDialogBridge}. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class OtpVerificationDialogBridgeTest {
+    private static final long NATIVE_OTP_VERIFICATION_DIALOG_VIEW = 100L;
+
+    private FakeModalDialogManager mModalDialogManager;
+    private OtpVerificationDialogBridge mOtpVerificationDialogBridge;
+    @Mock private OtpVerificationDialogBridge.Natives mNativeMock;
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Before
+    public void setUp() {
+        mModalDialogManager = new FakeModalDialogManager(ModalDialogType.TAB);
+        mOtpVerificationDialogBridge =
+                new OtpVerificationDialogBridge(
+                        NATIVE_OTP_VERIFICATION_DIALOG_VIEW,
+                        ApplicationProvider.getApplicationContext(),
+                        mModalDialogManager);
+        OtpVerificationDialogBridgeJni.setInstanceForTesting(mNativeMock);
+    }
+
+    @Test
+    @SmallTest
+    public void testOnConfirm_callsNative() {
+        mOtpVerificationDialogBridge.onConfirm("123456");
+
+        verify(mNativeMock, times(1)).onConfirm(NATIVE_OTP_VERIFICATION_DIALOG_VIEW, "123456");
+    }
+
+    @Test
+    @SmallTest
+    public void testOnNewOtpRequested_callsNative() {
+        mOtpVerificationDialogBridge.onNewOtpRequested();
+
+        verify(mNativeMock, times(1)).onNewOtpRequested(NATIVE_OTP_VERIFICATION_DIALOG_VIEW);
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDialogDismissed_callsNativeAndClearsPointer() {
+        mOtpVerificationDialogBridge.onDialogDismissed();
+
+        verify(mNativeMock, times(1)).onDialogDismissed(NATIVE_OTP_VERIFICATION_DIALOG_VIEW);
+
+        // Subsequent calls should not reach native.
+        mOtpVerificationDialogBridge.onConfirm("123456");
+        mOtpVerificationDialogBridge.onNewOtpRequested();
+        mOtpVerificationDialogBridge.onDialogDismissed();
+
+        verify(mNativeMock, never()).onConfirm(anyLong(), anyString());
+        verify(mNativeMock, never()).onNewOtpRequested(anyLong());
+        verify(mNativeMock, times(1)).onDialogDismissed(anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testNativeCallsDoNotOccurAfterDismissed() {
+        mOtpVerificationDialogBridge.onDialogDismissed();
+
+        verify(mNativeMock).onDialogDismissed(NATIVE_OTP_VERIFICATION_DIALOG_VIEW);
+
+        mOtpVerificationDialogBridge.onConfirm("123456");
+        mOtpVerificationDialogBridge.onNewOtpRequested();
+
+        verify(mNativeMock, never()).onConfirm(anyLong(), anyString());
+        verify(mNativeMock, never()).onNewOtpRequested(anyLong());
+    }
+}
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in OtpVerificationDialogViewAndroid during dismissal animation

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.

Overview: A potential Use-After-Free (UAF) exists in the Android OTP verification dialog. The native C++ object is destroyed synchronously upon dismissal, but the UI remains interactive during a 200ms fade-out animation. Interactions during this window invoke JNI methods using a stale pointer, potentially leading to Remote Code Execution in the browser process.

Affected files:

  • chrome/browser/ui/android/autofill/otp_verification_dialog_view_android.cc
  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java
  • chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogMediator.java
  • components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/TabModalPresenter.java
  • ui/android/java/src/org/chromium/ui/modaldialog/ModalDialogManager.java

Estimated timestamp from git blame: 2021-12-07

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in the OTP verification dialog for Autofill Payments on Chrome for Android. The issue stems from a lifecycle mismatch: the native OtpVerificationDialogViewAndroid C++ object deletes itself synchronously when the dialog is dismissed, but the corresponding Android UI remains visible and interactive during a 200ms exit animation.

Because the native pointer is stored as a primitive long in the Java bridge (OtpVerificationDialogBridge), it is not protected by MiraclePtr (BackupRefPtr). User or attacker-induced interaction with the UI during the animation window triggers JNI calls on the freed memory, which could potentially be exploited to achieve Remote Code Execution (RCE) in the highly privileged browser process.

Technical Details

  1. Dialog Initialization: When the OTP dialog is created, OtpVerificationDialogBridge (Java) receives a raw pointer to OtpVerificationDialogViewAndroid (C++) and stores it in a primitive long field named mNativeOtpVerificationDialogView.
  2. Synchronous Destruction: When the dialog is dismissed (e.g., via the Cancel button or programmatically), ModalDialogManager.dismissDialog() synchronously triggers the controller’s onDismiss callback. This propagates to the Java bridge, which calls the native JNI method onDialogDismissed. The C++ method OtpVerificationDialogViewAndroid::OnDialogDismissed executes delete this;, freeing the native object’s memory.
  3. Asynchronous UI Teardown: Immediately after notifying the controller, ModalDialogManager instructs TabModalPresenter to remove the view. Because the view is attached, TabModalPresenter initiates a 200ms alpha fade-out animation (runExitAnimation). During this 200ms window, the view is not hidden, and its click listeners (such as the ChromeClickableSpan for the “Get new code” link) are not disabled.
  4. Triggering the UAF: If an interaction occurs on the fading UI during this 200ms window (e.g., tapping “Get new code”), the Java bridge blindly passes the dangling mNativeOtpVerificationDialogView pointer across JNI to OtpVerificationDialogViewAndroid::OnNewOtpRequested.
  5. Exploitation Path: Because the pointer crosses JNI as a primitive integer, MiraclePtr does not track it. An attacker capable of spraying the browser process heap (e.g., from a compromised renderer) could predictably reclaim the freed memory during the 200ms window. The C++ code evaluates if (controller_) where controller_ is a base::WeakPtr. The attacker can forge the WeakPtr and its internal Flag object to pass the validity check. The subsequent call to the pure virtual method controller_->OnNewCodeLinkClicked() reads a fake vtable from the attacker-controlled memory, leading to potential arbitrary control-flow hijacking.

Note: These steps describe a potential exploitation path based on code analysis; an active proof-of-concept exploit has not been executed.

Potential Reproduction Steps

  1. Trigger an OTP verification dialog in Chrome for Android.
  2. Initiate dismissal of the dialog (e.g., tap ‘Cancel’).
  3. Within the 200ms exit animation window, tap the ‘Get new code’ link or the ‘Confirm’ button.
  4. Observe a crash or unexpected behavior due to the use of the freed native object.

Proposed Fix

The Java OtpVerificationDialogBridge should clear its native pointer upon dismissal to prevent further JNI calls. Specifically, in OtpVerificationDialogBridge.java:

    @Override
    public void onDialogDismissed() {
        if (mNativeOtpVerificationDialogView == 0) return;
        OtpVerificationDialogBridgeJni.get().onDialogDismissed(mNativeOtpVerificationDialogView);
        // Add this line to clear the pointer:
        mNativeOtpVerificationDialogView = 0;
    }

All other methods in OtpVerificationDialogBridge that call into native (e.g., onConfirm, onNewOtpRequested) should check if mNativeOtpVerificationDialogView != 0 before proceeding. Additionally, TabModalPresenter should consider disabling interaction (e.g., via setClickable(false)) on the dialog container immediately when an exit animation begins.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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