Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in SVG
DescriptionUse after free in SVG
ComponentSVG
Bug ClassUAF
Tracker496284584
Fix commit122679a5ea41 (chromium/src) +2/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/core/svg/svg_element.cc
modified

Files Changed

  • third_party/blink/renderer/core/svg/svg_element.cc
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);
   }
 }
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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.

  1. Create an <svg id="target" tabindex="0"> element.
  2. Create a <use href="#target"> element, which instantiates a shadow instance.
  3. Register a blur event listener on the window (capturing phase).
  4. Programmatically or manually focus the rendered <use> element (the shadow instance inherits the tabindex).
  5. Use JavaScript to remove the tabindex from the target: document.getElementById('target').removeAttribute('tabindex');.
  6. The SynchronizeAttributeInShadowInstances loop begins in C++.
  7. The tabindex removal is synchronized to the shadow instance, synchronously triggering the blur event.
  8. In the JS blur handler, append many new <use href="#target"> elements to the DOM and force a synchronous layout (e.g., by reading offsetTop) to ensure the shadow trees are built and added to InstancesForElement().
  9. Still in the JS handler, allocate many objects of the same size as the backing store to reclaim the explicitly freed memory.
  10. 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.

View on issue tracker