Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactObject lifecycle issue in SVG
DescriptionObject lifecycle issue in SVG
ComponentSVG
Bug ClassLogic Error
Tracker517309206
Fix commit7a5040845ca3 (chromium/src) +101/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/svg/graphics/svg_image.cc
modified
Document
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
Element
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
ExternalSVGResourceImageContent
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
IsolatedSVGDocumentHost
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
LayoutSVGRoot
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
LocalFrame
third_party/blink/renderer/core/svg/graphics/svg_image.h
modified
if
third_party/blink/renderer/core/svg/svg_resource.cc
modified

Files Changed

  • third_party/blink/renderer/core/svg/graphics/svg_image.cc
  • third_party/blink/renderer/core/svg/graphics/svg_image.h
  • third_party/blink/renderer/core/svg/svg_resource.cc
  • third_party/blink/renderer/core/svg/svg_resource.h
  • third_party/blink/renderer/core/svg/svg_resource_document_content.cc
From 7a5040845ca31241a97e5aff86df1000ac1c6c67 Mon Sep 17 00:00:00 2001
From: Fredrik Söderquist <[email protected]>
Date: Tue, 02 Jun 2026 04:16:30 -0700
Subject: [PATCH] Ensure the lifecycle is updated for external SVG resources

In some cases, the external SVG resource document can be invalidated,
and the elements referring would end up using potentially dirty layout
state.

Add a UpdateContentLifecycleForUse() hook to SVGResource and call that
before each access to the target element. Implement
UpdateContentLifecycleForUse() for the different types of resources
(local, svg resource document content and image content).

Fixed: 517309206
Change-Id: I341249b259de3d10d363039dc901054d10b07786
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7880227
Reviewed-by: Philip Rogers <[email protected]>
Commit-Queue: Fredrik Söderquist <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1640064}
---

diff --git a/third_party/blink/renderer/core/svg/graphics/svg_image.cc b/third_party/blink/renderer/core/svg/graphics/svg_image.cc
index 54ce780a3..b193791 100644
--- a/third_party/blink/renderer/core/svg/graphics/svg_image.cc
+++ b/third_party/blink/renderer/core/svg/graphics/svg_image.cc
@@ -694,13 +694,27 @@
   }
 }
 
