CVE-2026-119269
Overview
Background
- `relatedTarget`
- For focus, mouse and pointer events, the secondary
EventTargetinvolved (e.g. the node the pointer moved away from). The same-origin policy forbids handing a page anEventTargetthat belongs to a cross-origin document. - `UIEvent::ClearCrossOriginRelatedTarget` / `CheckCrossOriginRelatedTarget`
- A defense-in-depth hook in
third_party/blink/renderer/core/events/ui_event.cc, called fromFocusEvent,MouseEventandPointerEventdispatch, meant to ensure a cross-originrelatedTargetis never exposed to the target’s execution context. - `ExecutionContext` / `SecurityOrigin`
- Each document/frame has an
ExecutionContextwith aSecurityOrigin. When a frame is detached (e.g. its iframe is removed mid-dispatch),GetExecutionContext()can returnnull, so origin comparison must handle the null case. - `BoundaryEventDispatcher`
- In
boundary_event_dispatcher.cc, computespointerover/pointerenter/pointerout/pointerleaveacross a boundary, choosing therelatedTargetfor each side; it must also respect the cross-origin boundary, including when a side’s context is gone. - Prior fix `bb79a99b...`
- An earlier commit added the cross-origin
relatedTargetclearing this change corrects; this patch targets the null-execution-context gap it left.
Root Cause Analysis
The earlier cross-origin guard in UIEvent::ClearCrossOriginRelatedTarget was written as if (target_context && related_context && target_context != related_context) { ... if (!CanAccess) related_target = nullptr; }. The leading target_context && related_context conjunction meant that when either execution context was null — the case where a frame has been detached, e.g. an iframe removed during pointer-event dispatch — the entire cross-origin check was skipped and the relatedTarget was left intact. As a result a relatedTarget belonging to a now-detached, cross-origin document could be delivered to the surviving (parent, different-origin) context, bypassing the same-origin boundary. The companion logic in BoundaryEventDispatcher::SendBoundaryEvents had the same shape (exited_context && entered_context && exited_context != entered_context), so a removed side skipped the sanitization entirely.
The patch renames the hook to CheckCrossOriginRelatedTarget and makes null-context handling explicit: it CHECK_EQ(!target_context, !related_context) (both present or both absent) and then, for differing contexts, CHECK(target_origin->CanAccess(related_origin)) — turning the old silent “set to null” into a hard defense-in-depth assertion, on the reasoning that if that invariant is ever actually violated the real fix belongs in the caller. In BoundaryEventDispatcher the rewrite computes is_same_origin only when both contexts exist and treats a missing context as cross-origin, routing it through SendBoundaryEventsSameOrigin with a nullptr related target (and the FindFrameOwnerIfNested substitutions guarded on both contexts being non-null). An early-return was also added for the degenerate exited_target == entered_target case. The new WPT tests exercise exactly this: an iframe removed on pointerdown, after which the parent’s relatedTarget must be null.
relatedTarget guard only fired when both execution contexts were non-null, so detaching one side (removing an iframe mid-dispatch) made the guard a no-op and let a cross-origin/detached relatedTarget reach a different-origin context. The advisory labels this a “Use after free in Blink”; the diff substantiates a same-origin-policy boundary failure on relatedTarget (treating a null/detached context as safe rather than cross-origin). A UAF of the leaked detached-frame node is plausible downstream but is (inferred) and not directly shown by this diff, which only tightens the origin/null check.Attack Path
- Nested cross-origin frame Attacker page embeds a cross-origin (or same-origin-but-separately-removable) iframe and sets up pointer capture inside the child, as in the added WPT tests.
- Trigger boundary dispatch
A touch pointer stream moves from the child frame to a parent target, generating boundary events whose
relatedTargetreferences a node in the child document. - Detach mid-dispatch
On
pointerdown, script removes the iframe, detaching the child’s execution context soGetExecutionContext()returns null during the subsequent boundary/pointer event dispatch. - Guard bypass
Because the old check required both contexts to be non-null, the cross-origin clearing is skipped and the parent receives the child-document node as
relatedTarget. - Primitive
Cross-origin / cross-document information disclosure: the attacker’s parent context obtains a reference to an
EventTargetfrom a detached cross-origin document (an SOP boundary bypass). Use-after-free of that detached node is (inferred) and not shown by the diff.
Impact Assessment
relatedTarget leaking into a different-origin execution context. The patch’s escalation to CHECK() indicates the project treats any such leak as a hard invariant violation. Whether the leaked reference yields a durable UAF (the advisory’s stated class) depends on frame-teardown lifetime and is (inferred), not demonstrated by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/events/ui_event.cc |
modified | |
ifthird_party/blink/renderer/core/input/boundary_event_dispatcher.cc |
modified | |
ifthird_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html |
modified |
Files Changed
third_party/blink/renderer/core/events/mouse_event.ccthird_party/blink/renderer/core/events/ui_event.hthird_party/blink/renderer/core/events/focus_event.ccthird_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.htmlthird_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.htmlthird_party/blink/renderer/core/events/pointer_event.ccthird_party/blink/renderer/core/input/boundary_event_dispatcher.ccthird_party/blink/renderer/core/events/ui_event.ccthird_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html
Audit Directions
- Null-`ExecutionContext` assumptions in origin checksGrep for
GetExecutionContext()comparisons guarded bya && b && a != b; detached frames produce null contexts that such guards silently treat as safe. Review other event/DOM paths for the same pattern. - Frame-removal races during event dispatchAudit pointer-capture and boundary-event code for state captured before an iframe is removed and used afterward (
relatedTarget, cached targets), both for cross-origin exposure and for dangling references to detached documents. - `relatedTarget` producersCheck every event type that populates
relatedTarget(focus, mouse, pointer, drag) routes through the corrected cross-origin check and that adjustment (AdjustForRelatedTarget) cannot reintroduce a cross-origin node.
Patch
From dbefa3bf41687456c245fcafe948d010047903e6 Mon Sep 17 00:00:00 2001 From: David Baron <[email protected]> Date: Tue, 27 Oct 2026 10:01:40 -0700 Subject: [PATCH] Correct cross-origin check for relatedTarget to handle null execution context. This fixes the cross-origin check for relatedTarget for both of the fixes in commit bb79a99bd4473e26031cd5433e34d047fa527f3c to handle null execution context better (by treating it as cross-origin). This also changes the defense-in-depth part of the fix to CHECK() rather than setting relatedTarget to null, since I think that's more appropriate for a defense-in-depth check (and if we're actually hitting it we may need to fix the callers that are causing it in other ways). Fixed: 577072000 Bug: 572556403, 574379564, 574843953, 577359831 Change-Id: I954eadb02e7a9bec59c0a0306d62cbdfad3543ea Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8641583 Commit-Queue: David Baron <[email protected]> Reviewed-by: Mustaq Ahmed <[email protected]> Cr-Commit-Position: refs/heads/main@{#1728234} --- diff --git a/third_party/blink/renderer/core/events/focus_event.cc b/third_party/blink/renderer/core/events/focus_event.cc index e843393dd..7d574f67 100644 --- a/third_party/blink/renderer/core/events/focus_event.cc +++ b/third_party/blink/renderer/core/events/focus_event.cc @@ -72,7 +72,7 @@ } DispatchEventResult FocusEvent::DispatchEvent(EventDispatcher& dispatcher) { - ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); + CheckCrossOriginRelatedTarget(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 8323d76..7f9b51bb 100644 --- a/third_party/blink/renderer/core/events/mouse_event.cc +++ b/third_party/blink/renderer/core/events/mouse_event.cc @@ -389,7 +389,7 @@ } DispatchEventResult MouseEvent::DispatchEvent(EventDispatcher& dispatcher) { - ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); + CheckCrossOriginRelatedTarget(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/pointer_event.cc b/third_party/blink/renderer/core/events/pointer_event.cc index d738ef6f..8595c9b7 100644 --- a/third_party/blink/renderer/core/events/pointer_event.cc +++ b/third_party/blink/renderer/core/events/pointer_event.cc @@ -193,7 +193,7 @@ DCHECK(!RawTarget() || RawTarget() != relatedTarget()); - ClearCrossOriginRelatedTarget(dispatcher.GetNode(), related_target_); + CheckCrossOriginRelatedTarget(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 1876415..4cc3a5a2 100644 --- a/third_party/blink/renderer/core/events/ui_event.cc +++ b/third_party/blink/renderer/core/events/ui_event.cc @@ -22,6 +22,7 @@ #include "third_party/blink/renderer/core/events/ui_event.h" +#include "base/check.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" @@ -105,9 +106,8 @@ Event::Trace(visitor); } -void UIEvent::ClearCrossOriginRelatedTarget( - const Node& target, - Member<EventTarget>& related_target) { +void UIEvent::CheckCrossOriginRelatedTarget(const Node& target, + EventTarget* related_target) { if (!RuntimeEnabledFeatures::ClearCrossOriginRelatedTargetEnabled() || !related_target) { return; @@ -115,12 +115,11 @@ const ExecutionContext* target_context = target.GetExecutionContext(); const ExecutionContext* related_context = related_target->GetExecutionContext(); - if (target_context && related_context && target_context != related_context) { + CHECK_EQ(!target_context, !related_context); + if (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; - } + CHECK(target_origin->CanAccess(related_origin)); } } diff --git a/third_party/blink/renderer/core/events/ui_event.h b/third_party/blink/renderer/core/events/ui_event.h index 67b3a6d..7aad9c36e 100644 --- a/third_party/blink/renderer/core/events/ui_event.h +++ b/third_party/blink/renderer/core/events/ui_event.h @@ -95,8 +95,8 @@ 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); + void CheckCrossOriginRelatedTarget(const Node& target, + EventTarget* related_target); private: Member<AbstractView> view_; 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 af2f952..4a9a651 100644 --- a/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc +++ b/third_party/blink/renderer/core/input/boundary_event_dispatcher.cc @@ -91,6 +91,10 @@ EventTarget* exited_target, bool original_exited_target_removed, EventTarget* entered_target) { + if (exited_target == entered_target && !original_exited_target_removed) { + return; + } + // 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 @@ -106,26 +110,35 @@ 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; + bool is_same_origin = false; + if (exited_context && entered_context) { + if (exited_context == entered_context) { + is_same_origin = true; + } else { + const SecurityOrigin* exited_origin = + exited_context->GetSecurityOrigin(); + const SecurityOrigin* entered_origin = + entered_context->GetSecurityOrigin(); + is_same_origin = exited_origin->CanAccess(entered_origin) && + entered_origin->CanAccess(exited_origin); } } + if (!is_same_origin) { + EventTarget* exited_related_target = + (exited_context && entered_context) + ? FindFrameOwnerIfNested(*exited_context, *entered_context) + : nullptr; + EventTarget* entered_related_target = + (exited_context && entered_context) + ? FindFrameOwnerIfNested(*entered_context, *exited_context) + : nullptr; + 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, 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 index 291b07d..a688d8e 100644 --- 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 @@ -203,6 +203,53 @@ 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 = []; + childTarget.addEventListener('pointerdown', e => { + e.target.releasePointerCapture(e.pointerId); + iframe.remove(); + }); + + parentTarget.addEventListener('pointerover', e => { + parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null }); + }); + parentTarget.addEventListener('pointerenter', e => { + parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null }); + }); + + 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( + parentEvents.map(e => `${e.type} (null: ${e.relatedTargetIsNull})`).join(), + "pointerover (null: true),pointerenter (null: true)" + ); +}, 'Same-origin iframe to parent with iframe removed: relatedTarget is null'); + +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); diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html index c6b2ad1..38b9f6cc 100644 --- a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html +++ b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html @@ -43,7 +43,7 @@ let receivedMessages = []; window.addEventListener('message', e => { - if (e.data && e.data.source === 'boundary-event-test' && e.data.type !== 'child-ready') { + if (e.data && e.data.source === 'boundary-event-test' && e.data.type !== 'child-ready' && e.data.type !== 'child-pointerdown') { receivedMessages.push({ frameId: e.data.frameId, type: e.data.type, @@ -281,5 +281,55 @@ { frameId: 'deep', type: 'pointerleave', relatedTargetIsNull: true } ]), 'receivedMessages should match expected deep child events'); }, 'Nested iframes: relatedTarget in outer document is outermost iframe, inner document is null'); + +promise_test(async t => { + receivedMessages = []; + 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); + + const childSrc = `${REMOTE_ORIGIN}${CHILD_PAGE}?frame_id=child&target_id=child-target&report_pointerdown=true`; + await loadChildFrame(iframe, childSrc, 'child'); + + let parentEvents = []; + ['pointerover', 'pointerenter'].forEach(type => { + parentTarget.addEventListener(type, e => { + parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null }); + }); + }); + + const onMessage = e => { + if (e.data && e.data.source === 'boundary-event-test' && e.data.type === 'child-pointerdown') { + iframe.remove(); + } + }; + window.addEventListener('message', onMessage); + t.add_cleanup(() => window.removeEventListener('message', onMessage)); + + await new test_driver.Actions() + .addPointer('touchPointer', 'touch') + .pointerMove(0, 0, {origin: iframe}) + .pointerDown() + .pause(200) + .pointerMove(0, 0, {origin: parentTarget}) + .pointerUp() + .send(); + + await tapDoneButton(); + + assert_equals( + parentEvents.map(e => `${e.type} (null: ${e.relatedTargetIsNull})`).join(), + "pointerover (null: true),pointerenter (null: true)" + ); +}, 'Iframe to parent with iframe removed: relatedTarget in outer document is null'); </script> </body> diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html b/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html index 5150c3e..d7dae4e 100644 --- a/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html +++ b/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html @@ -45,6 +45,13 @@ } else { targetEl.addEventListener('pointerdown', e => { e.target.releasePointerCapture(e.pointerId); + if (params.get('report_pointerdown') === 'true') { + window.top.postMessage({ + source: 'boundary-event-test', + frameId: frameId, + type: 'child-pointerdown' + }, '*'); + } }); ['pointerover', 'pointerenter', 'pointerout', 'pointerleave'].forEach(type => { targetEl.addEventListener(type, e => {
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
index 291b07d..a688d8e 100644
--- 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
@@ -203,6 +203,53 @@
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 = [];
+ childTarget.addEventListener('pointerdown', e => {
+ e.target.releasePointerCapture(e.pointerId);
+ iframe.remove();
+ });
+
+ parentTarget.addEventListener('pointerover', e => {
+ parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null });
+ });
+ parentTarget.addEventListener('pointerenter', e => {
+ parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null });
+ });
+
+ 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(
+ parentEvents.map(e => `${e.type} (null: ${e.relatedTargetIsNull})`).join(),
+ "pointerover (null: true),pointerenter (null: true)"
+ );
+}, 'Same-origin iframe to parent with iframe removed: relatedTarget is null');
+
+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);
diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html
index c6b2ad1..38b9f6cc 100644
--- a/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html
+++ b/third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html
@@ -43,7 +43,7 @@
let receivedMessages = [];
window.addEventListener('message', e => {
- if (e.data && e.data.source === 'boundary-event-test' && e.data.type !== 'child-ready') {
+ if (e.data && e.data.source === 'boundary-event-test' && e.data.type !== 'child-ready' && e.data.type !== 'child-pointerdown') {
receivedMessages.push({
frameId: e.data.frameId,
type: e.data.type,
@@ -281,5 +281,55 @@
{ frameId: 'deep', type: 'pointerleave', relatedTargetIsNull: true }
]), 'receivedMessages should match expected deep child events');
}, 'Nested iframes: relatedTarget in outer document is outermost iframe, inner document is null');
+
+promise_test(async t => {
+ receivedMessages = [];
+ 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);
+
+ const childSrc = `${REMOTE_ORIGIN}${CHILD_PAGE}?frame_id=child&target_id=child-target&report_pointerdown=true`;
+ await loadChildFrame(iframe, childSrc, 'child');
+
+ let parentEvents = [];
+ ['pointerover', 'pointerenter'].forEach(type => {
+ parentTarget.addEventListener(type, e => {
+ parentEvents.push({ type: e.type, relatedTargetIsNull: e.relatedTarget === null });
+ });
+ });
+
+ const onMessage = e => {
+ if (e.data && e.data.source === 'boundary-event-test' && e.data.type === 'child-pointerdown') {
+ iframe.remove();
+ }
+ };
+ window.addEventListener('message', onMessage);
+ t.add_cleanup(() => window.removeEventListener('message', onMessage));
+
+ await new test_driver.Actions()
+ .addPointer('touchPointer', 'touch')
+ .pointerMove(0, 0, {origin: iframe})
+ .pointerDown()
+ .pause(200)
+ .pointerMove(0, 0, {origin: parentTarget})
+ .pointerUp()
+ .send();
+
+ await tapDoneButton();
+
+ assert_equals(
+ parentEvents.map(e => `${e.type} (null: ${e.relatedTargetIsNull})`).join(),
+ "pointerover (null: true),pointerenter (null: true)"
+ );
+}, 'Iframe to parent with iframe removed: relatedTarget in outer document is null');
</script>
</body>
diff --git a/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html b/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html
index 5150c3e..d7dae4e 100644
--- a/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html
+++ b/third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html
@@ -45,6 +45,13 @@
} else {
targetEl.addEventListener('pointerdown', e => {
e.target.releasePointerCapture(e.pointerId);
+ if (params.get('report_pointerdown') === 'true') {
+ window.top.postMessage({
+ source: 'boundary-event-test',
+ frameId: frameId,
+ type: 'child-pointerdown'
+ }, '*');
+ }
});
['pointerover', 'pointerenter', 'pointerout', 'pointerleave'].forEach(type => {
targetEl.addEventListener(type, e => {