Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Autofill
DescriptionUse after free in Autofill
ComponentAutofill
Bug ClassUAF
Tracker494740162
Fix commitc7c2ce4068f1 (chromium/src) +25/-19
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
components/autofill/core/browser/filling/form_autofill_history.cc
modified
if
components/autofill/core/browser/filling/form_filler.cc
modified

Files Changed

  • components/autofill/core/browser/filling/form_autofill_history.cc
  • components/autofill/core/browser/filling/form_autofill_history.h
  • components/autofill/core/browser/filling/form_filler.cc
From c7c2ce4068f1ddf78b2c019a7d4fec6a0a8800ea Mon Sep 17 00:00:00 2001
From: Gianmarco Picarella <picarella@google.com>
Date: Tue, 14 Apr 2026 07:57:24 -0700
Subject: [PATCH] Fix use-after-free of std::list iterator in FormFiller::UndoAutofill

Fixes a use-after-free crash in FormFiller::UndoAutofill(…) when
handling forms containing fields with duplicate FieldGlobalId.

Currently, FormAutofillHistory silently collapses these duplicate IDs
into a single entry. However, FormFiller::UndoAutofill(…) iterates over
all fields in the FormData to erase them. Calling
EraseFieldFillingEntry(…) for the duplicate IDs frees the underlying
std::list node prematurely. Subsequent loop iterations then attempt to
access this freed memory using a stale iterator, resulting in a browser
process crash.

This change resolved the problem by calling EraseFieldFillingEntry(…) on
the subset of unique FieldGlobalIds in a separate for loop. We also
fixed a minor bug in FormAutofillHistory::EraseFieldFillingEntry(…) by
updating size_ appropriately.

Bug: 494740162
Change-Id: Ic95dee48fa3ef8d2e3688880a3e49a6a37e4442b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7743400
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Gianmarco Picarella <picarella@google.com>
Reviewed-by: Jihad Hanna <jihadghanna@google.com>
Cr-Commit-Position: refs/heads/main@{#1614452}
---

diff --git a/components/autofill/core/browser/filling/form_autofill_history.cc b/components/autofill/core/browser/filling/form_autofill_history.cc
index 9d1e6c7..0e4e771f 100644
--- a/components/autofill/core/browser/filling/form_autofill_history.cc
+++ b/components/autofill/core/browser/filling/form_autofill_history.cc
@@ -95,12 +95,14 @@
   }
 }
 
-void FormAutofillHistory::EraseFieldFillingEntry(
-    std::list<FormFillingEntry>::iterator fill_operation,
-    FieldGlobalId field_id) {
-  fill_operation->erase(field_id);
-  if (fill_operation->empty()) {
-    EraseFormFillEntry(fill_operation);
+void FormAutofillHistory::EraseFieldFillingEntries(
+    std::list<FormFillingEntry>::iterator filling_entry,
+    base::span<const FieldGlobalId> field_ids) {
+  for (const FieldGlobalId& field_id : field_ids) {
+    size_ -= filling_entry->erase(field_id);
+  }
+  if (filling_entry->empty()) {
+    EraseFormFillEntry(filling_entry);
   }
 }
 
diff --git a/components/autofill/core/browser/filling/form_autofill_history.h b/components/autofill/core/browser/filling/form_autofill_history.h
index e60e0e32..4199dc4c0 100644
--- a/components/autofill/core/browser/filling/form_autofill_history.h
+++ b/components/autofill/core/browser/filling/form_autofill_history.h
@@ -103,12 +103,12 @@
       FillingProduct filling_product,
       bool is_refill);
 
-  // Erases the field history information corresponding to `field_id` in
-  // `fill_operation`. If the form filling entry becomes empty afterwards, the
-  // function also removes it from `history_`.
-  void EraseFieldFillingEntry(
-      std::list<FormFillingEntry>::iterator fill_operation,
-      FieldGlobalId field_id);
+  // Erases the field history information corresponding to all `field_ids` from
+  // `filling_entry`. If `filling_entry` becomes empty afterwards, the function
+  // also removes it from `history_`.
+  void EraseFieldFillingEntries(
+      std::list<FormFillingEntry>::iterator filling_entry,
+      base::span<const FieldGlobalId> field_ids);
 
   // Returns the first entry in `history_` (corresponding to the last
   // chronological entry) that has information about the field represented by
diff --git a/components/autofill/core/browser/filling/form_filler.cc b/components/autofill/core/browser/filling/form_filler.cc
index 9fcd29a2..5d1ed98 100644
--- a/components/autofill/core/browser/filling/form_filler.cc
+++ b/components/autofill/core/browser/filling/form_filler.cc
@@ -13,6 +13,7 @@
 #include "base/check_op.h"
 #include "base/containers/flat_set.h"
 #include "base/containers/map_util.h"
+#include "base/containers/to_vector.h"
 #include "base/feature_list.h"
 #include "base/hash/hash.h"
 #include "base/metrics/histogram_functions.h"
@@ -827,15 +828,18 @@
           previous_state.autofill_source_profile_guid);
       autofill_field.set_autofilled_type(previous_state.autofilled_type);
       autofill_field.set_filling_product(previous_state.filling_product);
