CVE-2026-106411
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html |
modified | |
forthird_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html |
modified |
Files Changed
third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.htmlthird_party/blink/renderer/core/html/parser/html_construction_site.cc
Patch
From 64fab9307cf2698c55fef3b21cde884f76f1113d Mon Sep 17 00:00:00 2001 From: Mason Freed <[email protected]> Date: Mon, 28 Sep 2026 12:03:11 -0700 Subject: [PATCH] Avoid stale entry reference in ReconstructTheActiveFormattingElements HTMLConstructionSite::ReconstructTheActiveFormattingElements() held an Entry& into active_formatting_elements_ across AttachOrFosterParent(). Because DOM insertion is synchronous, script executed during attachment (e.g. via DOMFocusOut from fieldset child changes) can re-enter the parser through document.write() and reallocate active_formatting_elements_. The loop would then write through the stale reference. This CL updates ReconstructTheActiveFormattingElements() to avoid holding references across AttachOrFosterParent(). It stores the HTMLStackItem pointer before attachment and safely looks up the entry again after attachment before calling ReplaceElement(). The loop is also converted to check IndexOfFirstUnopenFormattingElement() on each iteration so modifications made during re-entrancy are handled safely. Fixed: 566136674,563673584 Change-Id: I30f0ff76deb6a0462c84d12e6db8a4f4bc1e5c96 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8469828 Reviewed-by: David Baron <[email protected]> Commit-Queue: Mason Freed <[email protected]> Auto-Submit: Mason Freed <[email protected]> Cr-Commit-Position: refs/heads/main@{#1706325} --- diff --git a/third_party/blink/renderer/core/html/parser/html_construction_site.cc b/third_party/blink/renderer/core/html/parser/html_construction_site.cc index 8d3333db..b97249b 100644 --- a/third_party/blink/renderer/core/html/parser/html_construction_site.cc +++ b/third_party/blink/renderer/core/html/parser/html_construction_site.cc @@ -1463,19 +1463,26 @@ void HTMLConstructionSite::ReconstructTheActiveFormattingElements() { unsigned first_unopen_element_index; - if (!IndexOfFirstUnopenFormattingElement(first_unopen_element_index)) { - return; - } - - unsigned unopen_entry_index = first_unopen_element_index; - for (; unopen_entry_index < active_formatting_elements_.size(); - ++unopen_entry_index) { - HTMLFormattingElementList::Entry& unopened_entry = - active_formatting_elements_.at(unopen_entry_index); - HTMLStackItem* reconstructed = - CreateElementFromSavedToken(unopened_entry.StackItem()); + while (IndexOfFirstUnopenFormattingElement(first_unopen_element_index)) { + HTMLStackItem* unopened_item = + active_formatting_elements_.at(first_unopen_element_index).StackItem(); + HTMLStackItem* reconstructed = CreateElementFromSavedToken(unopened_item); + // AttachOrFosterParent() can execute script synchronously (e.g. via blur + // handlers on foster parenting), which might re-enter the parser and + // reallocate active_formatting_elements_. Avoid holding references to an + // HTMLFormattingElementList::Entry across the call and re-find the entry + // afterwards. AttachOrFosterParent(reconstructed); - unopened_entry.ReplaceElement(reconstructed); + if (first_unopen_element_index < active_formatting_elements_.size() && + active_formatting_elements_.at(first_unopen_element_index) + .StackItem() == unopened_item) { + active_formatting_elements_.at(first_unopen_element_index) + .ReplaceElement(reconstructed); + } else if (HTMLFormattingElementList::Entry* entry = + active_formatting_elements_.Find( + unopened_item->GetElement())) { + entry->ReplaceElement(reconstructed); + } } } diff --git a/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html b/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html new file mode 100644 index 0000000..5fab4ad59 --- /dev/null +++ b/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" href="mailto:[email protected]"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#reconstruct-the-active-formatting-elements"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<script> +if (window.testRunner) { + testRunner.setWindowFocus(true); +} + +function parseInIframe(t, html) { + const iframe = document.createElement("iframe"); + document.documentElement.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + return new Promise((resolve) => { + iframe.onload = () => resolve(iframe); + const doc = iframe.contentDocument; + doc.open(); + doc.write(html); + doc.close(); + }); +} + +promise_test(async (t) => { + const iframe = await parseInIframe( + t, + `<!DOCTYPE html> + <fieldset> + <legend><input id="inp"></legend> + <table> + <b> + <script> + window.focusOutFired = false; + const inp = document.getElementById("inp"); + inp.addEventListener("focusout", () => { + window.focusOutFired = true; + let s = ""; + for (let i = 0; i < 50; ++i) { + s += "<b a" + i + ">"; + } + document.write(s); + }, { once: true }); + window.focus(); + inp.focus(); + inp.disabled = true; + <\/script> + <tbody> + <img> + </table> + </fieldset>`, + ); + + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + assert_true(win.focusOutFired, "focusout should have fired during foster-parented element attachment"); + assert_equals(doc.querySelectorAll("[a0]").length, 1, "Injected formatting elements should not be duplicated"); +}, "ReconstructTheActiveFormattingElements reentrancy check, via document.write"); +</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html b/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html new file mode 100644 index 0000000..5fab4ad59 --- /dev/null +++ b/third_party/blink/web_tests/wpt_internal/html/syntax/parsing/reconstruct-active-formatting-reentrant-document-write.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" href="mailto:[email protected]"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#reconstruct-the-active-formatting-elements"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<script> +if (window.testRunner) { + testRunner.setWindowFocus(true); +} + +function parseInIframe(t, html) { + const iframe = document.createElement("iframe"); + document.documentElement.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + return new Promise((resolve) => { + iframe.onload = () => resolve(iframe); + const doc = iframe.contentDocument; + doc.open(); + doc.write(html); + doc.close(); + }); +} + +promise_test(async (t) => { + const iframe = await parseInIframe( + t, + `<!DOCTYPE html> + <fieldset> + <legend><input id="inp"></legend> + <table> + <b> + <script> + window.focusOutFired = false; + const inp = document.getElementById("inp"); + inp.addEventListener("focusout", () => { + window.focusOutFired = true; + let s = ""; + for (let i = 0; i < 50; ++i) { + s += "<b a" + i + ">"; + } + document.write(s); + }, { once: true }); + window.focus(); + inp.focus(); + inp.disabled = true; + <\/script> + <tbody> + <img> + </table> + </fieldset>`, + ); + + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + assert_true(win.focusOutFired, "focusout should have fired during foster-parented element attachment"); + assert_equals(doc.querySelectorAll("[a0]").length, 1, "Injected formatting elements should not be duplicated"); +}, "ReconstructTheActiveFormattingElements reentrancy check, via document.write"); +</script>
Original Bug Report
Use-after-free in HTMLConstructionSite::ReconstructTheActiveFormattingElements
Summary
HTMLConstructionSite::ReconstructTheActiveFormattingElements() holds an Entry& into the active-formatting-elements HeapVector across AttachOrFosterParent(). Since 4ddabf432e018 (2026-09-01) that insertion is synchronous, so a script it runs can re-enter the parser through document.write and reallocate the vector. The loop then writes through the stale reference. Renderer, zero interaction, default flags.
Environment
- Chrome for Testing 155.0.8059.12 (Beta) and 156.0.8072.0 (Canary); code present at main b7c07f35773a. Stable 154 is not affected (predates the change).
- Linux x86-64
- Evidence build: ASan, Chromium main 156.0.8074.0
Reproduction
chrome --user-data-dir=/tmp/p --no-first-run poc.html
Zero interaction; the renderer crashes after about 2 s.
Rate: Beta 8/8 at default flags. On Chromium/Chrome for Testing 156, add --disable-field-trial-config (4/4 Canary): their test config disables kHeapVectorPromptlyFree, which Google Chrome enables by default.
Second, independent set-up. poc-fieldset.html (needs page focus) reaches the same store: during the synchronous insertion, HTMLFieldSetElement::ChildrenChanged blurs a focused, disabled input, and its DOMFocusOut handler calls document.write. Beta 6/6 at default flags, renderer SIGSEGV writing into freed memory; ASan in asan-fieldset.txt.
Evidence
Renderer, main thread. Full reports: asan.txt; asan-default.txt is ASan 156 without the switch (container-overflow WRITE).
use-after-poison WRITE of size 4
#0 HTMLConstructionSite::ReconstructTheActiveFormattingElements :1478
#1 HTMLTreeBuilder::ProcessStartTagForInBody html_tree_builder.cc:1059
#2 HTMLTreeBuilder::ProcessStartTagForInTable html_tree_builder.cc:1408
Stock Beta 155 dies at a CFI check on a virtual call through the stale data (renderer SIGILL).
Vulnerable code
third_party/blink/renderer/core/html/parser/html_construction_site.cc (main)
1473 HTMLFormattingElementList::Entry& unopened_entry =
1474 active_formatting_elements_.at(unopen_entry_index); [1]
1477 AttachOrFosterParent(reconstructed); [2]
1478 unopened_entry.ReplaceElement(reconstructed); [3]
[1] Reference into the backing. [2] Script can reallocate it. [3] Stale write.
Impact
Any web page, no interaction. Renderer use-after-free write, introduced in 155. Reporter’s estimate: High (S1). A separate defect (ad tracker) reaches the same store on Stable 154; reported in 566156384.
Credit
Xinyang Ge (Anthropic), assisted by Claude
Attachments: poc.html, asan.txt, asan-default.txt, poc-fieldset.html, asan-fieldset.txt, report.md