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 <[email protected]> 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 <[email protected]> Commit-Queue: David Baron <[email protected]> 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