CVE-2026-7906
Overview
Background
- SVG use/shadow instances
- an SVG
<use>element clones a referenced element into a shadow tree, and Blink keeps the set of these cloned instances so attribute changes on the original can be mirrored to each clone. - `InstancesForElement()`
- returns the live collection of an element’s shadow instances as a
HeapHashSet<WeakMember<SVGElement>>, i.e. a set of garbage-collector-tracked weak pointers toSVGElements. - `WeakMember`
- an Oilpan reference that does not keep its target alive and is cleared when the target is collected, meaning entries in the set can disappear if a GC or DOM mutation occurs mid-use.
- `SetAttributeWithoutValidation()`
- applies an attribute value to an element, which can run further synchronization, DOM mutation, or script-observable side effects.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Commit is labelled a speculative fix; the diff only swaps a const reference for a copied HeapHashSet snapshot. No reentrant path that mutates InstancesForElement() or frees an instance is shown, yet the claim states it as fact.
SVGElement::SynchronizeAttributeInShadowInstances bound a const reference directly to the live set returned by InstancesForElement() and iterated it in place, calling instance->SetAttributeWithoutValidation(name, value) on each entry. Because the loop body mutates DOM state, it can re-enter code that modifies the very instance collection being iterated — adding or removing shadow instances, or clearing WeakMember entries — which invalidates the iterator or frees an element while the loop still references it, the classic use-after-free / iterator-invalidation pattern. The commit, described as a “speculative fix,” replaces the reference with a by-value copy, HeapHashSet<WeakMember<SVGElement>> instances = InstancesForElement();, and iterates that snapshot so the loop is no longer affected by mutations to the underlying collection.
The fix works because the copy decouples the iteration from any reentrant modification of the element’s live instance set.
SetAttributeWithoutValidation) could reentrantly modify that same collection; copying the set into a local snapshot before iterating removes the aliasing and makes the traversal safe.Attack Path
- Set up `<use>`-based shadow instances
A page constructs an SVG element referenced by one or more
<use>elements so it has a non-empty shadow-instance set tracked byInstancesForElement(). - Trigger attribute synchronization
An attribute change on the original element causes
SynchronizeAttributeInShadowInstancesto iterate the live instance set. - Reenter and mutate the set
Applying the attribute via
SetAttributeWithoutValidationon an instance causes further DOM mutation or synchronization that modifies or collects entries of the same instance collection during iteration (inferred mechanism; the commit calls it a “potential issue”). - Use the stale reference
The still-in-progress loop continues over an invalidated iterator or a freed
SVGElement, yielding a use-after-free consistent with the advisory’s “use after free in SVG.”
Impact Assessment
<use> instance synchronization, cause iteration over a collection that is mutated mid-loop, leading to a use-after-free in the renderer process; the advisory rates this high severity and does not report in-the-wild exploitation. The patch is explicitly a speculative fix and does not demonstrate a concrete controlled read/write or any escalation beyond the UAF, so exploitation primitives are inferred rather than established. The precondition is a document with SVG <use> shadow instances whose attribute synchronization can be driven from script or markup.Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/core/svg/svg_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/svg/svg_element.cc
Audit Directions
- Reference-iterating a live Heap collectionsearch for
const HeapHashSet<...>&/const HeapVector<...>&(or similar) bound to a getter’s return value and iterated while the loop body makes DOM, style, or attribute mutations that could modify that same collection. - `WeakMember` iteration with side-effecting bodiesflag loops over
HeapHashSet<WeakMember<...>>where the body can trigger GC or object removal, since weak entries may be cleared mid-traversal; copying or snapshotting is the safe pattern. - Synchronization/mirroring routinesreview other SVG and shadow-tree synchronization functions that fan an operation out across an element’s instance or observer lists while allowing reentrancy into list-modifying code.
Patch
From 122679a5ea41c567989005e90ae0bcd27d590b95 Mon Sep 17 00:00:00 2001 From: Kalvin Lee <[email protected]> Date: Thu, 26 Mar 2026 21:29:40 -0700 Subject: [PATCH] Terracotta-Phase-1: Copy vector in `SVGElement::SynchronizeAttributeInShadowInstances()` Speculative fix for potential issue (more context in the bug). Bug: 496284584 Change-Id: I4906a3269afe37aec524f36cd7aa64327327f9c5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7703714 Commit-Queue: Kalvin Lee <[email protected]> Reviewed-by: Keishi Hattori <[email protected]> Cr-Commit-Position: refs/heads/main@{#1605966} --- diff --git a/third_party/blink/renderer/core/svg/svg_element.cc b/third_party/blink/renderer/core/svg/svg_element.cc index 611df88b..be0394d 100644 --- a/third_party/blink/renderer/core/svg/svg_element.cc +++ b/third_party/blink/renderer/core/svg/svg_element.cc @@ -1166,8 +1166,8 @@ void SVGElement::SynchronizeAttributeInShadowInstances( const QualifiedName& name, const AtomicString& value) { - const HeapHashSet<WeakMember<SVGElement>>& set = InstancesForElement(); - for (SVGElement* instance : set) { + HeapHashSet<WeakMember<SVGElement>> instances = InstancesForElement(); + for (SVGElement* instance : instances) { instance->SetAttributeWithoutValidation(name, value); } }
Original Bug Report
Use-After-Free in SVGElement::SynchronizeAttributeInShadowInstances via sync blur
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Iterating over InstancesForElement() in SVGElement::SynchronizeAttributeInShadowInstances uses an unprotected iterator. Synchronizing the removal of a tabindex attribute can trigger a synchronous blur event, allowing JavaScript to mutate the instances set, rehash its backing store, and cause a Use-After-Free when the loop resumes.
Affected files:
third_party/blink/renderer/core/svg/svg_element.ccthird_party/blink/renderer/core/svg/svg_svg_element.cc
Estimated timestamp from git blame: 2025-10-08
Summary
A potential highly reliable Use-After-Free (UAF) exists in SVGElement::SynchronizeAttributeInShadowInstances within Blink’s SVG implementation. The vulnerability is caused by iterating over a live HeapHashSet (InstancesForElement()) without snapshotting it. A synchronous DOM event (blur) can be triggered during this iteration, allowing an attacker to execute JavaScript, mutate the HeapHashSet, and force a rehash. This explicitly frees the collection’s backing store, leaving the C++ iterator with a dangling pointer that is subsequently dereferenced.
Vulnerability Details
The vulnerable method is SVGElement::SynchronizeAttributeInShadowInstances:
void SVGElement::SynchronizeAttributeInShadowInstances(
const QualifiedName& name,
const AtomicString& value) {
const HeapHashSet<WeakMember<SVGElement>>& set = InstancesForElement();
for (SVGElement* instance : set) {
instance->SetAttributeWithoutValidation(name, value);
}
}
The range-based for loop uses a HashTableConstIterator which holds a raw pointer directly into the HeapHashSet’s contiguous backing store array. Concurrent modification checks (container_modifications_ == container_->Modifications()) exist but are guarded by DCHECK_IS_ON() and are entirely compiled out in Release builds.
When instance->SetAttributeWithoutValidation is called to synchronize the removal of the tabindex attribute (value is null), it propagates down to Element::AttributeChanged. If the shadow instance is currently focused, removing tabindex makes it unfocusable (!IsFocusable()), which causes Element::AttributeChanged to synchronously call blur().
The blur() method synchronously dispatches a blur event. An attacker can attach an event listener to capture this event, suspending the C++ iteration and executing arbitrary JavaScript.
Inside the JavaScript handler, the attacker can add new <use> elements to the DOM that reference the original <svg> target. This causes SVGElement::AddInstance to be called, inserting new instances into the HeapHashSet. If enough elements are added to exceed the load factor, the HeapHashSet performs a rehash.
During rehashing, the old backing store is explicitly freed via cppgc::subtle::FreeUnreferencedObject. The attacker can then use standard Oilpan heap spraying techniques within the same JavaScript execution to reclaim the freed backing store memory and populate it with fake SVGElement pointers.
When the JavaScript handler finishes and control returns to the C++ for loop, the iterator advances using its dangling pointer. It retrieves an attacker-controlled SVGElement* and calls instance->SetAttributeWithoutValidation, leading to virtual method calls on the fake object and granting arbitrary Remote Code Execution (RCE) in the renderer process.
Potential Trigger Steps
Note: This sequence has been formulated by an AI agent and represents a theoretical trigger path; no working PoC has been executed yet.
- Create an
<svg id="target" tabindex="0">element. - Create a
<use href="#target">element, which instantiates a shadow instance. - Register a
blurevent listener on thewindow(capturing phase). - Programmatically or manually focus the rendered
<use>element (the shadow instance inherits thetabindex). - Use JavaScript to remove the
tabindexfrom the target:document.getElementById('target').removeAttribute('tabindex');. - The
SynchronizeAttributeInShadowInstancesloop begins in C++. - The
tabindexremoval is synchronized to the shadow instance, synchronously triggering theblurevent. - In the JS
blurhandler, append many new<use href="#target">elements to the DOM and force a synchronous layout (e.g., by readingoffsetTop) to ensure the shadow trees are built and added toInstancesForElement(). - Still in the JS handler, allocate many objects of the same size as the backing store to reclaim the explicitly freed memory.
- The JS handler returns, the C++ loop resumes, and the UAF is triggered.
Suggested Fix
Do not iterate directly over the live HeapHashSet if the loop body can execute script. Instead, snapshot the collection into a local HeapVector before iterating:
void SVGElement::SynchronizeAttributeInShadowInstances(
const QualifiedName& name,
const AtomicString& value) {
HeapVector<Member<SVGElement>> instances;
CopyToVector(InstancesForElement(), instances);
for (SVGElement* instance : instances) {
if (instance) {
instance->SetAttributeWithoutValidation(name, value);
}
}
}
Evaluated with Chrome root at commit: a3f5fcb392f2902650ca2b71820e7e418787e18b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.