Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Payments
DescriptionUse after free in Payments
ComponentPayments
Bug ClassUAF
Tracker517522620
Fix commit87e5f2417a5b (chromium/src) +56/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-25

Changed Functions

FunctionChangeNotes
if
components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
modified
TEST_F
components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
modified

Files Changed

  • components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
  • components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
From 87e5f2417a5be76bd86f73ed516a4209ea9a26a5 Mon Sep 17 00:00:00 2001
From: Luis Antunes <[email protected]>
Date: Tue, 23 Jun 2026 07:22:12 -0700
Subject: [PATCH] [Autofill] Fix potential UAF in VirtualCardEnrollmentManager

Cache the enrollment source locally and use a WeakPtr to guard the
Reset() call after executing the response callback, as the callback
may synchronously delete the VirtualCardEnrollmentManager.

Fixed: 517522620
Change-Id: I577bf09137fba2cc818be52b3f29e88c2fa64b40
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7961536
Commit-Queue: Luis Antunes <[email protected]>
Reviewed-by: Slobodan Pejic <[email protected]>
Reviewed-by: Vinny Persky <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1650986}
---

diff --git a/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc b/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
index 55a6bf0..e7c43e05 100644
--- a/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
+++ b/components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
@@ -347,17 +347,27 @@
         state_.virtual_card_enrollment_fields.credit_card.instrument_id()));
   }
 
+  base::WeakPtr<VirtualCardEnrollmentManager> weak_this =
+      weak_ptr_factory_.GetWeakPtr();
+  VirtualCardEnrollmentSource source =
+      state_.virtual_card_enrollment_fields.virtual_card_enrollment_source;
+
   // Relay the response to the server card editor page. This also destroys the
-  // payments delegate if the editor was already closed.
+  // payments delegate if the editor was already closed. Running this callback
+  // may synchronously delete `this`, so no further accesses to `this` are
+  // allowed.
   if (virtual_card_enrollment_update_response_callback_.has_value()) {
     std::move(virtual_card_enrollment_update_response_callback_.value())
         .Run(result);
   }
 
   LogUpdateVirtualCardEnrollmentRequestResult(
-      state_.virtual_card_enrollment_fields.virtual_card_enrollment_source,
-      type, result == PaymentsRpcResult::kSuccess);
-  Reset();
+      source, type, result == PaymentsRpcResult::kSuccess);
+
+  // Guard `Reset()` to prevent UAF if the object was deleted synchronously.
+  if (weak_this) {
+    Reset();
+  }
 }
 
 void VirtualCardEnrollmentManager::OnVirtualCardEnrollCompleted(
diff --git a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
index af5c7e24..0087717 100644
--- a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/functional/callback.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
@@ -404,6 +405,47 @@
       /*sample=*/false, 1);
 }
 
+// Ensures that if the manager is synchronously destroyed during the enrollment
+// response callback, it does not cause a Use-After-Free.
+TEST_F(VirtualCardEnrollmentManagerTest, Enroll_JniCleanupDuringCallbackNoUaf) {
+  base::HistogramTester histogram_tester;
+
+  // Setup state for Enroll.
+  VirtualCardEnrollmentProcessState* state =
+      virtual_card_enrollment_manager_->GetVirtualCardEnrollmentProcessState();
+  state->vcn_context_token = kTestVcnContextToken;
+  state->virtual_card_enrollment_fields.credit_card = *card_;
+  state->virtual_card_enrollment_fields.virtual_card_enrollment_source =
+      VirtualCardEnrollmentSource::kDownstream;
+
+  payments_data_manager().SetPaymentsCustomerData(
+      std::make_unique<PaymentsCustomerData>(/*customer_id=*/"123456"));
+
+  // Mock the network call to run the callback synchronously.
+  EXPECT_CALL(multiple_request_payments_network_interface(),
+              UpdateVirtualCardEnrollment)
+      .WillOnce(
+          [&](const payments::UpdateVirtualCardEnrollmentRequestDetails& req,
+              base::OnceCallback<void(
+                  payments::PaymentsAutofillClient::PaymentsRpcResult)>
+                  callback) {
+            std::move(callback).Run(
+                payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess);
+            return payments::RequestId("11223344");
+          });
+
+  // Call Enroll with a callback that destroys the manager.
+  virtual_card_enrollment_manager_->Enroll(base::BindLambdaForTesting(
+      [&](payments::PaymentsAutofillClient::PaymentsRpcResult result) {
+        virtual_card_enrollment_manager_.reset();
+      }));
+
+  // Verify that the metrics were logged.
+  histogram_tester.ExpectUniqueSample(
+      "Autofill.VirtualCard.Enroll.Result.Downstream",
+      /*sample=*/true, 1);
+}
+
 #if !BUILDFLAG(IS_IOS)
 TEST_F(VirtualCardEnrollmentManagerTest, StrikeDatabase_BubbleAccepted) {
   base::HistogramTester histogram_tester;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
index af5c7e24..0087717 100644
--- a/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/virtual_card_enrollment_manager_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/functional/callback.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
@@ -404,6 +405,47 @@
       /*sample=*/false, 1);
 }
 
