CVE-2026-11688
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/svg/graphics/svg_image.cc |
modified | |
Documentthird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
Elementthird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
ExternalSVGResourceImageContentthird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
IsolatedSVGDocumentHostthird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
LayoutSVGRootthird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
LocalFramethird_party/blink/renderer/core/svg/graphics/svg_image.h |
modified | |
ifthird_party/blink/renderer/core/svg/svg_resource.cc |
modified |
Files Changed
third_party/blink/renderer/core/svg/graphics/svg_image.ccthird_party/blink/renderer/core/svg/graphics/svg_image.hthird_party/blink/renderer/core/svg/svg_resource.ccthird_party/blink/renderer/core/svg/svg_resource.hthird_party/blink/renderer/core/svg/svg_resource_document_content.cc
Patch
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"
Regression Test / PoC
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>
Original Bug Report
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.ccthird_party/blink/renderer/core/paint/clip_path_clipper.ccthird_party/blink/renderer/core/paint/svg_shape_painter.ccthird_party/blink/renderer/core/layout/svg/layout_svg_resource_clipper.ccthird_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:
ClipPathClipper::ResolveElementReference()(inthird_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.LayoutSVGResourceClipper::CreatePaintRecord()andLayoutSVGResourcePatternsimilarly 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:
- The external SVG resource (
ext.svg) contains an<image>pointing to an inlinedata:multipart/x-mixed-replace;boundary=boundary,...URL. - In
ResourceLoader::Start(), thedata:URL bypassesGetURLLoaderFactory()checks entirely and loads in-process viaResourceLoader::HandleDataUrl(). This sets up aMultipartImageResourceParserinsideImageResourceand feeds it bytes asynchronously. - The attacker structures the data stream so that the first part of the multipart stream (the first image frame) is exactly 1MB (
1,048,576bytes). - When
ResponseBodyLoader::OnStateChange(Task A) runs, its parsing loop is constrained bynetwork::kMaxNumConsumedBytesInTask(exactly 1MB). Task A completes parsing the first frame, which firesImageResource::OnePartInMultipartReceived(). This sets the resource status tokCached, triggers the isolated document’sloadevent, and schedules Task C (IsolatedSVGDocumentHost::AsyncLoadCompleted) to complete loading. - Because Task A reached the 1MB limit, it posts Task B (
ResponseBodyLoader::OnStateChangecontinuation loop) to process the rest of the stream. - 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 theNeedsLayoutdirty flags on the tree. - Task B then runs, processing the second part of the multipart stream (which specifies different image dimensions). This calls
LayoutImage::ImageChanged(), which invokesSetNeedsLayout()and dirties the layout subtree of the isolated document. - The host document receives the invalidation and schedules a repaint. The SVG painters (e.g.
ClipPathClipper) directly callPaint()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.
- Introduce layout safety validations: Upgrade layout checks in
ClipPathClipper::ResolveElementReferencefrom debug-only assertions to robustSECURITY_CHECKs and extend them to cover child subtrees. - Ensure the lifecycle runs: Update
SVGResourceDocumentContentto track when its document’s layout state is dirtied, and ensureUpdateAllLifecyclePhasesExceptPaintis 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.