CVE-2026-76031
Overview
Background
- Boundary events
pointerout/pointerover/pointerleave/pointerenter(and the mouse equivalents) dispatched when the pointer crosses between elements; dispatched byBoundaryEventDispatcher(core/input/boundary_event_dispatcher.cc).- `relatedTarget`
- The DOM node the pointer is coming from or going to, exposed to script on
MouseEvent/PointerEvent/FocusEvent. It must not reveal a node belonging to a different origin. - Local-root pointer stream
- The touch/pointer stream is owned by the local-root frame’s
PointerEventManager, which hit-tests through every same-process child frame; a boundary crossing can therefore straddle two documents of different origins in the same renderer process. - `ExecutionContext` / `SecurityOrigin::CanAccess`
- Each node’s context carries a
SecurityOrigin;CanAccessdecides cross-origin accessibility. - `ClearCrossOriginRelatedTarget` feature
- New runtime-enabled killswitch (status: stable) guarding the fix.
Root Cause Analysis
When a boundary crossing moved the pointer between two same-process documents of different origins, BoundaryEventDispatcher::SendBoundaryEvents passed the real exited_target/entered_target nodes straight through as each event’s relatedTarget. Because the pointer stream is owned by the local root and hit-tests across child frames, the pointerout/pointerover/pointerleave/pointerenter event delivered into one document could carry, as relatedTarget, a live Node from a cross-origin document — directly exposing a cross-origin DOM node reference to script. There was no origin check on the dispatch path, nor a backstop on the event objects themselves.
The patch adds two layers. In BoundaryEventDispatcher::SendBoundaryEvents, when exited_context != entered_context and the two security origins cannot mutually CanAccess, it no longer forwards the raw nodes: it computes a replacement via FindFrameOwnerIfNested (so an outer document legitimately sees the frame-owner <iframe> element that contains the inner document) and otherwise uses null, routing through the new SendBoundaryEventsSameOrigin helper. As defense-in-depth, a new UIEvent::ClearCrossOriginRelatedTarget is invoked from MouseEvent::DispatchEvent, PointerEvent::DispatchEvent, and FocusEvent::DispatchEvent; it nulls related_target_ whenever the dispatch target’s context and the related target’s context differ and !target_origin->CanAccess(related_origin). The MouseEvent change also hoists related_target_ into the base MouseEvent member layout so the shared clearing logic applies uniformly.
The advisory labels this “Use after free in Input,” but the diff substantiates a cross-origin information-exposure fix: it prevents handing a cross-origin Node/EventTarget to script via relatedTarget. No freed-memory access, allocator interaction, or lifetime bug is shown; the related_target_ member move is a layout/ownership tidy-up, not a UAF fix. The freed-memory/escalation framing in the advisory is not supported by this commit (inferred discrepancy).
relatedTarget node from a cross-origin document to script, because the dispatcher never checked whether the exited and entered targets shared an accessible origin.Attack Path
- Nest/adjoin a cross-origin document Attacker page embeds a cross-origin iframe (or arranges sibling frames) that renders in the same process and participates in hit-testing (inferred; the diff targets the inner/outer and sibling cases explicitly).
- Move the pointer across the boundary
The user (or an automated pointer/touch action) crosses from the attacker document into the cross-origin frame’s area, generating
pointerout/pointerover(and enter/leave) events. - Receive the cross-origin node
Pre-patch, the event delivered to the attacker’s listener exposes the cross-origin frame’s node as
event.relatedTarget. - Read cross-origin DOM info
The attacker inspects the leaked
Nodereference to infer cross-origin layout/identity information that the same-origin policy should hide (cross-origin information disclosure).
Impact Assessment
relatedTarget on boundary/mouse/focus events. It is gated on same-process cross-origin frames and a pointer/focus boundary crossing. This is a same-origin-policy violation, not a use-after-free and not RCE; the advisory’s “Use after free” classification is not substantiated by the code changes. Because the fix is behind the ClearCrossOriginRelatedTarget stable killswitch, downstream builds that disable it remain exposed.Changed Functions
| Function | Change | Notes |
|---|---|---|
synthetic_event_type_third_party/blink/renderer/core/events/mouse_event.cc |
modified | |
menu_source_type_third_party/blink/renderer/core/events/mouse_event.cc |
modified | |
ifthird_party/blink/renderer/core/events/ui_event.cc |
modified | |
InputDeviceCapabilitiesthird_party/blink/renderer/core/events/ui_event.h |
modified | |
Nodethird_party/blink/renderer/core/events/ui_event.h |
modified | |
ifthird_party/blink/renderer/core/input/boundary_event_dispatcher.cc |
modified |
Files Changed
third_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/renderer/core/events/mouse_event.ccthird_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.htmlthird_party/blink/renderer/core/events/pointer_event.ccthird_party/blink/renderer/core/input/boundary_event_dispatcher.ccthird_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.htmlthird_party/blink/renderer/core/input/boundary_event_dispatcher.hthird_party/blink/renderer/core/events/ui_event.hthird_party/blink/renderer/core/events/focus_event.ccthird_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.htmlthird_party/blink/renderer/core/events/ui_event.ccthird_party/blink/renderer/core/events/mouse_event.h
Audit Directions
- Other cross-document event fieldsAudit any event attribute that can carry a
Node/EventTargetacross documents (relatedTargetonFocusEvent, drag targets,composedPath()leakage) for an origin check. - Local-root hit-testing pathsReview every consumer of the local-root
PointerEventManagerhit-test results that can straddle child-frame origins. - Killswitch-off behaviorConfirm no release channel ships with
ClearCrossOriginRelatedTargetdisabled, and that the planned follow-up (cross-document, not merely cross-origin) does not regress the check. - Same-origin cross-document exposureThe commit deliberately keeps exposing related targets across same-origin documents (tentative WPT); audit whether that still leaks sensitive cross-document references.
Patch
From bb79a99bd4473e26031cd5433e34d047fa527f3c Mon Sep 17 00:00:00 2001 From: David Baron <[email protected]> Date: Thu, 22 Oct 2026 15:33:17 -0700 Subject: [PATCH] Don't send a cross-origin relatedTarget on a PointerEvent boundary event, and add more general defense-in-depth for relatedTarget. This prevents exposing a Node from another origin as the relatedTarget on pointerout, pointerover, pointerleave, and pointerenter events. The relatedTarget is instead null, except that when the two documents have an inner/outer relationship in the frame hierarchy, the events in the outer document use the frame owner element (e.g., iframe) in that document that contains the inner document as the relatedTarget. Same-origin cross-document boundary events retain the existing behavior of exposing the related target Node across documents, which is tested in a tentative WPT. It's possible that we should also change this behavior, though, depending on how other engines behave. This also includes a second defense-in-depth change to check relatedTarget for all event types (MouseEvent, PointerEvent, and FocusEvent) that report it to script (not just this particular dispatching path) and set it to null if it is a cross-origin node. Both of these behavior changes are guarded by the `ClearCrossOriginRelatedTarget` runtime-enabled feature (status: stable). As a followup (to roll out via the normal release branch and not be merged to older branches) I intend to change the cross-origin check to be just a cross-document check, which will match the behavior of other browser engines. This two-stage change will reduce the compatibility risk of the change that needs merging. Fixed: 572556403 Bug: 574379564, 574843953 Force-WPT-Export: true Change-Id: I123aff9643f43fd2de45dae1586f4d2cb2d635a9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8611718 Reviewed-by: Mustaq Ahmed <[email protected]> Commit-Queue: David Baron <[email protected]> Cr-Commit-Position: refs/heads/main@{#1725257} --- diff --git a/third_party/blink/renderer/core/events/focus_event.cc b/third_party/blink/renderer/core/events/focus_event.cc index 72b492c..e843393dd 100644 --- a/third_party/blink/renderer/core/events/focus_event.cc +++ b/third_party/blink/renderer/core/events/focus_event.cc @@ -72,6 +72,7 @@ } DispatchEventResult FocusEvent::DispatchEvent(EventDispatcher& dispatcher) { + ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); GetEventPath().AdjustForRelatedTarget(dispatcher.GetNode(), relatedTarget()); return dispatcher.Dispatch(); } diff --git a/third_party/blink/renderer/core/events/mouse_event.cc b/third_party/blink/renderer/core/events/mouse_event.cc index 19f0f660..8323d76 100644 --- a/third_party/blink/renderer/core/events/mouse_event.cc +++ b/third_party/blink/renderer/core/events/mouse_event.cc @@ -24,10 +24,12 @@ #include "third_party/blink/public/common/input/web_pointer_properties.h" #include "third_party/blink/renderer/bindings/core/v8/v8_mouse_event_init.h" +#include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/events/event_dispatcher.h" #include "third_party/blink/renderer/core/dom/events/event_path.h" #include "third_party/blink/renderer/core/event_interface_names.h" +#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" @@ -134,7 +136,6 @@ : position_type_(PositionType::kPosition), button_(0), buttons_(0), - related_target_(nullptr), synthetic_event_type_(kRealOrIndistinguishable) {} MouseEvent::MouseEvent(const AtomicString& event_type, @@ -146,13 +147,13 @@ : UIEventWithKeyState(event_type, initializer, platform_time_stamp), screen_x_(initializer->screenX()), screen_y_(initializer->screenY()), + related_target_(initializer->relatedTarget()), movement_delta_(initializer->movementX(), initializer->movementY()), position_type_(synthetic_event_type == kPositionless ? PositionType::kPositionless : PositionType::kPosition), button_(initializer->button()), buttons_(initializer->buttons()), - related_target_(initializer->relatedTarget()), synthetic_event_type_(synthetic_event_type), menu_source_type_(menu_source_type) { InitCoordinates(initializer->clientX(), initializer->clientY(), @@ -388,6 +389,7 @@ } DispatchEventResult MouseEvent::DispatchEvent(EventDispatcher& dispatcher) { + ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); // TODO(mustaq): Move click-specific code to `PointerEvent::DispatchEvent`. GetEventPath().AdjustForRelatedTarget(dispatcher.GetNode(), relatedTarget()); diff --git a/third_party/blink/renderer/core/events/mouse_event.h b/third_party/blink/renderer/core/events/mouse_event.h index f79d9c6c..e28277c 100644 --- a/third_party/blink/renderer/core/events/mouse_event.h +++ b/third_party/blink/renderer/core/events/mouse_event.h @@ -205,6 +205,7 @@ double offset_x_ = 0; double offset_y_ = 0; + Member<EventTarget> related_target_; bool has_cached_relative_position_ = false; private: @@ -236,7 +237,6 @@ PositionType position_type_; int16_t button_; uint16_t buttons_; - Member<EventTarget> related_target_; SyntheticEventType synthetic_event_type_; // Only used for contextmenu events. diff --git a/third_party/blink/renderer/core/events/pointer_event.cc b/third_party/blink/renderer/core/events/pointer_event.cc index 9d3be0af..d738ef6f 100644 --- a/third_party/blink/renderer/core/events/pointer_event.cc +++ b/third_party/blink/renderer/core/events/pointer_event.cc @@ -193,6 +193,7 @@ DCHECK(!RawTarget() || RawTarget() != relatedTarget()); + ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); GetEventPath().AdjustForRelatedTarget(dispatcher.GetNode(), relatedTarget()); return dispatcher.Dispatch(); diff --git a/third_party/blink/renderer/core/events/ui_event.cc b/third_party/blink/renderer/core/events/ui_event.cc index b95fa8d..1876415 100644 --- a/third_party/blink/renderer/core/events/ui_event.cc +++ b/third_party/blink/renderer/core/events/ui_event.cc @@ -22,8 +22,12 @@ #include "third_party/blink/renderer/core/events/ui_event.h" +#include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/core/event_interface_names.h" +#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/input/input_device_capabilities.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" namespace blink { @@ -101,4 +105,23 @@ Event::Trace(visitor); } +void UIEvent::ClearCrossOriginRelatedTarget( + const Node& target, + Member<EventTarget>& related_target) { + if (!RuntimeEnabledFeatures::ClearCrossOriginRelatedTargetEnabled() || + !related_target) { + return; + } + const ExecutionContext* target_context = target.GetExecutionContext(); + const ExecutionContext* related_context = + related_target->GetExecutionContext(); + if (target_context && related_context && target_context != related_context) { + const SecurityOrigin* target_origin = target_context->GetSecurityOrigin(); + const SecurityOrigin* related_origin = related_context->GetSecurityOrigin(); + if (!target_origin->CanAccess(related_origin)) { + related_target = nullptr; + } + } +} + } // namespace blink diff --git a/third_party/blink/renderer/core/events/ui_event.h b/third_party/blink/renderer/core/events/ui_event.h index bf74086..67b3a6d 100644 --- a/third_party/blink/renderer/core/events/ui_event.h +++ b/third_party/blink/renderer/core/events/ui_event.h @@ -33,6 +33,7 @@ namespace blink { class InputDeviceCapabilities; +class Node; // FIXME: Get rid of this type alias. using AbstractView = DOMWindow; @@ -91,6 +92,12 @@ void Trace(Visitor*) const override; + protected: + // Defense in depth: an event must never hand an EventTarget of a + // cross-origin ExecutionContext to the target's context via `relatedTarget`. + void ClearCrossOriginRelatedTarget(const Node& target, + Member<EventTarget>& related_target); + private: Member<AbstractView> view_; int detail_; diff --git a/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc b/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc index a597ad93..af2f952 100644 --- a/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc +++ b/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc @@ -6,12 +6,44 @@ #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/execution_context/execution_context.h" +#include "third_party/blink/renderer/core/frame/frame.h" +#include "third_party/blink/renderer/core/frame/local_dom_window.h" +#include "third_party/blink/renderer/core/frame/local_frame.h" +#include "third_party/blink/renderer/core/html/html_frame_owner_element.h" #include "third_party/blink/renderer/core/input/event_handling_util.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" namespace blink { namespace { +HTMLFrameOwnerElement* FindFrameOwnerIfNested( + const ExecutionContext& possible_outer_context, + const ExecutionContext& possible_inner_context) { + const auto* possible_outer_window = + DynamicTo<LocalDOMWindow>(possible_outer_context); + const auto* possible_inner_window = + DynamicTo<LocalDOMWindow>(possible_inner_context); + if (!possible_outer_window || !possible_inner_window) { + return nullptr; + } + const Frame* possible_outer_frame = possible_outer_window->GetFrame(); + const Frame* possible_inner_frame = possible_inner_window->GetFrame(); + if (!possible_outer_frame || !possible_inner_frame) { + return nullptr; + } + const Frame* child_frame = possible_inner_frame; + while (child_frame && child_frame->Parent() != possible_outer_frame) { + child_frame = child_frame->Parent(); + } + if (!child_frame) { + return nullptr; + } + return DynamicTo<HTMLFrameOwnerElement>(child_frame->Owner()); +} + void BuildAncestorChain(EventTarget* target, HeapVector<Member<Node>, 20>* ancestors) { if (!event_handling_util::IsInDocument(target)) @@ -59,6 +91,51 @@ EventTarget* exited_target, bool original_exited_target_removed, EventTarget* entered_target) { + // An EventTarget must never be exposed to a cross-origin ExecutionContext + // through `relatedTarget` (e.g. the touch pointer stream is owned by the + // local-root frame's PointerEventManager, which hit-tests through every + // same-process child frame). + // When the contexts have an inner/outer relationship in the frame hierarchy, + // the event in the outer document uses the frame owner (e.g. iframe) element + // that lives in that context as its relatedTarget. In other cases (e.g. for + // the event in the inner document, or sibling/cousin frames), the related + // target is null. + if (RuntimeEnabledFeatures::ClearCrossOriginRelatedTargetEnabled() && + exited_target && entered_target) { + const ExecutionContext* exited_context = + exited_target->GetExecutionContext(); + const ExecutionContext* entered_context = + entered_target->GetExecutionContext(); + if (exited_context && entered_context && + exited_context != entered_context) { + const SecurityOrigin* exited_origin = exited_context->GetSecurityOrigin(); + const SecurityOrigin* entered_origin = + entered_context->GetSecurityOrigin(); + if (!exited_origin->CanAccess(entered_origin) || + !entered_origin->CanAccess(exited_origin)) { + EventTarget* exited_related_target = + FindFrameOwnerIfNested(*exited_context, *entered_context); + EventTarget* entered_related_target = + FindFrameOwnerIfNested(*entered_context, *exited_context); + SendBoundaryEventsSameOrigin(exited_target, + original_exited_target_removed, + exited_related_target); + SendBoundaryEventsSameOrigin(entered_related_target, + /*original_exited_target_removed=*/false, + entered_target); + return; + } + } + } + + SendBoundaryEventsSameOrigin(exited_target, original_exited_target_removed, + entered_target); +} + +void BoundaryEventDispatcher::SendBoundaryEventsSameOrigin( + EventTarget* exited_target, + bool original_exited_target_removed, + EventTarget* entered_target) { if (exited_target == entered_target && !original_exited_target_removed) { return; } diff --git a/third_party/blink/renderer/core/input/boundary_event_dispatcher.h b/third_party/blink/renderer/core/input/boundary_event_dispatcher.h index 1401a28..5f1e171f 100644 --- a/third_party/blink/renderer/core/input/boundary_event_dispatcher.h +++ b/third_party/blink/renderer/core/input/boundary_event_dispatcher.h @@ -38,6 +38,10 @@ bool check_for_listener) = 0; private: + void SendBoundaryEventsSameOrigin(EventTarget* exited_target, + bool original_exited_target_removed, + EventTarget* entered_target); + const AtomicString& over_event_; const AtomicString& out_event_; const AtomicString& enter_event_; diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5 index 224d1924..deb49c66 100644 --- a/third_party/blink/renderer/platform/runtime_enabled_features.json5 +++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5 @@ -1270,6 +1270,13 @@ status: "stable", }, { + // Killswitch for preventing cross-origin relatedTarget on boundary + // events. + // https://issues.chromium.org/issues/574379564 + name: "ClearCrossOriginRelatedTarget", + status: "stable", + }, + { // Set currentTarget to null after dispatch for EventTarget. // This was added in M142 and can be removed after M144. // https://issues.chromium.org/issues/346766786 diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html new file mode 100644 index 0000000..291b07d --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html @@ -0,0 +1,349 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Pointer boundary events across same-origin documents (tentative)</title> +<!-- + +Issue: https://github.com/w3c/pointerevents/issues/656 + +This test is tentative because I'm not sure whether it's the correct behavior. +It's possible that cross-document should be treated the same as cross-origin. + +pointerevent_boundary_events_cross_origin.html tests that the cross-origin +cases do not expose the true relatedTarget node (which is required for +security). This test tests that the same-origin but cross-document cases do +expose the relatedTarget node. + +--> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<style> + html, body { + margin: 0; + padding: 0; + touch-action: none; + } + .target { + width: 100px; + height: 100px; + background: blue; + touch-action: none; + } + iframe { + width: 100px; + height: 100px; + border: none; + display: block; + } + #done { + display: block; + margin-top: 10px; + touch-action: none; + } +</style> +<body> +<button id="done">Done</button> +<script> +'use strict'; + +function loadIframe(iframe) { + return new Promise(resolve => { + iframe.srcdoc = `<!doctype html> + <style> + html, body { margin: 0; padding: 0; touch-action: none; } + #child-target { width: 100px; height: 100px; background: green; touch-action: none; } + </style> + <div id="child-target"></div>`; + iframe.onload = resolve; + }); ... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html
new file mode 100644
index 0000000..291b07d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html
@@ -0,0 +1,349 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Pointer boundary events across same-origin documents (tentative)</title>
+<!--
+
+Issue: https://github.com/w3c/pointerevents/issues/656
+
+This test is tentative because I'm not sure whether it's the correct behavior.
+It's possible that cross-document should be treated the same as cross-origin.
+
+pointerevent_boundary_events_cross_origin.html tests that the cross-origin
+cases do not expose the true relatedTarget node (which is required for
+security). This test tests that the same-origin but cross-document cases do
+expose the relatedTarget node.
+
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ touch-action: none;
+ }
+ .target {
+ width: 100px;
+ height: 100px;
+ background: blue;
+ touch-action: none;
+ }
+ iframe {
+ width: 100px;
+ height: 100px;
+ border: none;
+ display: block;
+ }
+ #done {
+ display: block;
+ margin-top: 10px;
+ touch-action: none;
+ }
+</style>
+<body>
+<button id="done">Done</button>
+<script>
+'use strict';
+
+function loadIframe(iframe) {
+ return new Promise(resolve => {
+ iframe.srcdoc = `<!doctype html>
+ <style>
+ html, body { margin: 0; padding: 0; touch-action: none; }
+ #child-target { width: 100px; height: 100px; background: green; touch-action: none; }
+ </style>
+ <div id="child-target"></div>`;
+ iframe.onload = resolve;
+ });
+}
+
+async function tapDoneButton() {
+ const doneButton = document.getElementById('done');
+ const donePromise = new Promise(resolve => {
+ const onDone = () => {
+ doneButton.removeEventListener('pointerup', onDone);
+ doneButton.removeEventListener('click', onDone);
+ resolve();
+ };
+ doneButton.addEventListener('pointerup', onDone);
+ doneButton.addEventListener('click', onDone);
+ });
+ await new test_driver.Actions()
+ .addPointer('touchPointerDone', 'touch')
+ .pointerMove(0, 0, {origin: doneButton})
+ .pointerDown()
+ .pointerUp()
+ .send();
+ await donePromise;
+}
+
+promise_test(async t => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ t.add_cleanup(() => container.remove());
+
+ const parentTarget = document.createElement('div');
+ parentTarget.id = 'parent-target';
+ parentTarget.className = 'target';
+ container.appendChild(parentTarget);
+
+ const iframe = document.createElement('iframe');
+ iframe.id = 'iframe';
+ container.appendChild(iframe);
+
+ await loadIframe(iframe);
+ const childDoc = iframe.contentDocument;
+ const childTarget = childDoc.getElementById('child-target');
+
+ let parentEvents = [];
+ let childEvents = [];
+
+ parentTarget.addEventListener('pointerdown', e => {
+ e.target.releasePointerCapture(e.pointerId);
+ });
+ parentTarget.addEventListener('pointerout', e => {
+ parentEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ parentTarget.addEventListener('pointerleave', e => {
+ parentEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ childTarget.addEventListener('pointerover', e => {
+ childEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ childTarget.addEventListener('pointerenter', e => {
+ childEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ await new test_driver.Actions()
+ .addPointer('touchPointer', 'touch')
+ .pointerMove(0, 0, {origin: parentTarget})
+ .pointerDown()
+ .pointerMove(0, 0, {origin: iframe})
+ .pointerUp()
+ .send();
+
+ await tapDoneButton();
+
+ assert_equals(
+ parentEvents.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerout from child-target,pointerleave from child-target"
+ );
+
+ assert_equals(
+ childEvents.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerover from parent-target,pointerenter from parent-target"
+ );
+}, 'Parent to same-origin iframe: relatedTarget is exposed across documents');
+
+promise_test(async t => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ t.add_cleanup(() => container.remove());
+
+ const parentTarget = document.createElement('div');
+ parentTarget.id = 'parent-target';
+ parentTarget.className = 'target';
+ container.appendChild(parentTarget);
+
+ const iframe = document.createElement('iframe');
+ iframe.id = 'iframe';
+ container.appendChild(iframe);
+
+ await loadIframe(iframe);
+ const childDoc = iframe.contentDocument;
+ const childTarget = childDoc.getElementById('child-target');
+
+ let parentEvents = [];
+ let childEvents = [];
+
+ childTarget.addEventListener('pointerdown', e => {
+ e.target.releasePointerCapture(e.pointerId);
+ });
+ childTarget.addEventListener('pointerout', e => {
+ childEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ childTarget.addEventListener('pointerleave', e => {
+ childEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ parentTarget.addEventListener('pointerover', e => {
+ parentEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ parentTarget.addEventListener('pointerenter', e => {
+ parentEvents.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ await new test_driver.Actions()
+ .addPointer('touchPointer', 'touch')
+ .pointerMove(0, 0, {origin: iframe})
+ .pointerDown()
+ .pointerMove(0, 0, {origin: parentTarget})
+ .pointerUp()
+ .send();
+
+ await tapDoneButton();
+
+ assert_equals(
+ childEvents.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerout from parent-target,pointerleave from parent-target"
+ );
+
+ assert_equals(
+ parentEvents.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerover from child-target,pointerenter from child-target"
+ );
+}, 'Same-origin iframe to parent: relatedTarget is exposed across documents');
+
+promise_test(async t => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ t.add_cleanup(() => container.remove());
+
+ const sib1 = document.createElement('iframe');
+ container.appendChild(sib1);
+
+ const sib2 = document.createElement('iframe');
+ container.appendChild(sib2);
+
+ await Promise.all([
+ new Promise(resolve => {
+ sib1.srcdoc = `<!doctype html>
+ <style>html, body { margin: 0; padding: 0; touch-action: none; }
+ #s1 { width: 100px; height: 100px; background: green; touch-action: none; }</style>
+ <div id="s1"></div>`;
+ sib1.onload = resolve;
+ }),
+ new Promise(resolve => {
+ sib2.srcdoc = `<!doctype html>
+ <style>html, body { margin: 0; padding: 0; touch-action: none; }
+ #s2 { width: 100px; height: 100px; background: red; touch-action: none; }</style>
+ <div id="s2"></div>`;
+ sib2.onload = resolve;
+ })
+ ]);
+
+ const s1 = sib1.contentDocument.getElementById('s1');
+ const s2 = sib2.contentDocument.getElementById('s2');
+
+ let s1Events = [];
+ let s2Events = [];
+
+ s1.addEventListener('pointerdown', e => {
+ e.target.releasePointerCapture(e.pointerId);
+ });
+ s1.addEventListener('pointerout', e => {
+ s1Events.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ s1.addEventListener('pointerleave', e => {
+ s1Events.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ s2.addEventListener('pointerover', e => {
+ s2Events.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+ s2.addEventListener('pointerenter', e => {
+ s2Events.push({ type: e.type, relatedTarget: e.relatedTarget });
+ });
+
+ await new test_driver.Actions()
+ .addPointer('touchPointer', 'touch')
+ .pointerMove(0, 0, {origin: sib1})
+ .pointerDown()
+ .pointerMove(0, 0, {origin: sib2})
+ .pointerUp()
+ .send();
+
+ await tapDoneButton();
+
+ assert_equals(
+ s1Events.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerout from s2,pointerleave from s2"
+ );
+
+ assert_equals(
+ s2Events.map(e => `${e.type} from ${e.relatedTarget.id}`).join(),
+ "pointerover from s1,pointerenter from s1"
+ );
+}, 'Sibling same-origin iframes: relatedTarget is exposed across documents');
+
+promise_test(async t => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ t.add_cleanup(() => container.remove());
+
+ const topTarget = document.createElement('div');
+ topTarget.id = 'top-target-nested';
+ topTarget.className = 'target';
+ topTarget.style.background = 'cyan';
+ container.appendChild(topTarget);
+
+ const outerIframe = document.createElement('iframe');
+ container.appendChild(outerIframe);
+
+ await new Promise(resolve => {
+ outerIframe.srcdoc = `<!doctype html>
+ <style>html, body { margin: 0; padding: 0; touch-action: none; }
+ iframe { width: 100px; height: 100px; border: none; display: block; }</style>
+ <iframe id="inner-iframe" srcdoc="<!doctype html>
+ <style>html, body { margin: 0; padding: 0; touch-action: none; }
+ #deep { width: 100px; height: 100px; background: orange; touch-action: none; }</style>
+ <div id='deep'></div>"></iframe>`;
... (truncated)