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 <dbaron@chromium.org>
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 <dbaron@chromium.org>
Reviewed-by: Mustaq Ahmed <mustaq@chromium.org>
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 => {