High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Command
DescriptionImproper input validation in Command
ComponentCommand
Sub-componentEditing
Bug ClassUAF
Tracker570638833
Fix commit278621c8ed4f (chromium/src) +99/-3
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

Background

`Element::FastGetAttribute`
Returns const AtomicString& — a reference into the element’s attribute storage (the backing AttributeVector), not an owning copy.
`AttributeVector` storage
An element’s attributes live inline up to a small capacity (4) and then in a heap buffer; appending attributes past capacity reallocates and frees the old buffer.
`Element::setAttribute`
Setting the id attribute notifies id observers (IdTargetObserver/IdTargetObserverRegistry), which can synchronously run script / style-attribute synchronization that mutates the source element’s attributes.

Root Cause Analysis

The advisory labels this “Improper input validation in Command,” but the diff substantiates a use-after-free of an attribute-value reference. In SplitElementCommand::DoUnapply(), the code recovered the original id with const AtomicString& id = element1_->FastGetAttribute(html_names::kIdAttr); — binding a reference that points directly into element1_’s attribute storage. It then called element2_->setAttribute(html_names::kIdAttr, id). Setting the id attribute notifies id observers, and those observers can run synchronous work (script, or lazy style-attribute synchronization driven by the style/layout update on an id change) that mutates element1_’s attributes. If that mutation appends attributes past the inline/heap-buffer capacity, the AttributeVector backing store is reallocated and the old buffer freed — leaving the id reference dangling. The value is then consumed by setAttribute from freed memory: a read-use-after-free of attacker-influenced attribute storage.

The patch changes a single character of ownership: const AtomicString& id becomes const AtomicString id — an owning AtomicString copy taken before the setAttribute call. Copying bumps the string’s refcount so the value survives any reallocation of element1_’s attribute storage during the id-observer notification. The comment explicitly notes this must be a copy, not a reference, and that the fix matches the ownership already used in Element::InsertedInto()/Element::RemovedFrom(). The new SplitElementCommandTest.UndoRestoresIdWhileAttributesAreReallocated test (with the AttributeReallocatingIdObserver helper that appends 8 padding attributes on IdTargetChanged()) is ASan-only, confirming the pre-fix code reads freed memory.

Key insight
The undo held a const AtomicString& reference into one element’s attribute storage across a setAttribute() that notifies id observers, and those observers can reallocate that very storage — freeing the buffer the reference pointed into before its value is used.

Attack Path

  1. Set up an editable split On a contenteditable region, build an element with enough attributes that its AttributeVector lives in a reallocatable heap buffer (the test uses id plus a..e, exceeding inline capacity of 4).
  2. Register a reacting id observer Arrange an id observer (in production: the style/layout update on an id change synchronizing the source element’s lazy style attribute; in the test, a custom IdTargetObserver) that mutates the source element’s attributes when the id changes.
  3. Perform then undo the split Execute SplitElementCommand and trigger its undo (DoUnapply), entering the id-recovery code that binds const AtomicString& id.
  4. Reallocate during notify element2_->setAttribute(kIdAttr, id) fires the id observer, which appends attributes to the source element, overflowing capacity and freeing the old attribute buffer.
  5. UAF read setAttribute consumes id from the freed buffer, a use-after-free read in the renderer. (Controlling the reclaimed contents / escalation is inferred, not shown.)

Impact Assessment

The defect is in the renderer process and reachable from untrusted web content: editing undo on a contenteditable region is script/user-triggerable, and id-observer notification during setAttribute is a normal DOM side effect, so no special privilege is needed. The proven primitive is a heap use-after-free read of an AtomicString whose backing attribute buffer was freed mid-operation; the ASan-only regression test confirms the freed-memory access. Turning the stale read into a controlled information leak or memory corruption is (inferred) and not demonstrated. The advisory’s “input validation” class is not substantiated — the code shows a lifetime/UAF bug.

Changed Functions

FunctionChangeNotes
IdTargetObserverRegistry
third_party/blink/renderer/core/dom/id_target_observer.h
modified
IdTargetObserver
third_party/blink/renderer/core/dom/id_target_observer.h
modified
CORE_EXPORT
third_party/blink/renderer/core/dom/id_target_observer.h
modified
SplitElementCommandTest
third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
modified
AttributeReallocatingIdObserver
third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
modified
for
third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
modified
TEST_F
third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
  • third_party/blink/renderer/core/editing/commands/split_element_command.cc
  • third_party/blink/renderer/core/dom/id_target_observer.h

