Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in DOM
DescriptionInformation leak in DOM
ComponentDOM
Bug ClassLogic Error
Tracker501661601
Fix commitb3bf6600e675 (chromium/src) +1273/-64
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/dom/element.cc
modified

Files Changed

  • third_party/blink/renderer/core/css/resolver/style_adjuster.cc
  • third_party/blink/renderer/core/dom/element.cc
  • third_party/blink/renderer/core/dom/flat_tree_traversal.h
From b3bf6600e675359e6fb5433f1bfd303921a8f8d1 Mon Sep 17 00:00:00 2001
From: Stephen Chenney <[email protected]>
Date: Sun, 19 Jul 2026 20:35:01 -0700
Subject: [PATCH] [HiC] Fix issues with shadow dom canvas children

Recursively mark shadow dom content as a canvas child when it's slotting
status changes (flat tree parent changes). This corrects problems with
privacy preserving painting and painting in general, particularly for
forms.

This approach was chosen over a style-based version for a couple of
reasons:
* There is a significant chunk of code that depends on the
  DidChangeCanvasSubtree method, so it would need to stay in some form.
  There's less use in moving to style if we still need this DOM status
  tracking.
* The style based relied on suppressing autofill data at the getter
  stage, rather than on setting. I think a setter approach is more
  robust to future changes.

Add tests for declarative slotting, custom elements, and privacy for
visited links and cross-origin image. The privacy tests cover things
protected by style setting (visited links) and paint flags (images).
Also a test for video correctly updating it's compositor read-back
needs, and lots of form control unit tests for autofill privacy.

Fixed: 501661601, 503553618
Change-Id: I261f0c55c3e7204ff919f3e64a9655c6f93e47e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7899615
Commit-Queue: Stephen Chenney <[email protected]>
Reviewed-by: Philip Rogers <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1664517}
---

diff --git a/third_party/blink/renderer/core/css/resolver/style_adjuster.cc b/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
index b3d29984..92eaeb2 100644
--- a/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
+++ b/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
@@ -743,8 +743,9 @@
   if (element && element->IsCanvasOrInCanvasSubtree() &&
       RuntimeEnabledFeatures::CanvasDrawElementEnabled(
           element->GetExecutionContext())) {
-    if (const auto* canvas =
-            DynamicTo<HTMLCanvasElement>(element->parentElement())) {
+    const Element* parent =
+        FlatTreeTraversal::ParentElementSkippingSlots(*element);
+    if (const auto* canvas = DynamicTo<HTMLCanvasElement>(parent)) {
       return canvas->layoutSubtree();
     }
   }
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index a7c2c17..bf6042f 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -4293,12 +4293,78 @@
   }
 }
 
