commit abb3d47ff5271af16eb04978fdd40fd54f2bfd5a
parent 0437f65d7894b30f06df1857d8319a0dda546925
Author: Edgar Chen <echen@mozilla.com>
Date: Wed, 8 Oct 2025 03:02:10 +0000
Bug 1989416 - Prevent mCallbackId from being copied to generated mouse events; r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D265412
Diffstat:
6 files changed, 146 insertions(+), 26 deletions(-)
diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
@@ -1612,6 +1612,10 @@ mozilla::ipc::IPCResult BrowserChild::RecvRealMouseMoveEvent(
// event listener. Therefore, the cloned event in the queue shouldn't
// cause eMouseRawUpdate later when it'll be dispatched.
WidgetMouseEvent pendingMouseMoveEvent(aEvent);
+ // We don't want to dispatch aEvent immediately, so the cloned event
+ // should track the cllback id. And the callback id of cloned event will
+ // be moved again if there is no coalesced data yet when coalescing.
+ pendingMouseMoveEvent.mCallbackId = std::move(aEvent.mCallbackId);
pendingMouseMoveEvent.convertToPointerRawUpdate = false;
data->Coalesce(pendingMouseMoveEvent, aGuid, aInputBlockId);
mCoalescedMouseEventFlusher->StartObserver();
@@ -1638,6 +1642,10 @@ mozilla::ipc::IPCResult BrowserChild::RecvRealMouseMoveEvent(
// event listener. Therefore, the cloned event in the queue shouldn't
// cause eMouseRawUpdate later when it'll be dispatched.
WidgetMouseEvent pendingMouseMoveEvent(aEvent);
+ // The cloned event should track the cllback id. And the callback id of
+ // cloned event will be moved again if there is no coalesced data yet when
+ // coalescing.
+ pendingMouseMoveEvent.mCallbackId = std::move(aEvent.mCallbackId);
pendingMouseMoveEvent.convertToPointerRawUpdate = false;
newData->Coalesce(pendingMouseMoveEvent, aGuid, aInputBlockId);
diff --git a/dom/ipc/CoalescedMouseData.cpp b/dom/ipc/CoalescedMouseData.cpp
@@ -19,10 +19,12 @@ void CoalescedMouseData::Coalesce(const WidgetMouseEvent& aEvent,
const uint64_t& aInputBlockId) {
if (IsEmpty()) {
mCoalescedInputEvent = MakeUnique<WidgetMouseEvent>(aEvent);
+ mCoalescedInputEvent->mCallbackId = std::move(aEvent.mCallbackId);
mGuid = aGuid;
mInputBlockId = aInputBlockId;
MOZ_ASSERT(!mCoalescedInputEvent->mCoalescedWidgetEvents);
} else {
+ MOZ_ASSERT(aEvent.mCallbackId.isNothing());
MOZ_ASSERT(mGuid == aGuid);
MOZ_ASSERT(mInputBlockId == aInputBlockId);
MOZ_ASSERT(mCoalescedInputEvent->mModifiers == aEvent.mModifiers);
diff --git a/testing/mochitest/tests/browser/browser.toml b/testing/mochitest/tests/browser/browser.toml
@@ -1,7 +1,8 @@
[DEFAULT]
support-files = [
"head.js",
- "dummy.html"]
+ "dummy.html",
+ "empty.html"]
["browser_BrowserTestUtils.js"]
diff --git a/testing/mochitest/tests/browser/browser_EventUtils.js b/testing/mochitest/tests/browser/browser_EventUtils.js
@@ -8,6 +8,43 @@ const gBaseURL = gChromeBaseURL.replace(
"https://example.com"
);
+async function synthesizeMouseFromParent(
+ aBrowser,
+ aOffsetX,
+ aOffsetY,
+ aAsyncEnabled
+) {
+ info(
+ `synthesizeMouse with asyncEnabled=${aAsyncEnabled} from parent process`
+ );
+
+ let haveReceiveMouseEvent = false;
+ const onMousemove = event => {
+ info(
+ `Received mouse event: ${event.type} ${event.offsetX} ${event.offsetY} ${event.button} ${event.buttons}`
+ );
+ haveReceiveMouseEvent = true;
+ };
+ aBrowser.addEventListener("mousemove", onMousemove, { once: true });
+ await new Promise(resolve => {
+ EventUtils.synthesizeMouse(
+ aBrowser,
+ aOffsetX,
+ aOffsetY,
+ {
+ type: "mousemove",
+ asyncEnabled: aAsyncEnabled,
+ },
+ window,
+ () => {
+ ok(haveReceiveMouseEvent, "Should have received mouse event");
+ aBrowser.removeEventListener("mousemove", onMousemove);
+ resolve();
+ }
+ );
+ });
+}
+
add_task(async function synthesizeEventFromParent() {
async function testSynthesizeWheelFromParent(aBrowser, aAsyncEnabled) {
info(`Testing synthesizeWheel with asyncEnabled=${aAsyncEnabled}`);
@@ -68,31 +105,7 @@ add_task(async function synthesizeEventFromParent() {
SimpleTest.executeSoon(resolve);
});
- let haveReceiveMouseEvent = false;
- const onMousemove = event => {
- info(
- `Received mouse event: ${event.type} ${event.offsetX} ${event.offsetY} ${event.button} ${event.buttons}`
- );
- haveReceiveMouseEvent = true;
- };
- aBrowser.addEventListener("mousemove", onMousemove, { once: true });
- await new Promise(resolve => {
- EventUtils.synthesizeMouse(
- aBrowser,
- 10,
- 10,
- {
- type: "mousemove",
- asyncEnabled: aAsyncEnabled,
- },
- window,
- () => {
- ok(haveReceiveMouseEvent, "Should have received mouse event");
- aBrowser.removeEventListener("mousemove", onMousemove);
- resolve();
- }
- );
- });
+ await synthesizeMouseFromParent(aBrowser, 10, 10, aAsyncEnabled);
}
await BrowserTestUtils.withNewTab(
@@ -184,3 +197,80 @@ add_task(async function synthesizeEventFromContent() {
}
);
});
+
+add_task(async function testCallbackForCrossProcressIframe() {
+ const iframeBaseURL = gChromeBaseURL.replace(
+ "chrome://mochitests/content",
+ "https://example.org/"
+ );
+
+ async function synthesizeMouseFromParentAndWait(
+ aBrowser,
+ aOffsetX,
+ aOffsetY,
+ aBrowsingContext
+ ) {
+ let eventPromise = SpecialPowers.spawn(aBrowsingContext, [], async () => {
+ await new Promise(resolve => {
+ content.document.addEventListener(
+ "mousemove",
+ () => {
+ info("Received mousemove event in the target browsing context");
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ });
+ // Enuse the event listener is registered on remote target.
+ await SpecialPowers.spawn(aBrowsingContext, [], async () => {
+ await new Promise(resolve => {
+ SpecialPowers.executeSoon(resolve);
+ });
+ });
+
+ await Promise.all([
+ synthesizeMouseFromParent(aBrowser, aOffsetX, aOffsetY, true),
+ eventPromise,
+ ]);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ gBaseURL + "empty.html",
+ async function (browser) {
+ // Synthesize mouse event to the parent document.
+ await synthesizeMouseFromParentAndWait(
+ browser,
+ 10,
+ 5,
+ browser.browsingContext
+ );
+
+ // Add an iframe.
+ await SpecialPowers.spawn(
+ browser,
+ [iframeBaseURL + "empty.html"],
+ async url => {
+ content.document.body.appendChild(
+ content.document.createElement("br")
+ );
+ let iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ let loadPromise = new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+ content.document.body.appendChild(iframe);
+ await loadPromise;
+ }
+ );
+
+ // Synthesize mouse event to the iframe document.
+ await synthesizeMouseFromParentAndWait(
+ browser,
+ 10,
+ 35,
+ browser.browsingContext.children[0]
+ );
+ }
+ );
+});
diff --git a/testing/mochitest/tests/browser/empty.html b/testing/mochitest/tests/browser/empty.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <title>This is a empty page</title>
+ <meta charset="utf-8">
+ <body>This is a empty page</body>
+</html>
diff --git a/widget/MouseEvents.h b/widget/MouseEvents.h
@@ -341,6 +341,11 @@ class WidgetMouseEvent : public WidgetMouseEventBase,
}
}
+ WidgetMouseEvent(const WidgetMouseEvent& aEvent)
+ : WidgetMouseEventBase(aEvent), WidgetPointerHelper(aEvent) {
+ AssignMouseEventDataOnly(aEvent);
+ }
+
#ifdef DEBUG
virtual ~WidgetMouseEvent() { AssertContextMenuEventButtonConsistency(); }
#endif
@@ -410,7 +415,12 @@ class WidgetMouseEvent : public WidgetMouseEventBase,
void AssignMouseEventData(const WidgetMouseEvent& aEvent, bool aCopyTargets) {
AssignMouseEventBaseData(aEvent, aCopyTargets);
AssignPointerHelperData(aEvent, /* aCopyCoalescedEvents */ true);
+ AssignMouseEventDataOnly(aEvent);
+ }
+ void AssignMouseEventDataOnly(const WidgetMouseEvent& aEvent) {
+ // NOTE: Intentionally not copying mClickTarget, it should only be used by
+ // the original mouseup event to dispatch the click event.
mReason = aEvent.mReason;
mContextMenuTrigger = aEvent.mContextMenuTrigger;
mExitFrom = aEvent.mExitFrom;
@@ -418,6 +428,9 @@ class WidgetMouseEvent : public WidgetMouseEventBase,
mIgnoreRootScrollFrame = aEvent.mIgnoreRootScrollFrame;
mIgnoreCapturingContent = aEvent.mIgnoreCapturingContent;
mClickEventPrevented = aEvent.mClickEventPrevented;
+ // NOTE: Intentionally not copying mSynthesizeMoveAfterDispatch, it should
+ // only be used by the original event to check whether we need to
+ // synthesize an additional mousemove or pointermove event.
mTriggerEvent = aEvent.mTriggerEvent;
// NOTE: Intentionally not copying mCallbackId, it should only be tracked by
// the original event or propagated to the cross-process event.