Audit Directions

  • `FastGetAttribute`/`getAttribute` reference lifetimes
    Grep Blink for const AtomicString& x = ...FastGetAttribute(...) (or other attribute accessors) held across a setAttribute/removeAttribute/notification that could mutate the same element’s attributes; each should take an owning copy.
  • Editing command undo/redo paths
    Audit other SimpleEditCommand/CompositeEditCommand DoApply/DoUnapply methods that cache attribute or node references across DOM-mutating calls.
  • Id/attribute observer reentrancy
    Review code that reads an attribute value immediately before a call that notifies IdTargetObserverRegistry or runs style-attribute synchronization.
From 278621c8ed4f9a399cf219a1acc98683ae9afb72 Mon Sep 17 00:00:00 2001
From: Leo Lee <[email protected]>
Date: Thu, 15 Oct 2026 14:25:22 -0700
Subject: [PATCH] [Editing] Copy id attribute when undoing a split

Undoing a split recovered the original element's id by holding a
reference returned from FastGetAttribute(). That reference points into
the source element's attribute storage, which the following
setAttribute() call can reallocate: setting the id notifies id
observers, which may run script that mutates the source element's
attributes before the value is consumed.

Hold an owning AtomicString instead. This matches the ownership already
used in Element::InsertedInto() and Element::RemovedFrom().

Bug: 570638833, 572546688
Change-Id: Iccbd5b7da12a64cd4f97cbc765bec4b9a37a1b86
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8564737
Reviewed-by: Dan Clark <[email protected]>
Commit-Queue: Leo Lee <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1720011}
---
diff --git a/third_party/blink/renderer/core/dom/id_target_observer.h b/third_party/blink/renderer/core/dom/id_target_observer.h
index 048c08a..ec1e8f8 100644
--- a/third_party/blink/renderer/core/dom/id_target_observer.h
+++ b/third_party/blink/renderer/core/dom/id_target_observer.h
@@ -26,6 +26,7 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_DOM_ID_TARGET_OBSERVER_H_
 
+#include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/member.h"
 #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
@@ -34,7 +35,7 @@
 
 class IdTargetObserverRegistry;
 
