High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in DOM
DescriptionMemory corruption in DOM
ComponentDOM
Bug ClassUAF
Tracker576203565
Fix commit3318e836e90b (chromium/src) +181/-6
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-29

Background

`UniqueElementData::attribute_vector_`
Blink’s per-element mutable attribute storage (core/dom/element.cc), a vector of Attribute entries. Appending/removing entries can reallocate the vector, invalidating any reference or index obtained earlier.
`Element::WillModifyAttribute()`
Called before an attribute mutation; it can synchronously run script two ways: (1) by pausing in the debugger via probe::WillModifyDOMAttr, and (2) via UpdateName() → AddNamedItem, which can lazily initialize a frame’s WindowProxy and invoke a page-defined setter (e.g. chrome.loadTimes).
`SetAttributeInternal` / `RemoveAttributeInternal` / `AppendAttributeInternal`
The three low-level mutators that acquire an index or a const AtomicString& reference into the attribute storage and then call WillModifyAttribute() before using it.
`DidMoveToNewDocument()`
Runs when an element is adopted into another document; synchronizes id/class via SetIdAttribute()/setAttribute(), which can EnsureUniqueElementData() (reallocating storage).
Re-entrant attribute mutation
If script invoked during WillModifyAttribute() mutates the same element’s attributes, previously-captured indices/references dangle.

Root Cause Analysis

The attribute mutators captured a position into attribute_vector_ and then called WillModifyAttribute(), which can run arbitrary script that re-indexes or reallocates that vector — leaving the captured reference/index dangling (use-after-free) or stale (wrong element / lost write). A prior CL (crrev.com/c/8510124) had already hardened SetAttributeInternal and RemoveAttributeInternal to re-validate index after WillModifyAttribute(). This CL closes the remaining gaps:

  1. AppendAttributeInternal unconditionally did EnsureUniqueElementData().Attributes().Append(name, value) after WillModifyAttribute(). If the re-entrant script already added the same attribute (the classic trigger: an img whose empty name attribute is modified → UpdateName → AddNamedItem → lazy WindowProxy init → page loadTimes setter adds attributes), the unconditional append created a duplicate attribute and used storage that script may have reallocated. The fix checks FindAttributeIndex(name); if the attribute now exists it calls .at(index).SetValue(value), otherwise appends.

  2. DidMoveToNewDocument held const AtomicString& id_attr = GetIdAttribute() (and the same for class_attr) — a reference into the element’s attribute storage — then called EnsureUniqueElementData() and SetIdAttribute(id_attr) / setAttribute(...), which can reallocate that storage, leaving id_attr/class_attr dangling and passing a freed AtomicString reference into the setter. The fix copies them by value (AtomicString id_attr = GetIdAttribute()), so the subsequent reallocation can’t dangle them.

  3. Comments in SetAttributeInternal/RemoveAttributeInternal are updated to note the new UpdateName()/WindowProxy script-execution path (not just the debugger).

The advisory’s “Memory corruption in DOM” is substantiated and the diff narrows it to a use-after-free / dangling-reference on UniqueElementData attribute storage across a re-entrant WillModifyAttribute() (plus a duplicate-attribute state corruption). The WPTs drive exactly this via a sandboxed same-origin iframe whose WindowProxy initializes during AddNamedItem, running a loadTimes setter that reallocates the element’s attribute vector mid-mutation.

Key insight
WillModifyAttribute() can synchronously run page script (debugger pause, or lazy WindowProxy init during UpdateName) that reallocates the element’s attribute vector, so any index or const AtomicString& into attribute storage captured before it — in AppendAttributeInternal and DidMoveToNewDocument — dangles and is used afterward.

Attack Path

  1. Set up a deferred script context Create a sandboxed allow-same-origin iframe so its script context stays uninitialized until named-item access (per the added WPT).
  2. Prepare the re-entrancy hook Name a child frame chrome so that when its WindowProxy initializes, content::GetOrCreateChromeObject() sets loadTimes on it; define a JS loadTimes setter that mutates the target element’s attributes (appends enough data-* to reallocate the vector, and/or removes earlier attributes).
  3. Trigger the vulnerable mutation Call img.setAttribute('name', 'target') (or adopt/append paths). This enters a mutator that captures an index/reference, then calls WillModifyAttribute() → UpdateName() → AddNamedItem → lazy WindowProxy init → the loadTimes setter.
  4. Reallocate mid-mutation The setter reallocates attribute_vector_, freeing the backing store the captured index/reference points at.
  5. Use freed/stale storage The mutator resumes: pre-patch AppendAttributeInternal appends into (or DidMoveToNewDocument passes) the dangling reference, yielding a use-after-free read/write on the attribute storage (and, in the append case, a duplicate name attribute). Converting this to controlled corruption/RCE is inferred, not shown by the diff.

