CVE-2026-79271
Overview
Background
- `CanvasDrawElement`
- the (feature-gated) capability that lets a
<canvas>draw its DOM child elements as an image; because that image can be read back, children in a canvas subtree get privacy-preserving painting. - `IsCanvasOrInCanvasSubtree` flag
- a per-
Elementbit (ElementFlags::kIsCanvasOrInCanvasSubtree) marking that an element is a canvas or lives under one, which triggers privacy protections such as suppressing autofill preview/suggested values, ignoring:visitedlink styling, and setting paint flags for cross-origin images. - Flat tree vs. DOM tree
- shadow DOM slotting distributes light-DOM children into
<slot>s so an element’s flat-tree parent (its rendering parent) can differ from its DOMparentElement()/shadow host;FlatTreeTraversalwalks the flat tree. - Slotting status change
- when an element’s assigned slot changes (via slotting,
moveBefore, etc.), its flat-tree parent changes, which must re-evaluate whether it is now inside a canvas subtree.
Root Cause Analysis
The canvas-subtree membership logic reasoned about the DOM tree rather than the flat tree, so shadow-DOM content slotted into a canvas was not recognized as being in the canvas subtree. Element::IsInCanvasSubtree() computed its parent via ParentOrShadowHostElement(), and callers like StyleAdjuster, HTMLCanvasElement, CompositingReasonFinder, and PaintPropertyTreeBuilder used element->parentElement()/object.Parent(), none of which follow slot distribution; consequently an element assigned into a <slot> under a <canvas layoutsubtree> kept IsCanvasOrInCanvasSubtree() false. This violated the invariant that every element rendered inside a canvas subtree carries the canvas-subtree bit, so the privacy-preserving painting (autofill suppression, :visited handling, cross-origin image paint flags) was not applied to slotted content that a page could then render into the canvas and read back.
The fix adds FlatTreeTraversal::ParentElementSkippingSlots and rewires IsInCanvasSubtree() and all those call sites to use it; makes Element::SetIsCanvasOrInCanvasSubtree recurse through shadow roots, slot-assigned nodes (AssignedNodesNoRecalc()), and light-DOM children; and updates Node::FlatTreeParentChanged/Node::RemovedFromFlatTree to recompute the flag whenever an element joins/leaves the flat tree, so slotting changes propagate the correct state.
FlatTreeTraversal::ParentElementSkippingSlots and propagates the IsCanvasOrInCanvasSubtree flag through slots on every flat-tree-parent change.Attack Path
- Enable canvas-drawn DOM
On a page with
CanvasDrawElementenabled, create a<canvas layoutsubtree>in a shadow root containing a<slot>. - Slot sensitive content into the canvas
Assign light-DOM content carrying privacy-sensitive rendering into that slot — e.g. form controls with autofill suggested values,
:visitedlinks, or cross-origin images — so it renders inside the canvas subtree via the flat tree but keepsIsCanvasOrInCanvasSubtree()false due to the DOM-tree-based check. - Miss the privacy protections
Because the element is not marked in-canvas, autofill previews are not suppressed,
:visitedstyling is not neutralized, and cross-origin image paint flags are not set (the regression tests show slottedinput/textarea/selectpreviews leaking pre-fix). - Read back the canvas Draw the element into the canvas and read the pixels from JavaScript to recover the otherwise-protected information (visited-link state, cross-origin image content, or autofill suggestions), per the advisory’s information-leak classification.
Impact Assessment
CanvasDrawElement feature to be enabled. No further escalation is claimed, and the advisory does not indicate in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/dom/element.cc |
modified | |
ifthird_party/blink/renderer/core/html/canvas/html_canvas_element.cc |
modified | |
ifthird_party/blink/renderer/core/paint/paint_property_tree_builder.cc |
modified |
Files Changed
third_party/blink/renderer/core/paint/compositing/compositing_reason_finder_test.ccthird_party/blink/renderer/core/html/html_slot_element.hthird_party/blink/renderer/core/exported/web_form_control_element_test.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/visited-link-slotted-color-ignored.tentative.htmlthird_party/blink/renderer/core/dom/flat_tree_traversal.hthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/resources/CanvasWidget.jsthird_party/blink/renderer/core/dom/element.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/canvas-in-custom-element-multiple-ref.htmlthird_party/blink/renderer/core/dom/node.ccthird_party/blink/renderer/core/html/media/html_video_element_test.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/support/CanvasWidgetWithImages.https.sub.jsthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/shadow-dom-slotted.tentative.htmlthird_party/blink/renderer/core/html/canvas/html_canvas_element_test.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/canvas-in-custom-element.tentative.htmlthird_party/blink/renderer/core/html/html_frame_owner_element.ccthird_party/blink/renderer/core/paint/paint_property_tree_builder.ccthird_party/blink/renderer/core/paint/compositing/compositing_reason_finder.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/shadow-dom-slot-canvas-child.tentative.htmlthird_party/blink/renderer/core/html/canvas/html_canvas_element.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/canvas-in-custom-element-multiple.tentative.htmlthird_party/blink/renderer/core/css/resolver/style_adjuster.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/canvas-in-custom-element-images-ignored.tentative.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/shadow-dom-slotted-moveBefore.tentative.html
Audit Directions
- DOM-tree parent used where flat tree is requiredSearch for
parentElement(),ParentOrShadowHostElement(), orobject.Parent()used to decide rendering/privacy state; slotted/shadow content needsFlatTreeTraversal-based parents. - State bits that must propagate on slotting changesAudit inheritable element flags (like
kIsCanvasOrInCanvasSubtree) to confirm they are recomputed inFlatTreeParentChanged/RemovedFromFlatTreeand recurse through slots, not just DOM children. - Privacy protections gated on subtree membershipReview autofill suppression,
:visitedhandling, and cross-origin image paint flags for other membership predicates that could be evaded by shadow-DOM slotting ormoveBeforere-parenting.
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..92eaeb22 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" namespace blink { @@ -73,6 +74,9 @@ static ContainerNode* Parent(const Node&); static Element* ParentElement(const Node&); + // Returns the flat tree parent element, skipping over any HTMLSlotElements. + // Used to correctly determine if a node is a child of a canvas element. + static Element* ParentElementSkippingSlots(const Node&); // Return the passed in Node if it is an Element, otherwise return the // ParentElement() static const Element* InclusiveParentElement(const Node&); @@ -215,6 +219,15 @@ return DynamicTo<Element>(FlatTreeTraversal::Parent(node)); } +inline Element* FlatTreeTraversal::ParentElementSkippingSlots( + const Node& node) { + Element* parent = FlatTreeTraversal::ParentElement(node); + while (parent && IsA<HTMLSlotElement>(parent)) { + parent = FlatTreeTraversal::ParentElement(*parent); + } + return parent; +} + inline Node* FlatTreeTraversal::NextSibling(const Node& node) { AssertPrecondition(node); Node* result = TraverseSiblings(node, kTraversalDirectionForward); diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc index 6936978b..4de5ac1b 100644 --- a/third_party/blink/renderer/core/dom/node.cc +++ b/third_party/blink/renderer/core/dom/node.cc @@ -3820,12 +3820,25 @@ // parent box may have changed. SetForceReattachLayoutTree(); } + if (auto* element = DynamicTo<Element>(this)) { + // Only set canvas subtree state for elements that are participating in + // the flat tree (i.e. not awaiting assignment) to avoid forcing assignment + // in the FlatTreeTraversal::ParentElement call inside IsInCanvasSubtree. + // We do not want to force assignment now because it interferes with + // moveBefore semantics. If an element is assigned a slot this method + // will be called again and the canvas flags will be set. + if (IsNodeInFlatTree(*this, GetStyleRecalcParent())) { + element->SetIsCanvasOrInCanvasSubtree(element->IsInCanvasSubtree()); + } + } } void Node::RemovedFromFlatTree() { if (Element* element = DynamicTo<Element>(this)) { element->DetachOverscroll(); + element->SetIsCanvasOrInCanvasSubtree(element->IsInCanvasSubtree()); } + StyleEngine& engine = GetDocument().GetStyleEngine(); StyleEngine::DetachLayoutTreeScope detach_scope(engine); // This node was previously part of the flat tree, but due to slot re- diff --git a/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc b/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc index 566bfb6b9..1fcbff7f 100644 --- a/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc +++ b/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc @@ -64,6 +64,7 @@ #include "third_party/blink/renderer/core/dom/dom_node_ids.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/element_traversal.h" +#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/fileapi/file.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" @@ -921,7 +922,9 @@ Element* element, const String& func_name, ExceptionState& exception_state) const { - if (element->parentElement() != this) { + const Element* parent = + FlatTreeTraversal::ParentElementSkippingSlots(*element); + if (parent != this) { exception_state.ThrowDOMException( DOMExceptionCode::kInvalidStateError, "Only immediate children of the <canvas> element can be passed to " + diff --git a/third_party/blink/renderer/core/html/html_frame_owner_element.cc b/third_party/blink/renderer/core/html/html_frame_owner_element.cc index 3d4668cc31..99407b2 100644 --- a/third_party/blink/renderer/core/html/html_frame_owner_element.cc +++ b/third_party/blink/renderer/core/html/html_frame_owner_element.cc @@ -239,32 +239,11 @@ return result; } -static void SetIsCanvasOrInCanvasSubtreeRecursively(Element& element, - bool is_in_canvas) { - if (IsA<HTMLCanvasElement>(element)) { - is_in_canvas = true; - } - if (element.IsCanvasOrInCanvasSubtree() == is_in_canvas) { - return; - } - element.SetIsCanvasOrInCanvasSubtree(is_in_canvas); - - if (ShadowRoot* shadow_root = element.GetShadowRoot()) { - for (Element& child : ElementTraversal::ChildrenOf(*shadow_root)) { - SetIsCanvasOrInCanvasSubtreeRecursively(child, is_in_canvas); - } - } - for (Element& child : ElementTraversal::ChildrenOf(element)) { - SetIsCanvasOrInCanvasSubtreeRecursively(child, is_in_canvas); - } -} - void HTMLFrameOwnerElement::DidChangeIsCanvasOrInCanvasSubtree() { HTMLElement::DidChangeIsCanvasOrInCanvasSubtree(); if (Document* inner_document = contentDocument()) { if (Element* root = inner_document->documentElement()) { - SetIsCanvasOrInCanvasSubtreeRecursively(*root, - IsCanvasOrInCanvasSubtree()); + root->SetIsCanvasOrInCanvasSubtree(IsCanvasOrInCanvasSubtree()); } } } diff --git a/third_party/blink/renderer/core/html/html_slot_element.h b/third_party/blink/renderer/core/html/html_slot_element.h index df3cf5d..25ea50c 100644 --- a/third_party/blink/renderer/core/html/html_slot_element.h +++ b/third_party/blink/renderer/core/html/html_slot_element.h @@ -55,6 +55,15 @@ const HeapVector<Member<Node>>& AssignedNodes() const; const HeapVector<Member<Node>> AssignedNodesForBinding( const AssignedNodesOptions*); + // Use AssignedNodes() in almost all cases. Only use AssignedNodesNoRecalc() + // when calling RecalcAssignment() is forbidden or dangerous (e.g., during + // tree modifications, moveBefore semantics, or state propagation like + // Element::SetIsCanvasOrInCanvasSubtree where eventual consistency is + // guaranteed by subsequent FlatTreeParentChanged/RemovedFromFlatTree + // hooks). + const HeapVector<Member<Node>>& AssignedNodesNoRecalc() const { + return assigned_nodes_; + } const HeapVector<Member<Element>> AssignedElements(); const HeapVector<Member<Element>> AssignedElementsForBinding( const AssignedNodesOptions*); diff --git a/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder.cc b/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder.cc index 6c6bc8b..88341a30 100644 --- a/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder.cc +++ b/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder.cc @@ -7,6 +7,7 @@ #include "third_party/blink/renderer/core/css/css_property_names.h" #include "third_party/blink/renderer/core/css/properties/css_bitset.h" #include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" @@ -381,8 +382,9 @@ object.GetDocument().GetExecutionContext())) { if (element->IsInCanvasSubtree() && !object.StyleRef().IsRenderedInTopLayer(*element)) [[unlikely]] { - auto* canvas_parent = - DynamicTo<HTMLCanvasElement>(element->parentElement()); + const Element* parent = + FlatTreeTraversal::ParentElementSkippingSlots(*element); + auto* canvas_parent = DynamicTo<HTMLCanvasElement>(parent); if (IsA<LayoutBox>(object) && canvas_parent && canvas_parent->layoutSubtree() && canvas_parent->GetLayoutObject() && canvas_parent->GetLayoutObject()->IsCanvas()) { diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc index fee7246..66d083c 100644 --- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc +++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc @@ -17,6 +17,7 @@ #include "third_party/blink/renderer/core/css/clip_path_paint_image_generator.h" #include "third_party/blink/renderer/core/css/properties/css_bitset.h" #include "third_party/blink/renderer/core/dom/dom_node_ids.h" +#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/pseudo_element.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame_client.h" @@ -583,6 +584,16 @@ return false; } +static HTMLCanvasElement* FindCanvasParent(const LayoutObject& object) { + const Element* element = DynamicTo<Element>(object.GetNode()); + if (!element) { + return nullptr; + } + const Element* parent = + FlatTreeTraversal::ParentElementSkippingSlots(*element); + return DynamicTo<HTMLCanvasElement>(const_cast<Element*>(parent)); +} + static bool NeedsPaintOffsetTranslation( const LayoutObject& object, CompositingReasons direct_compositing_reasons, @@ -610,7 +621,7 @@ // TODO(crbug.com/349835587): Should Element or LayoutObject have a public // IsCanvasDrawElementImage() function? - if (object.Parent() && object.Parent()->IsCanvas()) { + if (FindCanvasParent(object)) { // The object may be drawn with drawElementImage and should ignore the paint // offset. return true; @@ -1930,10 +1941,14 @@ ... (file diff truncated)
Regression Test / PoC
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)