+#if DCHECK_IS_ON()
+void VerifySubtreeIsInCanvas(const Element& element, bool value) {
+  if (IsA<HTMLCanvasElement>(element)) {
+    // When the verifier starts with an element outside the tree that should
+    // have value false, but then reaches a canvas within the subtree (e.g.
+    // in an iframe or nested), we should set the expected value back to true.
+    value = true;
+  }
+  DCHECK(element.IsCanvasOrInCanvasSubtree() == value);
+  if (ShadowRoot* shadow_root = element.GetShadowRoot()) {
+    for (Element& child : ElementTraversal::ChildrenOf(*shadow_root)) {
+      VerifySubtreeIsInCanvas(child, value);
+    }
+  }
+  if (auto* slot = ToHTMLSlotElementIfSupportsAssignmentOrNull(element)) {
+    for (Node* node : slot->AssignedNodesNoRecalc()) {
+      if (auto* child = DynamicTo<Element>(node)) {
+        VerifySubtreeIsInCanvas(*child, value);
+      }
+    }
+  }
+  if (const auto* frame_owner = DynamicTo<HTMLFrameOwnerElement>(element)) {
+    if (Document* inner_document = frame_owner->contentDocument()) {
+      if (Element* root = inner_document->documentElement()) {
+        VerifySubtreeIsInCanvas(*root, value);
+      }
+    }
+  }
+  for (Element& child : ElementTraversal::ChildrenOf(element)) {
+    if (child.AssignedSlotWithoutRecalc()) {
+      continue;
+    }
+    VerifySubtreeIsInCanvas(child, value);
+  }
+}
+#endif
+
 void Element::SetIsCanvasOrInCanvasSubtree(bool value) {
-  if (value == IsCanvasOrInCanvasSubtree()) {
+  if (IsA<HTMLCanvasElement>(*this)) {
+    value = true;
+  }
+
+  if (value != IsCanvasOrInCanvasSubtree()) {
+    SetElementFlag(ElementFlags::kIsCanvasOrInCanvasSubtree, value);
+    DidChangeIsCanvasOrInCanvasSubtree();
+  } else {
+#if DCHECK_IS_ON()
+    if (!GetDocument().IsSlotAssignmentRecalcForbidden()) {
+      VerifySubtreeIsInCanvas(*this, value);
+    }
+#endif
     return;
   }
-  SetElementFlag(ElementFlags::kIsCanvasOrInCanvasSubtree, value);
-  DidChangeIsCanvasOrInCanvasSubtree();
+
+  if (ShadowRoot* shadow_root = GetShadowRoot()) {
+    for (Element& child : ElementTraversal::ChildrenOf(*shadow_root)) {
+      child.SetIsCanvasOrInCanvasSubtree(value);
+    }
+  }
+  if (auto* slot = ToHTMLSlotElementIfSupportsAssignmentOrNull(*this)) {
+    for (Node* node : slot->AssignedNodesNoRecalc()) {
+      if (auto* child = DynamicTo<Element>(node)) {
+        child->SetIsCanvasOrInCanvasSubtree(value);
+      }
+    }
+  }
+  for (Element& child : ElementTraversal::ChildrenOf(*this)) {
+    if (!child.IsPseudoElement() && child.AssignedSlotWithoutRecalc()) {
+      continue;
+    }
+    child.SetIsCanvasOrInCanvasSubtree(value);
+  }
 }
 
 void Element::DidChangeIsCanvasOrInCanvasSubtree() {
@@ -4313,6 +4379,33 @@
   }
 }
 