Impact Assessment

Renderer-process, fully script-reachable from an ordinary web page (the WPT uses only a same-origin sandboxed iframe and the window.chrome/loadTimes lazy-init path — no DevTools or special privilege required). The primitive is a use-after-free / dangling-reference on heap-allocated UniqueElementData attribute storage during a re-entrant attribute mutation, plus an attribute-vector state-corruption (duplicate attribute) path. This is genuine memory corruption consistent with the advisory; escalation to a controlled read/write primitive or RCE is plausible for an attribute-vector UAF but is inferred, not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
for
third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
modified
set
third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
modified
if
third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
modified

Files Changed

  • third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
  • third_party/blink/renderer/core/dom/element.cc

Audit Directions

  • References/indices across `WillModifyAttribute()`
    Audit every caller that reads an AtomicString&/index/Attribute& from element data and then calls a function that can run script (WillModifyAttribute, UpdateName, AddNamedItem, debugger probes) before using it.
  • Lazy `WindowProxy` init side effects
    Review DOM mutation paths that can trigger LocalWindowProxy::Initialize → GetOrCreateChromeObject → page setters synchronously, treating them as arbitrary-script re-entry points.
  • `EnsureUniqueElementData()` reallocation
    Search for places holding a prior reference into attribute_vector_ across an EnsureUniqueElementData()/Append/Remove.
  • Adoption/`DidMoveToNewDocument` peers
    Check other document-move/clone synchronizers that re-set id/class/attributes from references into the source element’s storage.
From 3318e836e90b01f367be032759ad1368d7001bd3 Mon Sep 17 00:00:00 2001
From: Mason Freed <[email protected]>
Date: Mon, 26 Oct 2026 12:55:08 -0700
Subject: [PATCH] Harden Element attribute mutation methods against re-entrant changes

Element::WillModifyAttribute() can run script both by pausing in the
debugger (via probe::WillModifyDOMAttr) and when UpdateName() lazily
initializes a frame's WindowProxy and synchronously invokes a
page-defined setter (e.g. via chrome.loadTimes). If script mutates the
element's attributes and reallocates or re-indexes
UniqueElementData::attribute_vector_, references and indices into the
attribute storage acquired before WillModifyAttribute() become invalid.

While https://crrev.com/c/8510124 already hardened
SetAttributeInternal() and RemoveAttributeInternal(), this CL:
1. Checks in AppendAttributeInternal() whether WillModifyAttribute()
   already added the attribute re-entrantly before appending.
2. Copies id_attr and class_attr by value in DidMoveToNewDocument()
   before calling SetIdAttribute() / setAttribute().
3. Updates comments in SetAttributeInternal() and
   RemoveAttributeInternal() and adds regression tests.

Bug: 576203565, 575317938
Change-Id: Ic63c0b5a15568a9c8d2331e7b49dde89e36b5283
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8633017
Auto-Submit: Mason Freed <[email protected]>
Commit-Queue: Mason Freed <[email protected]>
Reviewed-by: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1727352}
---
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index 4d7b016..1e198ec 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -8490,7 +8490,8 @@
     DetachAttrNodeFromElementWithValue(attr_node, value_being_removed);
   }
 
-  // WillModifyAttribute() may pause in the debugger and allow script to mutate
+  // WillModifyAttribute() may run script (e.g. by pausing in the debugger or
+  // lazily initializing a WindowProxy during UpdateName()) and mutate
   // attributes, invalidating `index`.
   if (index >= mutable_attributes.size() ||
       mutable_attributes[index].GetName() != name) {
@@ -8519,7 +8520,15 @@
   }
   attribute_or_class_bloom_ |= FilterForAttribute(name);
   UpdateSubtreeBloomFilterAfterInsert();
