CVE-2026-106330
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/paint/svg_mask_painter.cc |
modified | |
promise_testthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html |
modified |
Files Changed
third_party/blink/renderer/core/paint/svg_mask_painter.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
Patch
From 91b61e4bed84d126f55fd267ab0c489238a6c3e4 Mon Sep 17 00:00:00 2001
From: Philip Rogers <pdr@chromium.org>
Date: Mon, 14 Sep 2026 12:25:13 -0700
Subject: [PATCH] [html-in-canvas] Disallow SVG masks that are outside a canvas subtree
As with SVG patterns, we disallow the use of masks defined outside of a
canvas subtree, where privacy protections are not available.
Fixed: 553135213
Change-Id: Id4fa486444b0896e574dbf51d65579bb56b86e9b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8398235
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Commit-Queue: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697429}
---
diff --git a/third_party/blink/renderer/core/paint/svg_mask_painter.cc b/third_party/blink/renderer/core/paint/svg_mask_painter.cc
index 62a4206..a93e4da2 100644
--- a/third_party/blink/renderer/core/paint/svg_mask_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_mask_painter.cc
@@ -283,6 +283,10 @@
if (!masker) {
return;
}
+ if ((paint_flags & PaintFlag::kPrivacyPreserving) &&
+ !masker->GetElement()->IsInCanvasSubtree()) {
+ return;
+ }
const AffineTransform content_transformation =
MaskToContentTransform(*masker, reference_box, zoom);
SubtreeContentTransformScope content_transform_scope(content_transformation);
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
index 5ae47f1..27f10b7 100644
--- a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
@@ -28,9 +28,6 @@
<feFuncB type="linear" slope="0" intercept="1"/>
</feComponentTransfer>
</filter>
- <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
- <text x="0" y="50" filter="url(#filter-same)">x</text>
- </mask>
<mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<text x="0" y="50" filter="url(#filter-cross)">x</text>
</mask>
@@ -39,6 +36,11 @@
<canvas id="canvas" width="100" height="200" layoutsubtree>
<svg id="child" width="100" height="200">
+ <defs>
+ <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+ <text x="0" y="50" filter="url(#filter-same)">x</text>
+ </mask>
+ </defs>
<svg x="0" y="0" width="100" height="100">
<rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
</svg>
@@ -61,7 +63,7 @@
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let pixel = _getPixelFromImageData(imgData, 50, 50);
- assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in external mask should be drawn");
+ assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in internal mask should be drawn");
pixel = _getPixelFromImageData(imgData, 50, 150);
assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image in external mask should not be drawn");
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
index efaf94f..ed561ee 100644
--- a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
@@ -28,11 +28,6 @@
<feFuncB type="linear" slope="0" intercept="1"/>
</feComponentTransfer>
</filter>
- <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
- <g filter="url(#filter-same)">
- <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
- </g>
- </mask>
<mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<g filter="url(#filter-cross)">
<rect width="100" height="100" fill="black" filter="url(#filter-cross)"/>
@@ -53,6 +48,13 @@
<canvas id="canvas" width="100" height="400" layoutsubtree>
<svg id="child" width="100" height="400">
+ <defs>
+ <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+ <g filter="url(#filter-same)">
+ <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
+ </g>
+ </mask>
+ </defs>
<svg x="0" y="0" width="100" height="100">
<rect width="100" height="100" fill="rgb(20, 120, 220)" mask="url(#mask-same)"/>
</svg>
@@ -81,7 +83,7 @@
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let pixel = _getPixelFromImageData(imgData, 50, 50);
- assert_array_equals(pixel, [20, 120, 220, 255], "Nested same origin filter images in external mask should be drawn");
+ assert_array_equals(pixel, [20, 120, 220, 255], "Nested same origin filter images in internal mask should be drawn");
pixel = _getPixelFromImageData(imgData, 50, 150);
assert_array_equals(pixel, [0, 0, 255, 255], "Nested cross origin filter images in external mask should not be drawn");
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html
new file mode 100644
index 0000000..1eb0896f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>drawElementImage does not reveal visited links in external SVG masks</title>
+ <link rel="help" href="https://github.com/WICG/html-in-canvas">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='/html/canvas/resources/canvas-tests.js'></script>
+ <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+ <style>
+ /* In mask-same, link is white so mask luminance is 1 (alpha 255), allowing the element to draw. */
+ #mask-same a:link { color: #ffffff; }
+ #mask-same a:visited { color: #ffffff; }
+
+ /* In mask-cross, unvisited is black (luma 0 -> alpha 0), visited is white (luma 1 -> alpha 255).
+ Privacy-preserving rendering disallows external masks, preventing the element from drawing. */
+ #mask-cross a:link { color: #000000; }
+ #mask-cross a:visited { color: #ffffff; }
+ </style>
+</head>
+
+<body>
+ <svg width="0" height="0">
+ <defs>
+ <mask id="mask-cross" maskContentUnits="objectBoundingBox">
+ <a href="">
+ <rect width="1" height="1" fill="currentColor"/>
+ </a>
+ </mask>
+ </defs>
+ </svg>
+
+ <canvas id="canvas" width="100" height="200" layoutsubtree>
+ <svg id="child" width="100" height="200">
+ <defs>
+ <mask id="mask-same" maskContentUnits="objectBoundingBox">
+ <a href="">
+ <rect width="1" height="1" fill="currentColor"/>
+ </a>
+ </mask>
+ </defs>
+ <svg x="0" y="0" width="100" height="100">
+ <rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
+ </svg>
+ <svg x="0" y="100" width="100" height="100">
+ <rect width="100" height="100" fill="rgb(40, 140, 240)" mask="url(#mask-cross)"/>
+ </svg>
+ </svg>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ await waitForCanvasPaint(canvas);
+ var ctx = canvas.getContext('2d');
+ ctx.fillStyle = 'blue';
+ ctx.fillRect(0, 0, 100, 200);
+ ctx.drawElementImage(document.getElementById('child'), 0, 0);
+
+ // Fetch all pixel data once to avoid multiple slow readbacks.
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ let pixel = _getPixelFromImageData(imgData, 50, 50);
+ assert_array_equals(pixel, [30, 130, 230, 255], "Same origin internal mask should be drawn");
+
+ pixel = _getPixelFromImageData(imgData, 50, 150);
+ assert_array_equals(pixel, [0, 0, 255, 255], "External mask should not be drawn");
+ });
+ }
+ </script>
+</body>
+</html>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
index 5ae47f1..27f10b7 100644
--- a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
@@ -28,9 +28,6 @@
<feFuncB type="linear" slope="0" intercept="1"/>
</feComponentTransfer>
</filter>
- <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
- <text x="0" y="50" filter="url(#filter-same)">x</text>
- </mask>
<mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<text x="0" y="50" filter="url(#filter-cross)">x</text>
</mask>
@@ -39,6 +36,11 @@
<canvas id="canvas" width="100" height="200" layoutsubtree>
<svg id="child" width="100" height="200">
+ <defs>
+ <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+ <text x="0" y="50" filter="url(#filter-same)">x</text>
+ </mask>
+ </defs>
<svg x="0" y="0" width="100" height="100">
<rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
</svg>
@@ -61,7 +63,7 @@
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let pixel = _getPixelFromImageData(imgData, 50, 50);
- assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in external mask should be drawn");
+ assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in internal mask should be drawn");
pixel = _getPixelFromImageData(imgData, 50, 150);
assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image in external mask should not be drawn");
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
index efaf94f..ed561ee 100644
--- a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
@@ -28,11 +28,6 @@
<feFuncB type="linear" slope="0" intercept="1"/>
</feComponentTransfer>
</filter>
- <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
- <g filter="url(#filter-same)">
- <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
- </g>
- </mask>
<mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<g filter="url(#filter-cross)">
<rect width="100" height="100" fill="black" filter="url(#filter-cross)"/>
@@ -53,6 +48,13 @@
<canvas id="canvas" width="100" height="400" layoutsubtree>
<svg id="child" width="100" height="400">
+ <defs>
+ <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+ <g filter="url(#filter-same)">
+ <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
+ </g>
+ </mask>
+ </defs>
<svg x="0" y="0" width="100" height="100">
<rect width="100" height="100" fill="rgb(20, 120, 220)" mask="url(#mask-same)"/>
</svg>
@@ -81,7 +83,7 @@
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let pixel = _getPixelFromImageData(imgData, 50, 50);
- assert_array_equals(pixel, [20, 120, 220, 255], "Nested same origin filter images in external mask should be drawn");
+ assert_array_equals(pixel, [20, 120, 220, 255], "Nested same origin filter images in internal mask should be drawn");
pixel = _getPixelFromImageData(imgData, 50, 150);
assert_array_equals(pixel, [0, 0, 255, 255], "Nested cross origin filter images in external mask should not be drawn");
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html
new file mode 100644
index 0000000..1eb0896f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-visited.tentative.https.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>drawElementImage does not reveal visited links in external SVG masks</title>
+ <link rel="help" href="https://github.com/WICG/html-in-canvas">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='/html/canvas/resources/canvas-tests.js'></script>
+ <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+ <style>
+ /* In mask-same, link is white so mask luminance is 1 (alpha 255), allowing the element to draw. */
+ #mask-same a:link { color: #ffffff; }
+ #mask-same a:visited { color: #ffffff; }
+
+ /* In mask-cross, unvisited is black (luma 0 -> alpha 0), visited is white (luma 1 -> alpha 255).
+ Privacy-preserving rendering disallows external masks, preventing the element from drawing. */
+ #mask-cross a:link { color: #000000; }
+ #mask-cross a:visited { color: #ffffff; }
+ </style>
+</head>
+
+<body>
+ <svg width="0" height="0">
+ <defs>
+ <mask id="mask-cross" maskContentUnits="objectBoundingBox">
+ <a href="">
+ <rect width="1" height="1" fill="currentColor"/>
+ </a>
+ </mask>
+ </defs>
+ </svg>
+
+ <canvas id="canvas" width="100" height="200" layoutsubtree>
+ <svg id="child" width="100" height="200">
+ <defs>
+ <mask id="mask-same" maskContentUnits="objectBoundingBox">
+ <a href="">
+ <rect width="1" height="1" fill="currentColor"/>
+ </a>
+ </mask>
+ </defs>
+ <svg x="0" y="0" width="100" height="100">
+ <rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
+ </svg>
+ <svg x="0" y="100" width="100" height="100">
+ <rect width="100" height="100" fill="rgb(40, 140, 240)" mask="url(#mask-cross)"/>
+ </svg>
+ </svg>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ await waitForCanvasPaint(canvas);
+ var ctx = canvas.getContext('2d');
+ ctx.fillStyle = 'blue';
+ ctx.fillRect(0, 0, 100, 200);
+ ctx.drawElementImage(document.getElementById('child'), 0, 0);
+
+ // Fetch all pixel data once to avoid multiple slow readbacks.
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ let pixel = _getPixelFromImageData(imgData, 50, 50);
+ assert_array_equals(pixel, [30, 130, 230, 255], "Same origin internal mask should be drawn");
+
+ pixel = _getPixelFromImageData(imgData, 50, 150);
+ assert_array_equals(pixel, [0, 0, 255, 255], "External mask should not be drawn");
+ });
+ }
+ </script>
+</body>
+</html>
Original Bug Report
Visited link history leak via SVG mask luminance in HTML-in-Canvas
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: The HTML-in-Canvas feature (CanvasDrawElement) enforces privacy-preserving rendering for elements drawn into <canvas layoutsubtree>, but visited-link suppression is only applied style-time to elements that reside inside the canvas subtree. When an element inside the canvas references an external SVG <mask id="..."> located outside the canvas subtree, the mask’s visited styles are resolved and rendered without an IsInCanvasSubtree check. Under the default luminance masking mode, visited link colors are translated into alpha values, potentially allowing web content to read the user’s visited link history via drawElementImage() and getImageData() without tainting the canvas.
Affected files:
third_party/blink/renderer/core/paint/svg_mask_painter.ccthird_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.ccthird_party/blink/renderer/core/css/resolver/style_resolver_state.cc
Estimated timestamp from git blame: 2026-04-29
Potential Visited Link History Leak via SVG Mask Luminance in HTML-in-Canvas
Summary
When painting descendants of <canvas layoutsubtree> with the HTMLInCanvas origin trial feature (CanvasDrawElement), Blink applies PaintFlag::kPrivacyPreserving to prevent privacy leaks (third_party/blink/renderer/core/paint/paint_layer_painter.cc:633). However, the only visited-link suppression in this path occurs at style-resolution time and is strictly gated on GetElement().IsInCanvasSubtree() (third_party/blink/renderer/core/css/resolver/style_resolver_state.cc:172-175).
When a canvas child references an out-of-subtree SVG <mask id="...">, SVGMaskPainter::ResolveElementReference (third_party/blink/renderer/core/paint/svg_mask_painter.cc:49-70) resolves and paints the mask without checking PaintFlag::kPrivacyPreserving or IsInCanvasSubtree(). Because default SVG masks use luminance masking (EMaskType::kLuminance), visited link colors (e.g., white vs. black) are converted via cc::ColorFilter::MakeLuma() into alpha transparency values on the canvas child. When the child is drawn into the canvas via drawElementImage() (third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc:1407-1500), the canvas remains origin-clean, potentially allowing script to read the visited state via getImageData().
Root Cause
- Style-time gate bypass:
StyleResolverState::InsideLink()forces visited links to unvisited only ifGetElement().IsInCanvasSubtree()is true (style_resolver_state.cc:172-175). Mask resources defined outside the<canvas layoutsubtree>retainEInsideLink::kInsideVisitedLink. - Missing membership gate in mask resolution: Unlike SVG paint servers (which are explicitly gated in
third_party/blink/renderer/core/paint/svg_object_painter.cc:34-37viaif (paint_flags & PaintFlag::kPrivacyPreserving && !uri_resource->GetElement()->IsInCanvasSubtree()) return false;),SVGMaskPainter::ResolveElementReference(svg_mask_painter.cc:49-70) andSVGMaskPainter::PaintSVGMaskLayer(svg_mask_painter.cc:273-307) perform no membership or privacy check. - Luma-to-alpha conversion:
PaintSVGMaskLayerappliescc::ColorFilter::MakeLuma()whenmasker->StyleRef().MaskType() == EMaskType::kLuminance(svg_mask_painter.cc:294-297), translating mask RGB into alpha. - Origin-clean readback:
BaseRenderingContext2D::DrawElementInternal(base_rendering_context_2d.cc:1407-1500) replays the recorded paint ops without marking the canvas origin as tainted, allowinggetImageData()to read the resulting pixel values.
Potential Trigger Steps
(Note: These are potential steps deduced from static code analysis; our tooling agent does not execute code.)
- Serve a page with an
HTMLInCanvasorigin trial token. - In HTML, construct a canvas with
layoutsubtreecontaining an SVG child referencing an external mask:<canvas id="c" layoutsubtree width="100" height="100"> <svg id="child" width="100" height="100"> <rect width="100" height="100" fill="black" style="mask: url(#m);" /> </svg> </canvas> <svg width="0" height="0"> <mask id="m" maskContentUnits="objectBoundingBox"> <a href="https://target-probe.example/"> <rect width="1" height="1" fill="currentColor" /> </a> </mask> </svg> - Define CSS visited styles on the mask link:
#m a:link { color: rgb(0, 0, 0); } #m a:visited { color: rgb(255, 255, 255); } - After layout and paint settle, draw the element into the canvas:
const ctx = document.getElementById('c').getContext('2d'); ctx.drawElementImage(document.getElementById('child'), 0, 0); const alpha = ctx.getImageData(50, 50, 1, 1).data[3]; - If
alpha === 255, the URL is visited in the current partition; ifalpha === 0, it is unvisited.
Suggested Fix
Add a check in third_party/blink/renderer/core/paint/svg_mask_painter.cc (e.g., inside ResolveElementReference or PaintSVGMaskLayer), matching the logic used in svg_object_painter.cc:34-37, to ensure that when paint_flags & PaintFlag::kPrivacyPreserving is present, external mask resources not contained within the canvas subtree (!masker->GetElement()->IsInCanvasSubtree()) are rejected or forced to paint with unvisited styling.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.