-
-      // The filling history is not cleared on previews as it might be used for
-      // future previews or for the filling. it is also cleared field by field
-      // because some fields in the current entry might not be used now but
-      // could still be valuable (see crbug.com/416019464).
-      form_autofill_history_.EraseFieldFillingEntry(fill_operation_it,
-                                                    field.global_id());
     }
   }
+
+  if (action_persistence == mojom::ActionPersistence::kFill) {
+    // The filling history is not cleared on previews as it might be used for
+    // future previews or for the filling. It is also cleared field by field
+    // because some fields in the current entry might not be used now but
+    // could still be valuable (see crbug.com/416019464).
+    form_autofill_history_.EraseFieldFillingEntries(
+        fill_operation_it, base::ToVector(fields, &FormFieldData::global_id));
+  }
+
   form.set_fields(std::move(fields));
 
   // Do not attempt a refill after an Undo operation.
Loading diff…

Original Bug Report

reported by je...@gmail.com

Use-after-free of std::list iterator in FormFiller::UndoAutofill via duplicate FieldGlobalIds

Use-after-free of std::list iterator in FormFiller::UndoAutofill via duplicate FieldGlobalIds

Summary

A use-after-free in the browser process can be triggered by a compromised renderer that sends a FormData containing duplicate FieldRendererIds through the mojom::AutofillDriver::AskForValuesToFill IPC. When the user fills a form with Autofill and subsequently undoes the fill, the browser’s FormFiller::UndoAutofill iterates over all fields including duplicates, erasing fill history entries one by one. Because duplicates were silently collapsed into a single map entry during the fill, the loop drains the map and frees the underlying std::list node while a stale iterator to that node remains in use for subsequent iterations. The dangling iterator is not protected by MiraclePtr since the freed object is an STL list node, not a raw_ptr-wrapped member. This affects all platforms.

Bisect

Introducing Commit: a58a578316bfa8edc0dcc649581e9a394509964d

This CL changed UndoAutofill from erasing the entire fill history entry once after the loop to erasing entries per-field inside the loop (to fix crbug.com/416019464). The new EraseFieldFillingEntry deletes the std::list node when the map empties, but the loop still holds fill_operation_it — so duplicate field IDs from a compromised renderer can drain the map early and free the node mid-iteration.

Root Cause

The browser process does not enforce that FieldRendererId values are unique within a single FormData. The FormData::fields() accessor documents this explicitly, noting that collisions can occur when the renderer is compromised:

// components/autofill/core/common/form_data.h:295-310
//
// WARNING: `fields` may contain duplicates:
//
// Usually, FormFieldData::global_id() ... uniquely identify
// objects in `fields`. This is reliable enough for practical purposes, but
// not guaranteed.
//
// Collisions are possible in rare cases. Two known scenarios are:
// - The renderer is compromised and sends duplicates.

Neither IsValidFormData() nor bad_message::CheckFieldInForm() reject a FormData with duplicate field IDs. IsValidFormData() checks only string lengths and field count; CheckFieldInForm() checks only whether a given field_id exists somewhere in the form.

When a form is filled through Autofill, FormAutofillHistory::AddFormFillingEntry records per-field undo state in a std::map<FieldGlobalId, FieldFillingEntry> within a std::list node. Because std::map::emplace does not overwrite existing entries, duplicate FieldGlobalId values in the input are silently collapsed into a single map key:

// components/autofill/core/browser/filling/form_autofill_history.cc:64-90
for (const auto [field, autofill_field] :
     base::zip(filled_fields, filled_autofill_fields)) {
  size_ +=
      history_.front()
          .emplace(field->global_id(), FieldFillingEntry(...))
          .second;  // returns false (no increment) for duplicate keys
}

