CVE-2026-79073
Overview
Background
- Adoption agency algorithm (AAA)
- the HTML tree-builder step that handles misnested formatting elements such as
<b>…<p>…</b>by cloning formatting elements and moving already-parsed nodes (the “furthest block” and its relatives) to new parents. - `kInsertAlreadyParsedChild` task
- an
HTMLConstructionSiteTaskoperation thatHTMLConstructionSitequeues to re-parent a node already in the DOM. AAA uses it throughHTMLConstructionSite::InsertAlreadyParsedChild. - Foster parenting
- when the target parent is a table-related element (
CausesFosterParenting()), the node is inserted at a “foster site” found byFindFosterSite(normally just before the<table>), which may be a different parent entirely. - Script-driven DOM drift
- inline
<script>blocks run during parsing and can move nodes still on the parser’s stack of open elements, so the DOM no longer matches the tree builder’s view: for example, an ancestor becomes a descendant, a node moves into a template or shadow root, or a<table>moves into anotherDocument.
Root Cause Analysis
The tree builder’s AAA trusts its stack of open elements when choosing a new parent for an already-parsed node. Script that runs mid-parse can change the real DOM relationships first. In the regression tests, script appends the “furthest block” fb to body and then appends the “common ancestor” ca into fb, either directly, through a shadow root, or through template.content. AAA then asks to insert fb under ca, but ca is now inside fb, so the move would create a cycle. In the document-target test, the <table> is moved into a Document that has no root element, so the foster site’s parent is a Document.
Before the fix, the executor for kInsertAlreadyParsedChild tasks called Insert(task) without checking either case. In addition, InsertAlreadyParsedChild foster-parented through the generic FosterParent path instead of an already-parsed-child task.
The fix adds two checks before Insert(task). If the parent is a Document that already has a documentElement(), the node is dropped. If task.child->ContainsIncludingHostElements(*task.parent) is true, which also covers cycles through shadow hosts and template hosts, the node is dropped. “Dropped” means it is removed from its current parent with ParserRemoveChild and not reinserted.
The fix also adds FosterParentAlreadyParsedChild, which queues a kInsertAlreadyParsedChild task, so foster-parented AAA moves now pass through these checks too.
Document a second element child. The fix checks both cases at the point of insertion, drops nodes that cannot be reinserted legally (following whatwg/html PR #12709), and sends foster-parented AAA moves through the same check.Attack Path
- Misnested markup
a page serves HTML whose misnested formatting element triggers AAA, such as
<div id=ca><b><p id=fb>…</b></div>. Ordinary web content can do this with no flags. - Mid-parse DOM rearrangement
an inline
<script>, run while those elements are still open, moves nodes so that the AAA “common ancestor” becomes a descendant of the “furthest block” (directly, via a shadow root, or viatemplate.content), or moves a<table>into a separateDocumentwith no root element. - AAA queues an illegal move
when the closing tag (
</b>) runs AAA,InsertAlreadyParsedChildqueues a re-parent offbunderca, or under the foster site’sDocument. Before the fix, nothing checked whether that move was legal. - Inconsistent tree
Insert(task)would create a node cycle or an invalidDocumentchild list. The tests show this ends in a crash; any memory-corruption consequence beyond that, such as use-after-free while walking a cyclic tree, is(inferred).
Impact Assessment
Document with an extra element child. At minimum this crashes the renderer process, as the regression tests show. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. Any exploitable memory corruption that follows from the invalid tree is (inferred), and the impact stays inside the sandboxed renderer. The only requirements are loading attacker-controlled HTML with script enabled; no user gesture, flag or compromised renderer is needed.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html |
modified | |
ifthird_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html |
modified | |
ifthird_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html |
modified | |
ifthird_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html |
modified | |
ifthird_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html |
modified |
Files Changed
third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.htmlthird_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.htmlthird_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txtthird_party/blink/renderer/core/html/parser/html_construction_site.ccthird_party/blink/renderer/core/html/parser/html_construction_site.hthird_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txtthird_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.htmlthird_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txtthird_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txtthird_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txtthird_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.htmlthird_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html
Audit Directions
- Parser fast paths that skip DOM pre-insertion validity checksreview every
Parser*insert/append routine and queuedHTMLConstructionSitetask for missing hierarchy checks (cycles,Documentchild constraints, template/shadow boundaries) when script can run between queuing and execution. - Tree-builder state vs. live DOMlook for other algorithms that rely on the stack of open elements or the list of active formatting elements (AAA inner loop, foster parenting,
</p>/</body>recovery,templatehandling) and may act on nodes that script has since moved. - Host-crossing containmentwhen checking ancestry, make sure checks use host-aware helpers such as
ContainsIncludingHostElements. Plaincontains()misses cycles that pass through shadow roots ortemplate.content.
Patch
From 5d50303b9695b52ec773f27a6ecb3c2ca464f532 Mon Sep 17 00:00:00 2001
From: Noam Rosenthal <nrosenthal@chromium.org>
Date: Mon, 20 Jul 2026 15:36:21 -0700
Subject: [PATCH] HTML parser: Drop a node the adoption agency cannot reinsert
Make a few check when moving around nodes with AAA.
The tests are internal temporarily until the upstream WPT and spec PR
land, but that process shouldn't delay fixing the crash.
See https://github.com/whatwg/html/pull/12709
Bug: 536662911
Change-Id: Ia357a2efea8029129b68b7fe647de4d7ba450b42
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8124514
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1664983}
---
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 94ed802..8448e69 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
@@ -293,6 +293,21 @@
DCHECK_EQ(task.operation,
HTMLConstructionSiteTask::kInsertAlreadyParsedChild);
+ // See https://github.com/whatwg/html/pull/12709
+ if (Document* parentDoc = DynamicTo<Document>(task.parent.Get())) {
+ if (parentDoc->documentElement()) {
+ if (task.child->parentNode()) {
+ task.child->parentNode()->ParserRemoveChild(*task.child);
+ }
+ return;
+ }
+ } else if (task.child->ContainsIncludingHostElements(*task.parent)) {
+ if (task.child->parentNode()) {
+ task.child->parentNode()->ParserRemoveChild(*task.child);
+ }
+ return;
+ }
+
Insert(task);
}
@@ -1224,7 +1239,7 @@
void HTMLConstructionSite::InsertAlreadyParsedChild(HTMLStackItem* new_parent,
HTMLStackItem* child) {
if (new_parent->CausesFosterParenting()) {
- FosterParent(child->GetNode());
+ FosterParentAlreadyParsedChild(child->GetNode());
return;
}
@@ -1613,6 +1628,15 @@
QueueTask(task, true);
}
+void HTMLConstructionSite::FosterParentAlreadyParsedChild(Node* child) {
+ HTMLConstructionSiteTask task(
+ HTMLConstructionSiteTask::kInsertAlreadyParsedChild);
+ FindFosterSite(task);
+ task.child = child;
+ DCHECK(task.parent);
+ QueueTask(task, true);
+}
+
void HTMLConstructionSite::PendingText::Trace(Visitor* visitor) const {
visitor->Trace(parent);
visitor->Trace(next_child);
diff --git a/third_party/blink/renderer/core/html/parser/html_construction_site.h b/third_party/blink/renderer/core/html/parser/html_construction_site.h
index 987bb598..968906f 100644
--- a/third_party/blink/renderer/core/html/parser/html_construction_site.h
+++ b/third_party/blink/renderer/core/html/parser/html_construction_site.h
@@ -189,6 +189,7 @@
bool ShouldFosterParent() const;
void FosterParent(Node*);
+ void FosterParentAlreadyParsedChild(Node*);
bool IndexOfFirstUnopenFormattingElement(
unsigned& first_unopen_element_index) const;
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html
new file mode 100644
index 0000000..edf7f06
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+document.body.appendChild(window.fb);
+window.fb.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html
new file mode 100644
index 0000000..a2d7238
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<table><tbody><tr><b><div id=fb><script>
+window.fb = document.getElementById('fb');
+window.other = document.implementation.createHTMLDocument('');
+window.other.documentElement.remove();
+window.other.appendChild(document.querySelector('table'));
+</script></b></table>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html
new file mode 100644
index 0000000..f45e242
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><i><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+document.body.appendChild(window.fb);
+window.fb.appendChild(window.ca);
+</script></i></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html
new file mode 100644
index 0000000..1bb1fec
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+var host = document.createElement('span');
+var root = host.attachShadow({mode:'open'});
+document.body.appendChild(window.fb);
+window.fb.appendChild(host);
+root.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html
new file mode 100644
index 0000000..ebd0cc5
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+var tmpl = document.createElement('template');
+document.body.appendChild(window.fb);
+window.fb.appendChild(tmpl);
+tmpl.content.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
Regression Test / PoC
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html
new file mode 100644
index 0000000..edf7f06
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-cycle-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+document.body.appendChild(window.fb);
+window.fb.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html
new file mode 100644
index 0000000..a2d7238
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-document-target-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<table><tbody><tr><b><div id=fb><script>
+window.fb = document.getElementById('fb');
+window.other = document.implementation.createHTMLDocument('');
+window.other.documentElement.remove();
+window.other.appendChild(document.querySelector('table'));
+</script></b></table>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html
new file mode 100644
index 0000000..f45e242
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-inner-loop-cycle-crash.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><i><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+document.body.appendChild(window.fb);
+window.fb.appendChild(window.ca);
+</script></i></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html
new file mode 100644
index 0000000..1bb1fec
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-shadow-cycle-crash.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+var host = document.createElement('span');
+var root = host.attachShadow({mode:'open'});
+document.body.appendChild(window.fb);
+window.fb.appendChild(host);
+root.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt
new file mode 100644
index 0000000..c2541f4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html
new file mode 100644
index 0000000..ebd0cc5
--- /dev/null
+++ b/third_party/blink/web_tests/fast/parser/adoption-agency-template-cycle-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<div id=ca><b><p id=fb><script>
+window.fb = document.getElementById('fb');
+window.ca = document.getElementById('ca');
+var tmpl = document.createElement('template');
+document.body.appendChild(window.fb);
+window.fb.appendChild(tmpl);
+tmpl.content.appendChild(window.ca);
+</script></b></div>
+<script>
+if (window.testRunner) {
+ testRunner.notifyDone();
+}
+</script>
+PASS if no crash.