+bool Element::IsInCanvasSubtree() const {
+  auto& document = GetDocument();
+  const Element* parent = nullptr;
+  if (document.IsFlatTreeTraversalForbidden() ||
+      document.IsInSlotAssignmentRecalc()) {
+    if (IsPseudoElement()) {
+      parent = ParentOrShadowHostElement();
+    } else if (const auto* slot = AssignedSlotWithoutRecalc()) {
+      parent = slot;
+    } else {
+      parent = ParentOrShadowHostElement();
+    }
+  } else {
+    parent = FlatTreeTraversal::ParentElementSkippingSlots(*this);
+  }
+  if (parent) {
+    return parent->IsCanvasOrInCanvasSubtree();
+  }
+
+  if (!isConnected()) {
+    return false;
+  }
+
+  auto* owner = document.LocalOwner();
+  return owner && owner->IsCanvasOrInCanvasSubtree();
+}
+
 void Element::RemovedFrom(ContainerNode& insertion_point) {
   bool was_in_document = insertion_point.isConnected();
   if (Element* parent = DynamicTo<Element>(insertion_point)) {
@@ -10347,18 +10440,6 @@
   return style.Display() == EDisplay::kContents;
 }
 
-bool Element::IsInCanvasSubtree() const {
-  auto* parent = ParentOrShadowHostElement();
-  if (parent) {
-    return parent->IsCanvasOrInCanvasSubtree();
-  }
-  if (!isConnected()) {
-    return false;
-  }
-  auto* owner = GetDocument().LocalOwner();
-  return owner && owner->IsCanvasOrInCanvasSubtree();
-}
-
 AtomicString Element::ComputeInheritedLanguage() const {
   const Node* n = this;
   AtomicString value;
diff --git a/third_party/blink/renderer/core/dom/flat_tree_traversal.h b/third_party/blink/renderer/core/dom/flat_tree_traversal.h
index b209391..1bc9a33 100644
--- a/third_party/blink/renderer/core/dom/flat_tree_traversal.h
+++ b/third_party/blink/renderer/core/dom/flat_tree_traversal.h
@@ -33,6 +33,7 @@
 #include "third_party/blink/renderer/core/dom/node_traversal.h"
 #include "third_party/blink/renderer/core/dom/shadow_root.h"
 #include "third_party/blink/renderer/core/dom/traversal_range.h"
+#include "third_party/blink/renderer/core/html/html_slot_element.h"
 #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/exported/web_form_control_element_test.cc b/third_party/blink/renderer/core/exported/web_form_control_element_test.cc
index 24141ec0..9c6d387 100644
--- a/third_party/blink/renderer/core/exported/web_form_control_element_test.cc
+++ b/third_party/blink/renderer/core/exported/web_form_control_element_test.cc
@@ -18,16 +18,20 @@
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/dom/shadow_root.h"
+#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
 #include "third_party/blink/renderer/core/event_type_names.h"
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
 #include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
 #include "third_party/blink/renderer/core/html/forms/html_form_element.h"
 #include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
 #include "third_party/blink/renderer/core/input_type_names.h"
 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 
 namespace blink {
 
@@ -98,10 +102,7 @@
 }
 
 TEST_F(WebFormControlElementTest, TextControlPreviewDisabledInCanvas) {
-  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled(
-          GetDocument().GetExecutionContext())) {
-    return;
-  }
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
 
   GetDocument().documentElement()->SetInnerHTMLWithoutTrustedTypes(R"(
     <form>
@@ -128,10 +129,7 @@
 
 TEST_F(WebFormControlElementTest,
        TextControlPreviewDisabledWhenMovingToCanvas) {
-  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled(
-          GetDocument().GetExecutionContext())) {
-    return;
-  }
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
 
   GetDocument().documentElement()->SetInnerHTMLWithoutTrustedTypes(R"(
     <form>
@@ -162,10 +160,7 @@
 }
 
 TEST_F(WebFormControlElementTest, SelectPreviewDisabledInCanvas) {
-  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled(
-          GetDocument().GetExecutionContext())) {
-    return;
-  }
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
 
   GetDocument().documentElement()->SetInnerHTMLWithoutTrustedTypes(R"(
     <form>
@@ -190,10 +185,7 @@
 
 TEST_F(WebFormControlElementTest,
        SelectPreviewDisabledInCanvasWhenMovingToCanvas) {
-  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled(
-          GetDocument().GetExecutionContext())) {
-    return;
-  }
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
 
   GetDocument().documentElement()->SetInnerHTMLWithoutTrustedTypes(R"(
     <form>
@@ -219,6 +211,548 @@
   EXPECT_TRUE(select.SuggestedValue().IsEmpty());
 }
 
+TEST_F(WebFormControlElementTest, TextControlSlottedPreviewDisabledInCanvas) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <div>
+      <template shadowrootmode="open">
+        <canvas layoutsubtree>
+          <slot name="slot1"></slot>
+        </canvas>
+      </template>
+      <form id="slotted" slot="slot1">
+        <input id="input_id">
+        <textarea id="textarea_id"></textarea>
+      </form>
+    </div>
+  )");
+
+  WebFormControlElement input(
+      DynamicTo<HTMLFormControlElement>(GetElementById("input_id")));
+  WebFormControlElement textarea(
+      DynamicTo<HTMLFormControlElement>(GetElementById("textarea_id")));
+
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_TRUE(GetElementById("input_id")->IsInCanvasSubtree());
+  EXPECT_TRUE(GetElementById("textarea_id")->IsInCanvasSubtree());
+
+  input.SetSuggestedValue("suggestion");
+  textarea.SetSuggestedValue("suggestion");
+
+  // Elements inside canvas should not show autofill suggestions, as this can
+  // leak the information to javascript.
+  EXPECT_TRUE(input.SuggestedValue().IsEmpty());
+  EXPECT_TRUE(textarea.SuggestedValue().IsEmpty());
+}
+
+TEST_F(WebFormControlElementTest, TextControlPreviewDisabledWhenMovingToSlot) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <div id=slotHost>
+      <template shadowrootmode="open">
+        <canvas layoutsubtree>
+          <slot name="slot1"></slot>
+        </canvas>
+      </template>
+    </div>
+    <form id="slotted" slot="slot1">
+      <input id="input_id">
+      <textarea id="textarea_id"></textarea>
+    </form>
+  )");
+
+  Element* input_elmt = GetElementById("input_id");
+  Element* textarea_elmt = GetElementById("textarea_id");
+
+  WebFormControlElement input(DynamicTo<HTMLFormControlElement>(input_elmt));
+  WebFormControlElement textarea(
+      DynamicTo<HTMLFormControlElement>(textarea_elmt));
+
+  EXPECT_FALSE(input_elmt->IsInCanvasSubtree());
+  EXPECT_FALSE(textarea_elmt->IsInCanvasSubtree());
+
+  input.SetSuggestedValue("suggestion");
+  textarea.SetSuggestedValue("suggestion");
+
+  // Suggestions should work outside canvas.
+  EXPECT_EQ(input.SuggestedValue().Ascii(), "suggestion");
+  EXPECT_EQ(textarea.SuggestedValue().Ascii(), "suggestion");
+
+  Element* host = GetElementById("slotHost");
+  Element* form = GetElementById("slotted");
+
+  host->moveBefore(form, nullptr, ASSERT_NO_EXCEPTION);
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_TRUE(input_elmt->IsInCanvasSubtree());
+  EXPECT_TRUE(textarea_elmt->IsInCanvasSubtree());
+
+  // Moving the element into a canvas subtree should disable autofill
+  // suggestions, as these can leak the information to javascript.
+  EXPECT_TRUE(input.SuggestedValue().IsEmpty());
+  EXPECT_TRUE(textarea.SuggestedValue().IsEmpty());
+}
+
+TEST_F(WebFormControlElementTest,
+       TextControlPreviewDisabledInCanvasWhenSlotted) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <div id="host">
+      <template shadowrootmode="open">
+        <canvas layoutsubtree>
+          <div id="slotwrapper">
+            <slot></slot>
+          </div>
+        </canvas>
+      </template>
+      <input id="input_id">
+      <textarea id="textarea_id"></textarea>
+      <select id="select_id">
+        <option value="Bar">Bar</option>
+        <option value="Foo">Foo</option>
+      </select>
+    </div>
+  )");
+
+  WebFormControlElement input(
+      DynamicTo<HTMLFormControlElement>(GetElementById("input_id")));
+  WebFormControlElement textarea(
+      DynamicTo<HTMLFormControlElement>(GetElementById("textarea_id")));
+  WebFormControlElement select(
+      DynamicTo<HTMLFormControlElement>(GetElementById("select_id")));
+
+  EXPECT_TRUE(input.Unwrap<HTMLInputElement>()->IsInCanvasSubtree());
+  input.SetSuggestedValue("suggestion");
+  textarea.SetSuggestedValue("suggestion");
+  select.SetSuggestedValue("Foo");
+
+  // Elements slotted inside a canvas should not show autofill suggestions.
+  EXPECT_TRUE(input.SuggestedValue().IsEmpty());
+  EXPECT_TRUE(textarea.SuggestedValue().IsEmpty());
+  EXPECT_TRUE(select.SuggestedValue().IsEmpty());
+}
+
+TEST_F(WebFormControlElementTest,
+       TextControlPreviewDisabledInCanvasWhenNestedAndSlotted) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <div id="host">
+      <template shadowrootmode="open">
+        <div id="normal_div">
+          <slot name="s1"></slot>
+        </div>
+        <canvas id="canvas" layoutsubtree>
+          <slot name="s2"></slot>
+        </canvas>
+      </template>
+      <div id="wrapper" slot="s1">
+        <input id="input_id">
+        <textarea id="textarea_id"></textarea>
+      </div>
+    </div>
+  )");
+
+  WebFormControlElement input(
+      DynamicTo<HTMLFormControlElement>(GetElementById("input_id")));
+  WebFormControlElement textarea(
+      DynamicTo<HTMLFormControlElement>(GetElementById("textarea_id")));
+
+  input.SetSuggestedValue("suggestion");
+  textarea.SetSuggestedValue("suggestion");
+  EXPECT_EQ(input.SuggestedValue().Ascii(), "suggestion");
+  EXPECT_EQ(textarea.SuggestedValue().Ascii(), "suggestion");
+
+  // Now dynamically change the slot to re-slot the wrapper into the canvas.
+  GetElementById("wrapper")->setAttribute(html_names::kSlotAttr,
+                                          AtomicString("s2"));
+
+  // Force slot assignment recalc and style update.
+  GetDocument().UpdateStyleAndLayoutTree();
+
+  // Nested elements slotted inside a canvas should have their suggestions
+  // cleared.
+  EXPECT_TRUE(input.SuggestedValue().IsEmpty());
+  EXPECT_TRUE(textarea.SuggestedValue().IsEmpty());
+}
+
+TEST_F(WebFormControlElementTest,
+       TextControlPreviewDisabledWhenSlottedInsideIframeUnderCanvas) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  frame_test_helpers::WebViewHelper web_view_helper;
+  web_view_helper.Initialize();
+
+  Document* top_doc =
+      web_view_helper.LocalMainFrame()->GetFrame()->GetDocument();
+  ASSERT_TRUE(top_doc);
+
+  top_doc->body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <div id="host">
+      <template shadowrootmode="open">
+        <canvas layoutsubtree>
+          <div id="slotwrapper">
+            <slot></slot>
+          </div>
+        </canvas>
+      </template>
+      <iframe id="iframe_id"></iframe>
+    </div>
+  )");
+
+  web_view_helper.LocalMainFrame()->FrameWidget()->UpdateAllLifecyclePhases(
+      DocumentUpdateReason::kTest);
+
+  auto* iframe = DynamicTo<HTMLFrameOwnerElement>(
+      top_doc->getElementById(AtomicString("iframe_id")));
+  ASSERT_TRUE(iframe);
+  Document* inner_doc = iframe->contentDocument();
+  ASSERT_TRUE(inner_doc);
+
+  inner_doc->body()->SetHTMLUnsafeWithoutTrustedTypes(R"(
+    <input id="inner_input_id">
+  )");
+
+  WebFormControlElement inner_input(DynamicTo<HTMLFormControlElement>(
+      inner_doc->getElementById(AtomicString("inner_input_id"))));
+
+  inner_input.SetSuggestedValue("suggestion");
+
+  // Elements inside an iframe slotted inside a canvas should have suggestions
+  // suppressed.
+  EXPECT_TRUE(inner_input.SuggestedValue().IsEmpty());
+}
+
+TEST_F(WebFormControlElementTest,
+       TextControlPreviewDisabledWhenIframeDynamicallySlottedIntoCanvas) {
+  ScopedCanvasDrawElementForTest forced_canvas_draw_element_feature(true);
+
+  frame_test_helpers::WebViewHelper web_view_helper;
+  web_view_helper.Initialize();
+
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.