CVE-2026-7906
Overview
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
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.