+// Ensures that if the manager is synchronously destroyed during the enrollment
+// response callback, it does not cause a Use-After-Free.
+TEST_F(VirtualCardEnrollmentManagerTest, Enroll_JniCleanupDuringCallbackNoUaf) {
+  base::HistogramTester histogram_tester;
+
+  // Setup state for Enroll.
+  VirtualCardEnrollmentProcessState* state =
+      virtual_card_enrollment_manager_->GetVirtualCardEnrollmentProcessState();
+  state->vcn_context_token = kTestVcnContextToken;
+  state->virtual_card_enrollment_fields.credit_card = *card_;
+  state->virtual_card_enrollment_fields.virtual_card_enrollment_source =
+      VirtualCardEnrollmentSource::kDownstream;
+
+  payments_data_manager().SetPaymentsCustomerData(
+      std::make_unique<PaymentsCustomerData>(/*customer_id=*/"123456"));
+
+  // Mock the network call to run the callback synchronously.
+  EXPECT_CALL(multiple_request_payments_network_interface(),
+              UpdateVirtualCardEnrollment)
+      .WillOnce(
+          [&](const payments::UpdateVirtualCardEnrollmentRequestDetails& req,
+              base::OnceCallback<void(
+                  payments::PaymentsAutofillClient::PaymentsRpcResult)>
+                  callback) {
+            std::move(callback).Run(
+                payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess);
+            return payments::RequestId("11223344");
+          });
+
+  // Call Enroll with a callback that destroys the manager.
+  virtual_card_enrollment_manager_->Enroll(base::BindLambdaForTesting(
+      [&](payments::PaymentsAutofillClient::PaymentsRpcResult result) {
+        virtual_card_enrollment_manager_.reset();
+      }));
+
+  // Verify that the metrics were logged.
+  histogram_tester.ExpectUniqueSample(
+      "Autofill.VirtualCard.Enroll.Result.Downstream",
+      /*sample=*/true, 1);
+}
+
 #if !BUILDFLAG(IS_IOS)
 TEST_F(VirtualCardEnrollmentManagerTest, StrikeDatabase_BubbleAccepted) {
   base::HistogramTester histogram_tester;
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser UAF in VirtualCardEnrollmentManager via synchronous JNI cleanup

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 exists in the Android Autofill settings payment methods flow. When a user exits the server card editor while a virtual card enrollment request is in-flight, a deferred cleanup is configured. When the server response subsequently arrives, a synchronous JNI callback deletes the parent delegate and its child objects while their methods are actively executing on the stack.

Affected files:

  • components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc
  • chrome/browser/android/preferences/autofill/autofill_payment_methods_delegate.cc
  • components/autofill/core/browser/payments/multiple_request_payments_network_interface_base.cc
  • chrome/browser/android/preferences/autofill/autofill_payment_methods_delegate.h

Estimated timestamp from git blame: 2022-06-14

Summary

A potential Use-After-Free (UAF) vulnerability exists in the Android Autofill payment methods settings flow. When a user backs out of the virtual card enrollment or unenrollment editor while a server request is in-flight, the editor defers its native cleanup. When the server response subsequently arrives, a synchronous JNI callback is executed that deletes the parent AutofillPaymentMethodsDelegate class. Since this delegate owns both VirtualCardEnrollmentManager and the payments network interface, their destruction occurs while their methods are actively executing on the call stack, resulting in potential Use-After-Free reads/writes and heap corruption in the browser process.

Root Cause Analysis

In components/autofill/core/browser/payments/virtual_card_enrollment_manager.cc:

void VirtualCardEnrollmentManager::OnDidGetUpdateVirtualCardEnrollmentResponse(
    VirtualCardEnrollmentRequestType type,
    PaymentsRpcResult result) {
...
  // Relay the response to the server card editor page. This also destroys the
  // payments delegate if the editor was already closed.
  if (virtual_card_enrollment_update_response_callback_.has_value()) {
    std::move(virtual_card_enrollment_update_response_callback_.value())
        .Run(result);
  }

  LogUpdateVirtualCardEnrollmentRequestResult(
      state_.virtual_card_enrollment_fields.virtual_card_enrollment_source,
      type, result == PaymentsRpcResult::kSuccess);
  Reset();
}

The synchronous callback invocation .Run(result) eventually invokes RunVirtualCardEnrollmentUpdateResponseCallback inside chrome/browser/android/preferences/autofill/autofill_payment_methods_delegate.cc. This executes base::android::RunBooleanCallbackAndroid to run JNI code.

In chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillServerCardEditor.java:

mVirtualCardEnrollmentUpdateResponseCallback =
        isUpdateSuccessful -> {
            if (mServerCardEditorClosed) {
                mDelegate.cleanup();
            } else {
...

If the user closed the editor screen while the request was in-flight, mServerCardEditorClosed is true, which synchronously invokes the JNI mapping for mDelegate.cleanup(), executing delete this on AutofillPaymentMethodsDelegate.

Deconstruction of AutofillPaymentMethodsDelegate deletes VirtualCardEnrollmentManager and MultipleRequestPaymentsNetworkInterface. Consequently, when the JNI call returns, this is dangling inside OnDidGetUpdateVirtualCardEnrollmentResponse(). The execution then attempts to access state_ and call Reset(), causing a UAF read and write.

Additionally, in components/autofill/core/browser/payments/multiple_request_payments_network_interface_base.cc:

void RequestOperation::ReportOperationResult(PaymentsRpcResult result) {
  CHECK(request_);
  request_->RespondToDelegate(result);
  payments_network_interface_->OnRequestFinished(request_operation_id_);
}

Once RespondToDelegate(result) returns, the active RequestOperation object (this) and the payments_network_interface_ have both been deleted, leading to an additional Use-After-Free dereference when attempting to call OnRequestFinished.

Because the active execution context uses stack-allocated references (including the implicit this pointer) to execute class methods on heap-allocated objects that have already been deleted, this UAF is not mitigated by raw_ptr (MiraclePtr / BackupRefPtr) protections.

Potential Steps to Trigger

Note: The following are suggested/potential steps to trigger the issue, as our tooling agent does not yet have the capability to run code or successfully compile a proof-of-concept.

  1. Open Chrome on Android, go to Settings -> Payment methods.
  2. Select an eligible server-saved card to open AutofillServerCardEditor.
  3. Tap “Turn on virtual card” (or “Turn off”) and accept the confirmation dialog.
  4. Immediately exit the editor page (e.g., press Back) while the network request is still in-flight.
  5. If the network response arrives shortly after exiting, the synchronous cleanup should execute, potentially causing a crash or heap corruption in the browser process.

Suggested Fix

To resolve this issue, ensure that the cleanup of the AutofillPaymentMethodsDelegate occurs asynchronously rather than synchronously within the callback. This will allow the C++ call stack to safely unwind before the executing objects are deleted.

For example, modify the callback in AutofillServerCardEditor.java to post a task to the main thread’s message loop rather than destroying the delegate synchronously:

mVirtualCardEnrollmentUpdateResponseCallback =
        isUpdateSuccessful -> {
            if (mServerCardEditorClosed) {
                new Handler(Looper.getMainLooper()).post(() -> mDelegate.cleanup());
            } else {
...

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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.

View on issue tracker