High chrome Cross Origin 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassCross Origin
Tracker577072000
Fix commitdbefa3bf4168 (chromium/src) +147/-31
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-29

Background

`relatedTarget`
For focus, mouse and pointer events, the secondary EventTarget involved (e.g. the node the pointer moved away from). The same-origin policy forbids handing a page an EventTarget that 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 from FocusEvent, MouseEvent and PointerEvent dispatch, meant to ensure a cross-origin relatedTarget is never exposed to the target’s execution context.
`ExecutionContext` / `SecurityOrigin`
Each document/frame has an ExecutionContext with a SecurityOrigin. When a frame is detached (e.g. its iframe is removed mid-dispatch), GetExecutionContext() can return null, so origin comparison must handle the null case.
`BoundaryEventDispatcher`
In boundary_event_dispatcher.cc, computes pointerover/pointerenter/pointerout/pointerleave across a boundary, choosing the relatedTarget for 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 relatedTarget clearing 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.

Key insight
The cross-origin 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

  1. 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.
  2. Trigger boundary dispatch A touch pointer stream moves from the child frame to a parent target, generating boundary events whose relatedTarget references a node in the child document.
  3. Detach mid-dispatch On pointerdown, script removes the iframe, detaching the child’s execution context so GetExecutionContext() returns null during the subsequent boundary/pointer event dispatch.
  4. 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.
  5. Primitive Cross-origin / cross-document information disclosure: the attacker’s parent context obtains a reference to an EventTarget from 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

This is a renderer-process security-boundary bug reachable from untrusted web content with no elevated privilege, exercised through ordinary pointer/mouse/focus event dispatch combined with iframe removal. The substantiated primitive is a same-origin-policy violation: a cross-origin or detached-frame 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

FunctionChangeNotes
if
third_party/blink/renderer/core/events/ui_event.cc
modified
if
third_party/blink/renderer/core/input/boundary_event_dispatcher.cc
modified
if
third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html
modified

Files Changed

  • third_party/blink/renderer/core/events/mouse_event.cc
  • third_party/blink/renderer/core/events/ui_event.h
  • third_party/blink/renderer/core/events/focus_event.cc
  • third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_origin.html
  • third_party/blink/web_tests/external/wpt/pointerevents/resources/pointerevent_boundary_events_cross_origin_child.html
  • third_party/blink/renderer/core/events/pointer_event.cc
  • third_party/blink/renderer/core/input/boundary_event_dispatcher.cc
  • third_party/blink/renderer/core/events/ui_event.cc
  • third_party/blink/web_tests/external/wpt/pointerevents/pointerevent_boundary_events_cross_document_same_origin.tentative.html

Audit Directions

  • Null-`ExecutionContext` assumptions in origin checks
    Grep for GetExecutionContext() comparisons guarded by a && 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 dispatch
    Audit 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` producers
    Check 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.
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 => {
Loading diff…

Regression Test / PoC

shipped with the fix
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 => {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.