-  EnsureUniqueElementData().Attributes().Append(name, value);
+  // WillModifyAttribute() may run script (e.g. by pausing in the debugger or
+  // lazily initializing a WindowProxy during UpdateName()) and add the
+  // attribute re-entrantly before we append it here.
+  if (wtf_size_t index = FindAttributeIndex(name); index != kNotFound)
+      [[unlikely]] {
+    EnsureUniqueElementData().Attributes().at(index).SetValue(value);
+  } else {
+    EnsureUniqueElementData().Attributes().Append(name, value);
+  }
   if (reason !=
       AttributeModificationReason::kBySynchronizationOfLazyAttribute) {
     DidAddAttribute(name, value);
@@ -12495,13 +12504,13 @@
     // this element's attributes, we shouldn't make UniqueElementData, and this
     // element should point to the shareable one.
 
-    if (const AtomicString& id_attr = GetIdAttribute()) {
+    if (AtomicString id_attr = GetIdAttribute()) {
       if (!id_attr.ContainsNoAsciiUpper()) {
         EnsureUniqueElementData();
         SetIdAttribute(id_attr);
       }
     }
-    if (const AtomicString& class_attr = GetClassAttribute()) {
+    if (AtomicString class_attr = GetClassAttribute()) {
       if (!class_attr.ContainsNoAsciiUpper()) {
         EnsureUniqueElementData();
         // Going through setAttribute() to synchronize the attribute is only
@@ -14132,8 +14141,9 @@
     }
     MutableAttributeCollection mutable_attributes =
         EnsureUniqueElementData().Attributes();
-    // WillModifyAttribute() may pause in the debugger and allow script to
-    // mutate attributes, invalidating `index`.
+    // WillModifyAttribute() may run script (e.g. by pausing in the debugger or
+    // lazily initializing a WindowProxy during UpdateName()) and mutate
+    // attributes, invalidating `index`.
     if (index >= mutable_attributes.size() ||
         mutable_attributes[index].GetName() != existing_attribute_name) {
       index = mutable_attributes.FindIndex(existing_attribute_name);
diff --git a/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html b/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
new file mode 100644
index 0000000..79195e1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:[email protected]">
+<link rel="help" href="https://crbug.com/575317938">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="container"></div>
+<script>
+test((t) => {
+  const container = document.getElementById('container');
+
+  // Create a sandboxed iframe without allow-scripts so its script context
+  // is not initialized until AddNamedItem accesses its WindowProxy.
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  // An empty name attribute exists in the attribute vector at index 0,
+  // but does not register a named item on insertion, leaving the sandboxed
+  // frame's WindowProxy uninitialized.
+  img.setAttribute('name', '');
+  for (let i = 0; i < 7; i++) {
+    img.setAttribute('data-a' + i, 'v');
+  }
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      for (let i = 7; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  // Modifying the existing 'name' attribute calls WillModifyAttribute ->
+  // UpdateName -> AddNamedItem, which initializes the WindowProxy and invokes
+  // the loadTimes setter above, reallocating img's attribute vector.
+  img.setAttribute('name', 'target_item');
+  assert_equals(img.getAttribute('name'), 'target_item');
+  assert_equals(innerDoc.target_item, img);
+}, 'Reallocating attributes during WillModifyAttribute in SetAttributeInternal should not dangle or lose the new value');
+
+test((t) => {
+  const container = document.getElementById('container');
+
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  img.setAttribute('data-first', '1');
+  img.setAttribute('name', '');
+  for (let i = 0; i < 7; i++) {
+    img.setAttribute('data-a' + i, 'v');
+  }
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  let setterRan = false;
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      setterRan = true;
+      // Remove an earlier attribute so 'name' shifts from index 1 to index 0,
+      // and append enough attributes to reallocate the vector.
+      img.removeAttribute('data-first');
+      for (let i = 7; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  img.setAttribute('name', 'shifted_item');
+  if (setterRan) {
+    assert_equals(img.getAttribute('data-first'), null);
+  }
+  assert_equals(img.getAttribute('name'), 'shifted_item');
+  assert_equals(img.getAttribute('data-a0'), 'v');
+  assert_equals(innerDoc.shifted_item, img);
+}, 'Removing earlier attributes during WillModifyAttribute in SetAttributeInternal should update the re-validated index');
+
+test((t) => {
+  const container = document.getElementById('container');
+
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      img.setAttribute('name', 'inner_name');
+      for (let i = 0; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  img.setAttribute('name', 'outer_name');
+  assert_equals(img.getAttribute('name'), 'outer_name');
+  let nameAttrCount = 0;
+  for (const attr of img.attributes) {
+    if (attr.name === 'name') {
+      nameAttrCount++;
+    }
+  }
+  assert_equals(nameAttrCount, 1);
+}, 'Setting the same attribute re-entrantly during AppendAttributeInternal should not create duplicate attributes');
+
+test((t) => {
+  const container = document.getElementById('container');
+  const quirksFrame = document.createElement('iframe');
+  container.appendChild(quirksFrame);
+  t.add_cleanup(() => quirksFrame.remove());
+
+  const quirksDoc = quirksFrame.contentDocument;
+  quirksDoc.open();
+  quirksDoc.write('<div id="MyUpperId" class="MyUpperClass"></div>');
+  quirksDoc.close();
+  assert_equals(quirksDoc.compatMode, 'BackCompat');
+
+  const div = quirksDoc.getElementById('MyUpperId');
+  document.adoptNode(div);
+  assert_equals(div.id, 'MyUpperId');
+  assert_equals(div.className, 'MyUpperClass');
+}, 'Adopting an element with uppercase id and class between quirks and standards mode documents');
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html b/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
new file mode 100644
index 0000000..79195e1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/dom/documents/dom-tree-accessors/nameditem-setattribute-attr-mutation.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:[email protected]">
+<link rel="help" href="https://crbug.com/575317938">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="container"></div>
+<script>
+test((t) => {
+  const container = document.getElementById('container');
+
+  // Create a sandboxed iframe without allow-scripts so its script context
+  // is not initialized until AddNamedItem accesses its WindowProxy.
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  // An empty name attribute exists in the attribute vector at index 0,
+  // but does not register a named item on insertion, leaving the sandboxed
+  // frame's WindowProxy uninitialized.
+  img.setAttribute('name', '');
+  for (let i = 0; i < 7; i++) {
+    img.setAttribute('data-a' + i, 'v');
+  }
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      for (let i = 7; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  // Modifying the existing 'name' attribute calls WillModifyAttribute ->
+  // UpdateName -> AddNamedItem, which initializes the WindowProxy and invokes
+  // the loadTimes setter above, reallocating img's attribute vector.
+  img.setAttribute('name', 'target_item');
+  assert_equals(img.getAttribute('name'), 'target_item');
+  assert_equals(innerDoc.target_item, img);
+}, 'Reallocating attributes during WillModifyAttribute in SetAttributeInternal should not dangle or lose the new value');
+
+test((t) => {
+  const container = document.getElementById('container');
+
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  img.setAttribute('data-first', '1');
+  img.setAttribute('name', '');
+  for (let i = 0; i < 7; i++) {
+    img.setAttribute('data-a' + i, 'v');
+  }
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  let setterRan = false;
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      setterRan = true;
+      // Remove an earlier attribute so 'name' shifts from index 1 to index 0,
+      // and append enough attributes to reallocate the vector.
+      img.removeAttribute('data-first');
+      for (let i = 7; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  img.setAttribute('name', 'shifted_item');
+  if (setterRan) {
+    assert_equals(img.getAttribute('data-first'), null);
+  }
+  assert_equals(img.getAttribute('name'), 'shifted_item');
+  assert_equals(img.getAttribute('data-a0'), 'v');
+  assert_equals(innerDoc.shifted_item, img);
+}, 'Removing earlier attributes during WillModifyAttribute in SetAttributeInternal should update the re-validated index');
+
+test((t) => {
+  const container = document.getElementById('container');
+
+  const sandbox = document.createElement('iframe');
+  sandbox.sandbox = 'allow-same-origin';
+  container.appendChild(sandbox);
+  t.add_cleanup(() => sandbox.remove());
+
+  const innerDoc = sandbox.contentDocument;
+  const img = innerDoc.createElement('img');
+  innerDoc.body.appendChild(img);
+
+  // Name the child frame 'chrome' so that when LocalWindowProxy::Initialize()
+  // runs DidCreateScriptContext, content::GetOrCreateChromeObject() looks up
+  // window.chrome on innerDoc's global, gets child.contentWindow via named
+  // frame access, and sets 'loadTimes' on it, invoking the JS setter below
+  // synchronously inside WillModifyAttribute().
+  const child = innerDoc.createElement('iframe');
+  child.name = 'chrome';
+  innerDoc.body.appendChild(child);
+
+  Object.defineProperty(child.contentWindow, 'loadTimes', {
+    configurable: true,
+    set() {
+      img.setAttribute('name', 'inner_name');
+      for (let i = 0; i < 20; i++) {
+        img.setAttribute('data-a' + i, 'v');
+      }
+    }
+  });
+
+  img.setAttribute('name', 'outer_name');
+  assert_equals(img.getAttribute('name'), 'outer_name');
+  let nameAttrCount = 0;
+  for (const attr of img.attributes) {
+    if (attr.name === 'name') {
+      nameAttrCount++;
+    }
+  }
+  assert_equals(nameAttrCount, 1);
+}, 'Setting the same attribute re-entrantly during AppendAttributeInternal should not create duplicate attributes');
+
+test((t) => {
+  const container = document.getElementById('container');
+  const quirksFrame = document.createElement('iframe');
+  container.appendChild(quirksFrame);
+  t.add_cleanup(() => quirksFrame.remove());
+
+  const quirksDoc = quirksFrame.contentDocument;
+  quirksDoc.open();
+  quirksDoc.write('<div id="MyUpperId" class="MyUpperClass"></div>');
+  quirksDoc.close();
+  assert_equals(quirksDoc.compatMode, 'BackCompat');
+
+  const div = quirksDoc.getElementById('MyUpperId');
+  document.adoptNode(div);
+  assert_equals(div.id, 'MyUpperId');
+  assert_equals(div.className, 'MyUpperClass');
+}, 'Adopting an element with uppercase id and class between quirks and standards mode documents');
+</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.