-class IdTargetObserver : public GarbageCollected<IdTargetObserver> {
+class CORE_EXPORT IdTargetObserver : public GarbageCollected<IdTargetObserver> {
  public:
   virtual ~IdTargetObserver();
   virtual void Trace(Visitor*) const;
diff --git a/third_party/blink/renderer/core/editing/commands/split_element_command.cc b/third_party/blink/renderer/core/editing/commands/split_element_command.cc
index 7c5670f9..d96054c9 100644
--- a/third_party/blink/renderer/core/editing/commands/split_element_command.cc
+++ b/third_party/blink/renderer/core/editing/commands/split_element_command.cc
@@ -90,8 +90,10 @@
   for (const auto& child : children)
     element2_->InsertBefore(child, ref_child, IGNORE_EXCEPTION_FOR_TESTING);
 
-  // Recover the id attribute of the original element.
-  const AtomicString& id = element1_->FastGetAttribute(html_names::kIdAttr);
+  // Recover the id attribute of the original element. This must be a copy,
+  // not a reference: setAttribute() below notifies id observers, which can
+  // reallocate `element1_`'s attribute storage.
+  const AtomicString id = element1_->FastGetAttribute(html_names::kIdAttr);
   if (!id.IsNull())
     element2_->setAttribute(html_names::kIdAttr, id);
 
diff --git a/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc b/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
index 18385f7..6b84e65c 100644
--- a/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
+++ b/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
@@ -4,13 +4,53 @@
 
 #include "third_party/blink/renderer/core/editing/commands/split_element_command.h"
 
+#include "third_party/blink/renderer/core/dom/id_target_observer.h"
+#include "third_party/blink/renderer/core/dom/id_target_observer_registry.h"
 #include "third_party/blink/renderer/core/editing/commands/editing_state.h"
 #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html_names.h"
 
 namespace blink {
 
 class SplitElementCommandTest : public EditingTestBase {};
 
+namespace {
+
+// Appends attributes to `element` while an id change is being dispatched,
+// forcing its attribute storage to reallocate. This stands in for what
+// production does on this path: the id notification updates style and layout,
+// which synchronizes the source element's lazy style attribute and appends to
+// the same storage. Either way, any reference into that storage held by the
+// code dispatching the id change is invalidated.
+class AttributeReallocatingIdObserver final : public IdTargetObserver {
+ public:
+  AttributeReallocatingIdObserver(IdTargetObserverRegistry& registry,
+                                  const AtomicString& id,
+                                  Element* element)
+      : IdTargetObserver(registry, id), element_(element) {}
+
+  void IdTargetChanged() override {
+    // Enough appends to grow the attribute vector past its current capacity at
+    // least once, which frees the buffer it was using.
+    for (unsigned i = 0; i < 8; ++i) {
+      element_->setAttribute(
+          QualifiedName(g_null_atom, AtomicString(String::Number(i) + "pad"),
+                        g_null_atom),
+          AtomicString("1"));
+    }
+  }
+
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(element_);
+    IdTargetObserver::Trace(visitor);
+  }
+
+ private:
+  Member<Element> element_;
+};
+
+}  // namespace
+
 // Tests that SplitElementCommand works. It splits an element at the
 // passed child.
 TEST_F(SplitElementCommandTest, Basic) {
@@ -123,4 +163,57 @@
   EXPECT_EQ(4u, firstChildAfterRedo->CountChildren());
 }
 
+// Undoing a split restores the original element's id onto the element that the
+// split took it from. Dispatching that id change can run code that reallocates
+// the cloned element's attribute storage, so the undo must not hold a reference
+// into that storage while it does so. Regression test for crbug.com/572546688.
+//
+// Only meaningful under ASan: without the fix this reads freed memory, which an
+// unsanitized build will not notice.
+TEST_F(SplitElementCommandTest, UndoRestoresIdWhileAttributesAreReallocated) {
+  // The padding attributes put the element over AttributeVector's inline
+  // capacity of 4, so that its attributes live in a heap buffer that appending
+  // more attributes can reallocate.
+  SetBodyContent(
+      "<div contenteditable><blockquote id=split a=1 b=2 c=3 d=4 e=5>"
+      "a<br>b<br></blockquote></div>");
+  auto* div = To<ContainerNode>(GetDocument().body()->firstChild());
+  auto* blockquote = To<Element>(div->firstChild());
+  Node* at_child = blockquote->childNodes()->item(2);
+
+  SimpleEditCommand* command =
+      MakeGarbageCollected<SplitElementCommand>(blockquote, at_child);
+  EditingState editing_state;
+  command->DoApply(&editing_state);
+
+  // The split moved the id onto a clone inserted before `blockquote`.
+  auto* clone = To<Element>(div->firstChild());
+  ASSERT_NE(clone, blockquote);
+  ASSERT_EQ(AtomicString("split"),
+            clone->FastGetAttribute(html_names::kIdAttr));
+  ASSERT_TRUE(blockquote->FastGetAttribute(html_names::kIdAttr).IsNull());
+
+  // Move the clone off the shared, immutable attribute storage it was cloned
+  // with and onto a mutable, heap-allocated attribute vector.
+  clone->setAttribute(
+      QualifiedName(g_null_atom, AtomicString("f"), g_null_atom),
+      AtomicString("6"));
+
+  // Give `blockquote` an id of its own, so that restoring the original id
+  // replaces a value rather than adding one.
+  blockquote->setAttribute(html_names::kIdAttr, AtomicString("other"));
+
+  // Restoring the id notifies this observer, which reallocates the clone's
+  // attributes while the undo is still reading the id out of them.
+  MakeGarbageCollected<AttributeReallocatingIdObserver>(
+      GetDocument().EnsureIdTargetObserverRegistry(), AtomicString("split"),
+      clone);
+
+  command->DoUnapply();
+
+  EXPECT_EQ(AtomicString("split"),
+            blockquote->FastGetAttribute(html_names::kIdAttr));
+  EXPECT_EQ(1u, div->CountChildren());
+}
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc b/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
index 18385f7..6b84e65c 100644
--- a/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
+++ b/third_party/blink/renderer/core/editing/commands/split_element_command_test.cc
@@ -4,13 +4,53 @@
 
 #include "third_party/blink/renderer/core/editing/commands/split_element_command.h"
 
+#include "third_party/blink/renderer/core/dom/id_target_observer.h"
+#include "third_party/blink/renderer/core/dom/id_target_observer_registry.h"
 #include "third_party/blink/renderer/core/editing/commands/editing_state.h"
 #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html_names.h"
 
 namespace blink {
 
 class SplitElementCommandTest : public EditingTestBase {};
 
+namespace {
+
+// Appends attributes to `element` while an id change is being dispatched,
+// forcing its attribute storage to reallocate. This stands in for what
+// production does on this path: the id notification updates style and layout,
+// which synchronizes the source element's lazy style attribute and appends to
+// the same storage. Either way, any reference into that storage held by the
+// code dispatching the id change is invalidated.
+class AttributeReallocatingIdObserver final : public IdTargetObserver {
+ public:
+  AttributeReallocatingIdObserver(IdTargetObserverRegistry& registry,
+                                  const AtomicString& id,
+                                  Element* element)
+      : IdTargetObserver(registry, id), element_(element) {}
+
+  void IdTargetChanged() override {
+    // Enough appends to grow the attribute vector past its current capacity at
+    // least once, which frees the buffer it was using.
+    for (unsigned i = 0; i < 8; ++i) {
+      element_->setAttribute(
+          QualifiedName(g_null_atom, AtomicString(String::Number(i) + "pad"),
+                        g_null_atom),
+          AtomicString("1"));
+    }
+  }
+
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(element_);
+    IdTargetObserver::Trace(visitor);
+  }
+
+ private:
+  Member<Element> element_;
+};
+
+}  // namespace
+
 // Tests that SplitElementCommand works. It splits an element at the
 // passed child.
 TEST_F(SplitElementCommandTest, Basic) {
@@ -123,4 +163,57 @@
   EXPECT_EQ(4u, firstChildAfterRedo->CountChildren());
 }
 
+// Undoing a split restores the original element's id onto the element that the
+// split took it from. Dispatching that id change can run code that reallocates
+// the cloned element's attribute storage, so the undo must not hold a reference
+// into that storage while it does so. Regression test for crbug.com/572546688.
+//
+// Only meaningful under ASan: without the fix this reads freed memory, which an
+// unsanitized build will not notice.
+TEST_F(SplitElementCommandTest, UndoRestoresIdWhileAttributesAreReallocated) {
+  // The padding attributes put the element over AttributeVector's inline
+  // capacity of 4, so that its attributes live in a heap buffer that appending
+  // more attributes can reallocate.
+  SetBodyContent(
+      "<div contenteditable><blockquote id=split a=1 b=2 c=3 d=4 e=5>"
+      "a<br>b<br></blockquote></div>");
+  auto* div = To<ContainerNode>(GetDocument().body()->firstChild());
+  auto* blockquote = To<Element>(div->firstChild());
+  Node* at_child = blockquote->childNodes()->item(2);
+
+  SimpleEditCommand* command =
+      MakeGarbageCollected<SplitElementCommand>(blockquote, at_child);
+  EditingState editing_state;
+  command->DoApply(&editing_state);
+
+  // The split moved the id onto a clone inserted before `blockquote`.
+  auto* clone = To<Element>(div->firstChild());
+  ASSERT_NE(clone, blockquote);
+  ASSERT_EQ(AtomicString("split"),
+            clone->FastGetAttribute(html_names::kIdAttr));
+  ASSERT_TRUE(blockquote->FastGetAttribute(html_names::kIdAttr).IsNull());
+
+  // Move the clone off the shared, immutable attribute storage it was cloned
+  // with and onto a mutable, heap-allocated attribute vector.
+  clone->setAttribute(
+      QualifiedName(g_null_atom, AtomicString("f"), g_null_atom),
+      AtomicString("6"));
+
+  // Give `blockquote` an id of its own, so that restoring the original id
+  // replaces a value rather than adding one.
+  blockquote->setAttribute(html_names::kIdAttr, AtomicString("other"));
+
+  // Restoring the id notifies this observer, which reallocates the clone's
+  // attributes while the undo is still reading the id out of them.
+  MakeGarbageCollected<AttributeReallocatingIdObserver>(
+      GetDocument().EnsureIdTargetObserverRegistry(), AtomicString("split"),
+      clone);
+
+  command->DoUnapply();
+
+  EXPECT_EQ(AtomicString("split"),
+            blockquote->FastGetAttribute(html_names::kIdAttr));
+  EXPECT_EQ(1u, div->CountChildren());
+}
+
 }  // namespace blink
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.