-Element* SVGImage::GetResourceElement(const AtomicString& id) const {
+Element* SVGImage::GetResourceElement(
+    base::PassKey<ExternalSVGResourceImageContent>,
+    const AtomicString& id) const {
   if (!document_host_) {
     return nullptr;
   }
   return GetFrame()->GetDocument()->getElementById(id);
 }
 
+void SVGImage::UpdateLifecycleForUse(
+    base::PassKey<ExternalSVGResourceImageContent>) {
+  if (!document_host_) {
+    return;
+  }
+  // Temporarily disable change notifications triggered by the lifecycle
+  // update.
+  ImageObserverDisabler disable_image_observer(this);
+  GetFrame()->View()->UpdateAllLifecyclePhasesExceptPaint(
+      DocumentUpdateReason::kSVGImage);
+}
+
 void SVGImage::NotifyAsyncLoadCompleted() {
   if (GetImageObserver())
     GetImageObserver()->AsyncLoadCompleted(this);
diff --git a/third_party/blink/renderer/core/svg/graphics/svg_image.h b/third_party/blink/renderer/core/svg/graphics/svg_image.h
index 660c73c..ad54ab6e 100644
--- a/third_party/blink/renderer/core/svg/graphics/svg_image.h
+++ b/third_party/blink/renderer/core/svg/graphics/svg_image.h
@@ -31,6 +31,7 @@
 
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
+#include "base/types/pass_key.h"
 #include "third_party/blink/public/mojom/css/preferred_color_scheme.mojom-blink-forward.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/platform/geometry/physical_size.h"
@@ -48,6 +49,7 @@
 
 class Document;
 class Element;
+class ExternalSVGResourceImageContent;
 class IsolatedSVGDocumentHost;
 class LayoutSVGRoot;
 class LocalFrame;
@@ -135,8 +137,10 @@
   void SetPreferredColorScheme(
       mojom::blink::PreferredColorScheme preferred_color_scheme);
 
-  // Introspective service hatch for mask-image. Don't abuse for anything else.
-  Element* GetResourceElement(const AtomicString& id) const;
+  // Specialized interface for mask-image (via ExternalSVGResourceImageContent).
+  Element* GetResourceElement(base::PassKey<ExternalSVGResourceImageContent>,
+                              const AtomicString& id) const;
+  void UpdateLifecycleForUse(base::PassKey<ExternalSVGResourceImageContent>);
 
  protected:
   // Whether or not size is available yet.
diff --git a/third_party/blink/renderer/core/svg/svg_resource.cc b/third_party/blink/renderer/core/svg/svg_resource.cc
index 4803521..5c73b0ac 100644
--- a/third_party/blink/renderer/core/svg/svg_resource.cc
+++ b/third_party/blink/renderer/core/svg/svg_resource.cc
@@ -122,10 +122,17 @@
     client->ResourceContentChanged(this);
 }
 
+Element* SVGResource::Target() const {
+  UpdateContentLifecycleForUse();
+  return target_.Get();
+}
+
 LayoutSVGResourceContainer* SVGResource::ResourceContainerNoCycleCheck() const {
-  if (!target_)
+  Element* target = Target();
+  if (!target) {
     return nullptr;
-  return DynamicTo<LayoutSVGResourceContainer>(target_->GetLayoutObject());
+  }
+  return DynamicTo<LayoutSVGResourceContainer>(target->GetLayoutObject());
 }
 
 LayoutSVGResourceContainer* SVGResource::ResourceContainer(
@@ -342,6 +349,13 @@
   return external_document->getElementById(decoded_fragment);
 }
 
+void ExternalSVGResourceDocumentContent::UpdateContentLifecycleForUse() const {
+  if (!document_content_) {
+    return;
+  }
+  document_content_->UpdateLifecycleForUse();
+}
+
 void ExternalSVGResourceDocumentContent::Trace(Visitor* visitor) const {
   visitor->Trace(document_content_);
   SVGResource::Trace(visitor);
@@ -376,7 +390,17 @@
   }
   AtomicString decoded_fragment(
       DecodeUrlEscapeSequences(fragment_, DecodeUrlMode::kUtf8OrIsomorphic));
-  return svg_image->GetResourceElement(decoded_fragment);
+  return svg_image->GetResourceElement(
+      base::PassKey<ExternalSVGResourceImageContent>(), decoded_fragment);
+}
+
+void ExternalSVGResourceImageContent::UpdateContentLifecycleForUse() const {
+  auto* svg_image = DynamicTo<SVGImage>(image_content_->GetImage());
+  if (!svg_image) {
+    return;
+  }
+  svg_image->UpdateLifecycleForUse(
+      base::PassKey<ExternalSVGResourceImageContent>());
 }
 
 void ExternalSVGResourceImageContent::ImageNotifyFinished(
diff --git a/third_party/blink/renderer/core/svg/svg_resource.h b/third_party/blink/renderer/core/svg/svg_resource.h
index a9d15dfb..7f5bc2e 100644
--- a/third_party/blink/renderer/core/svg/svg_resource.h
+++ b/third_party/blink/renderer/core/svg/svg_resource.h
@@ -78,7 +78,7 @@
 
   virtual bool IsLoading() const { return false; }
 
-  Element* Target() const { return target_.Get(); }
+  Element* Target() const;
   // Returns the target's LayoutObject (if target exists and is attached to the
   // layout tree). Also perform cycle-checking, and may thus return nullptr if
   // this SVGResourceClient -> SVGResource reference would start a cycle.
@@ -104,6 +104,7 @@
   SVGResource();
 
   void InvalidateCycleCache();
+  virtual void UpdateContentLifecycleForUse() const = 0;
   void NotifyContentChanged();
 
   Member<Element> target_;
@@ -142,6 +143,7 @@
   void Trace(Visitor*) const override;
 
  private:
+  void UpdateContentLifecycleForUse() const override {}
   void TargetChanged(const AtomicString& id);
 
   Member<TreeScope> tree_scope_;
@@ -166,6 +168,7 @@
  private:
   bool IsLoading() const override;
   Element* ResolveTarget();
+  void UpdateContentLifecycleForUse() const override;
 
   // SVGResourceDocumentObserver:
   void ResourceNotifyFinished(SVGResourceDocumentContent*) override;
@@ -193,6 +196,7 @@
 
   bool IsLoading() const override;
   Element* ResolveTarget();
+  void UpdateContentLifecycleForUse() const override;
 
   // ImageResourceObserver overrides
   void ImageNotifyFinished(ImageResourceContent*) override;
diff --git a/third_party/blink/renderer/core/svg/svg_resource_document_content.cc b/third_party/blink/renderer/core/svg/svg_resource_document_content.cc
index d87df30..615f636 100644
--- a/third_party/blink/renderer/core/svg/svg_resource_document_content.cc
+++ b/third_party/blink/renderer/core/svg/svg_resource_document_content.cc
@@ -22,6 +22,7 @@
 
 #include "third_party/blink/renderer/core/svg/svg_resource_document_content.h"
 
+#include "base/auto_reset.h"
 #include "base/notreached.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/svg/crashtests/chrome-bug-517309206.html b/third_party/blink/web_tests/external/wpt/svg/crashtests/chrome-bug-517309206.html
new file mode 100644
index 0000000..cd303e13
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/svg/crashtests/chrome-bug-517309206.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<link rel="help" href="https://crbug.com/517309206">
+<p>This test should not crash.</p>
+<svg>
+  <rect width="100" height="100" fill="url(support/ext.svg#p)"/>
+</svg>
+<script>
+  let n = 0;
+  function bump() {
+    document.body.style.opacity = 1 - (n % 2) * 0.001;
+    if (++n < 60) {
+      requestAnimationFrame(bump);
+    } else {
+      document.documentElement.className = '';
+    }
+  }
+  requestAnimationFrame(bump);
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/svg/crashtests/support/ext.svg b/third_party/blink/web_tests/external/wpt/svg/crashtests/support/ext.svg
new file mode 100644
index 0000000..8950531d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/svg/crashtests/support/ext.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <pattern id="p" patternUnits="userSpaceOnUse" width="100" height="100">
+    <filter id="fp">
+      <!-- An SVG with a nested (raster) image load. -->
+      <feImage href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MiIgaGVpZ2h0PSI1MiI+PGltYWdlIHg9IjAiIHk9IjAiIGhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBQUVBQUFBQkNBUUFBQUMxSEF3Q0FBQUFDMGxFUVZSNDJtUDgveDhBQXVzQjlRRHE2UUFBQUFCSlJVNUVya0pnZ2c9PSIvPjwvc3ZnPg=="/>
+    </filter>
+    <rect x="0" y="0" width="100" height="100" fill="orange" filter="url(#fp)"/>
+  </pattern>
+</svg>
Loading diff…

Original Bug Report

reported by [email protected]

Potential renderer memory corruption via external SVG resource painting with dirty layout

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential security vulnerability exists in Blink’s handling of external SVG resources, where the layout tree of an isolated document can be dirtied after its initial lifecycle pass. When the host document subsequently paints, it can paint the dirty layout subtree of the isolated document without validating or re-running its layout lifecycle. This violates core layout invariants, potentially leading to memory corruption (such as use-after-free or out-of-bounds access) inside the sandboxed renderer process.

Affected files:

  • third_party/blink/renderer/core/svg/svg_resource_document_content.cc
  • third_party/blink/renderer/core/paint/clip_path_clipper.cc
  • third_party/blink/renderer/core/paint/svg_shape_painter.cc
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_clipper.cc
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc

Estimated timestamp from git blame: 2024-04-25

Summary

SVGResourceDocumentContent manages the lifecycle of external SVG resources (such as markers, clip-paths, and patterns referenced via url(ext.svg#id)). These resources are hosted inside an IsolatedSVGDocumentHost under kStatic mode, and their layout lifecycle is initially updated exactly once when the resource finishes loading. If the isolated document’s layout is subsequently dirtied (for example, via an inline data: URL stream containing a multipart/x-mixed-replace image), subsequent repaints by the host document paint the dirty layout subtree without performing a corresponding layout lifecycle update. This violates rendering engine invariants, potentially resulting in use-after-free or out-of-bounds access inside the sandboxed renderer process.

Technical Analysis

When an external SVG resource finishes loading, SVGResourceDocumentContent::LoadingFinished() executes a single, one-time layout lifecycle pass:

void SVGResourceDocumentContent::LoadingFinished() {
  LocalFrame* frame = document_host_->GetFrame();
  frame->View()->UpdateAllLifecyclePhasesExceptPaint(
      DocumentUpdateReason::kSVGImage);
  UpdateStatus(ResourceStatus::kCached);
}

Source: third_party/blink/renderer/core/svg/svg_resource_document_content.cc

However, subsequent invalidations via the document’s ChromeClient (such as ScheduleAnimation() or InvalidateContainer()) only trigger a repaint notification (ContentChanged()) to the host document’s observers, but do not run the layout lifecycle for the internal isolated document.

When the host document paints, the associated SVG painters and layout objects invoke the paint methods of the resource’s layout objects directly without verifying that the layout subtree is clean. Crucially:

  1. ClipPathClipper::ResolveElementReference() (in third_party/blink/renderer/core/paint/clip_path_clipper.cc) only performs a debug-only check: SECURITY_DCHECK(!resource_clipper->SelfNeedsFullLayout()). This is compiled out in official release builds and only asserts the container itself, not its child subtree.
  2. LayoutSVGResourceClipper::CreatePaintRecord() and LayoutSVGResourcePattern similarly iterate and paint child objects directly without ensuring that their layout states are clean.

Suggested Potential Exploitation Scenario

Note: These are potential steps; our testing tools do not currently have the capability to execute code to verify this programmatically.

An attacker could potentially use a same-origin external SVG document containing an inline data:multipart/x-mixed-replace subresource to schedule asynchronous tasks and dirty the isolated document’s layout:

  1. The external SVG resource (ext.svg) contains an <image> pointing to an inline data:multipart/x-mixed-replace;boundary=boundary,... URL.
  2. In ResourceLoader::Start(), the data: URL bypasses GetURLLoaderFactory() checks entirely and loads in-process via ResourceLoader::HandleDataUrl(). This sets up a MultipartImageResourceParser inside ImageResource and feeds it bytes asynchronously.
  3. The attacker structures the data stream so that the first part of the multipart stream (the first image frame) is exactly 1MB (1,048,576 bytes).
  4. When ResponseBodyLoader::OnStateChange (Task A) runs, its parsing loop is constrained by network::kMaxNumConsumedBytesInTask (exactly 1MB). Task A completes parsing the first frame, which fires ImageResource::OnePartInMultipartReceived(). This sets the resource status to kCached, triggers the isolated document’s load event, and schedules Task C (IsolatedSVGDocumentHost::AsyncLoadCompleted) to complete loading.
  5. Because Task A reached the 1MB limit, it posts Task B (ResponseBodyLoader::OnStateChange continuation loop) to process the rest of the stream.
  6. Task C, having been posted during Task A’s execution, runs before Task B. Task C calls SVGResourceDocumentContent::LoadingFinished(), running the single layout lifecycle update and successfully clearing the NeedsLayout dirty flags on the tree.
  7. Task B then runs, processing the second part of the multipart stream (which specifies different image dimensions). This calls LayoutImage::ImageChanged(), which invokes SetNeedsLayout() and dirties the layout subtree of the isolated document.
  8. The host document receives the invalidation and schedules a repaint. The SVG painters (e.g. ClipPathClipper) directly call Paint() on the dirty layout subtree of the isolated document. This violates rendering assumptions and could lead to renderer memory corruption.

Suggested Fix

To resolve this issue, Blink should ensure that the isolated document’s layout lifecycle is automatically updated before any painting occurs if its layout tree has been dirtied.

  1. Introduce layout safety validations: Upgrade layout checks in ClipPathClipper::ResolveElementReference from debug-only assertions to robust SECURITY_CHECKs and extend them to cover child subtrees.
  2. Ensure the lifecycle runs: Update SVGResourceDocumentContent to track when its document’s layout state is dirtied, and ensure UpdateAllLifecyclePhasesExceptPaint is run on the isolated document before any of its components are painted.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker