Chrome · DOM
CVE-2026-79271
Logic Error in DOM
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/dom/element.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/resolver/style_adjuster.ccthird_party/blink/renderer/core/dom/element.ccthird_party/blink/renderer/core/dom/flat_tree_traversal.h
Patch
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.
References
On This Page