The asymmetry emerges during undo. FormFiller::UndoAutofill retrieves a single fill_operation_it pointing to the list node, then iterates over all fields in the FormData, including every duplicate:

// components/autofill/core/browser/filling/form_filler.cc:782-821
for (FormFieldData& field : fields) {
    auto it = fill_operation_it->find(field.global_id());
    CHECK(it != fill_operation_it->end());
    const FormAutofillHistory::FieldFillingEntry& previous_state = it->second;
    // ...
    if (action_persistence == mojom::ActionPersistence::kFill) {
      // ...
      form_autofill_history_.EraseFieldFillingEntry(fill_operation_it,
                                                    field.global_id());
    }
}

Each call to EraseFieldFillingEntry removes one key from the map. When the map becomes empty, it frees the entire list node:

// components/autofill/core/browser/filling/form_autofill_history.cc:98-105
void FormAutofillHistory::EraseFieldFillingEntry(
    std::list<FormFillingEntry>::iterator fill_operation,
    FieldGlobalId field_id) {
  fill_operation->erase(field_id);
  if (fill_operation->empty()) {
    EraseFormFillEntry(fill_operation);  // history_.erase(fill_operation)
  }
}

With N unique fields and 2N total fields (each duplicated), the first N iterations of the loop erase all unique keys, draining the map to zero entries and freeing the list node. On iteration N+1, the code dereferences fill_operation_it to call find() on the freed map, producing a heap-use-after-free in the browser process’s main thread.

This UAF is not mitigated by any existing defense. MiraclePtr does not apply because the dangling reference is a standard library list iterator, not a raw_ptr<> class member. The CHECK(it != fill_operation_it->end()) guard executes after the UAF at fill_operation_it->find(), so it cannot prevent the invalid access.

Reproduce

Tested at commit 7c89d33808e551aed6122c1f324864784011c158.

Apply the renderer-only patch (simulates a compromised renderer by duplicating all fields in ExtractFormData):

cd ~/chromium/src
git apply patch.diff
autoninja -C out/asan-release chrome

Start an HTTP server and launch the ASAN build:

python3 -m http.server 8888 &

out/asan-release/Chromium.app/Contents/MacOS/Chromium --user-data-dir=/tmp/poc-$(date +%s)

Trigger manually:

  1. Navigate to chrome://settings/addresses, click “Add”, fill in any address, and save.
  2. Navigate back to http://127.0.0.1:8888/poc.html.
  3. Click the first form field (name field) and select the autofill address suggestion to fill the form.
  4. Click the first field (name field) again — it must be the first/leftmost field for the “Undo Autofill” option to appear.
  5. Select “Undo Autofill” from the dropdown.

The browser process crashes with the following ASAN report:

==3727567==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b485150ee68 at pc 0x56328503622a bp 0x7ffe4ca09270 sp 0x7ffe4ca09268
READ of size 8 at 0x7b485150ee68 thread T0 (chrome)
    #0 in autofill::FormFiller::UndoAutofill(...) gen/third_party/libc++/src/include/__tree:950:54
    #1 in autofill::BrowserAutofillManager::UndoAutofill(...) browser_autofill_manager.cc:2167
    #2 in autofill::BrowserAutofillManager::OnAskForValuesToFillImpl(...) browser_autofill_manager.cc:1273
    ...
    #11 in autofill::mojom::AutofillDriverStubDispatch::Accept(...) autofill_driver.mojom.cc:1899

freed by thread T0 (chrome) here:
    #0 in operator delete
    #1 in std::list<...>::erase(...)
    #2 in autofill::FormAutofillHistory::EraseFieldFillingEntry(...) form_autofill_history.cc:110
    #3 in autofill::FormFiller::UndoAutofill(...) form_filler.cc:818
    #4 in autofill::BrowserAutofillManager::UndoAutofill(...) browser_autofill_manager.cc:2167
    #5 in autofill::BrowserAutofillManager::OnAskForValuesToFillImpl(...) browser_autofill_manager.cc:1273

SUMMARY: AddressSanitizer: heap-use-after-free gen/third_party/libc++/src/include/__tree:950:54 in autofill::FormFiller::UndoAutofill(...)

The complete ASAN log is in asan.log.

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker