Skip to content

moveHandler: Remove the monitor grace period timer when a grab ends - #461

Merged
aleasto merged 2 commits into
ubuntu:mainfrom
PhilMeyr:fix/monitor-grace-period-timer-crash
Sep 11, 2026
Merged

moveHandler: Remove the monitor grace period timer when a grab ends#461
aleasto merged 2 commits into
ubuntu:mainfrom
PhilMeyr:fix/monitor-grace-period-timer-crash

Conversation

@PhilMeyr

Copy link
Copy Markdown
Contributor

Fixes the GNOME Shell abort reported in #435, which several of us have hit on Ubuntu 26.04 (@YannickMG, @cpaelzer and myself, plus the original report on Debian 13 / GNOME 48).

The bug

_edgeTilingPreview() arms a 150 ms grace period timer when a drag crosses a monitor boundary, so the tile preview sticks to the previous monitor for a moment:

this._latestMonitorLockTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => {
    if (timerId === this._latestMonitorLockTimerId) {
        this._monitorNr = global.display.get_current_monitor();
        if (global.display.is_grabbed())
            this._edgeTilingPreview(window, grabOp);
    }
    ...

The callback captures window, but the source is only ever removed in destroy() — never when the grab it belongs to ends. _onMoveFinished()'s finally block tears down everything else (_posChangedId, the tile preview, _isGrabOp) and leaves this one pending.

So if the dragged window is destroyed within those 150 ms — tearing off or dropping a browser tab does exactly that — the orphaned timer still fires and re-enters _edgeTilingPreview(), reaching:

const workArea = new Rect(window.get_work_area_for_monitor(this._monitorNr));

meta_window_get_work_area_for_monitor()meta_window_get_workspaces()g_assert_not_reached() on an unmanaging window → SIGABRT. Being a C assertion, no JS guard can contain it, and the whole Wayland session dies.

The unmanaging JS error logged from _onMoveFinished() in every report is a separate, non-fatal event that merely signals the window is gone. In my logs the two are consistently ~140 ms apart, and on 2026-07-28 that error occurred with no abort at all — the session survived two more days.

The fix

  • Remove the pending source in the finally block of _onMoveFinished(), so it cannot outlive the operation that armed it. This is what actually fixes the crash.
  • Additionally guard the callback with window.get_compositor_private(). global.display.is_grabbed() is a property of the display and says nothing about this window still being managed; the same idiom is already used in focusHint.js:290.

Notes

  • Only the grace period path is affected, so monitor-switch-grace-period false works as a user-side workaround on released versions.
  • The timer is armed exclusively inside if (this._lastMonitorNr !== currMonitorNr), which matches every report in Wayland crash report #435 being multi-monitor.
  • I could not build a deterministic reproducer — it is a race against a 150 ms window — so this is reasoned from four crash traces rather than from a red-to-green test. Happy to adjust if you would rather keep only the finally cleanup and drop the second guard.

@PhilMeyr PhilMeyr mentioned this pull request Jul 31, 2026

@aleasto aleasto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally guard the callback with window.get_compositor_private(). global.display.is_grabbed() is a property of the display and says nothing about this window still being managed; the same idiom is already used in focusHint.js:290.

I don't expect this change to be necessary, since if all goes well _onMoveFinished will be fired to cancel the timer before the window is unmanaged -- but it doesn't hurt to be overly protective so I'm ok with that.

Could we also silence the window.assertExistence() stacktrace dump since we're able to handle it gracefully?

Comment on lines +499 to +500
// is_grabbed() is about the display, not about `window`
// still being managed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is not helpful. this reasoning could go in a commit message, not in an in-line comment.
here you could say // check that the window still exists, and a grab is still active

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and the rationale moved to the commit message.

@PhilMeyr
PhilMeyr force-pushed the fix/monitor-grace-period-timer-crash branch from 2ae1f71 to 10693e0 Compare September 10, 2026 14:41
When a drag crosses a monitor boundary, _edgeTilingPreview() arms a 150 ms
timer that captures the dragged window and re-enters _edgeTilingPreview()
when it fires. Nothing removes that source when the grab ends, so a window
destroyed within those 150 ms leaves it pending. It then reaches
window.get_work_area_for_monitor(), which asserts in
meta_window_get_workspaces() on an unmanaging window and aborts the shell.

Remove the pending source in the finally block of _onMoveFinished(), and
guard the callback with get_compositor_private(), since is_grabbed() is a
property of the display rather than of the window.

Closes ubuntu#435
@PhilMeyr
PhilMeyr force-pushed the fix/monitor-grace-period-timer-crash branch from 10693e0 to c799dbb Compare September 10, 2026 14:50
@PhilMeyr

Copy link
Copy Markdown
Contributor Author

Rebased on current main, both done.

The assertExistence() dump is silenced in a separate commit so you can drop it on its own. I kept the catch around the assertion only, so errors from the tiling code below still surface — same shape as _onWindowWorkspaceChanged().

For what it's worth, that dump isn't much of a signal: my journal has ~180 of those unmanaging errors since January against 9 aborts, and 23 of them in the 41 days since I set monitor-switch-grace-period false, with no abort at all.

And you're right that _onMoveFinished fires — it's actually thrown from there, so the window is already unmanaging when grab-op-end runs and the abort lands ~140 ms later. Hence the GLib.Source.remove() in the finally rather than above the assertion.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The cleanup and guard directly address the crash, with only a minor inaccurate comment remaining.

Pull request overview

Prevents stale monitor grace-period callbacks from accessing destroyed windows after drag operations.

Changes:

  • Removes pending monitor timers when a grab ends.
  • Guards timer callbacks against unmanaged windows.
  • Suppresses expected destroyed-window errors during cleanup.
File summaries
File Description
moveHandler.js Safely cleans up monitor-lock timers and validates callback windows.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

_onMoveFinished(window) {
try {
window.assertExistence();
// Nested so that the finally block still runs for a destroyed window
_onMoveFinished() is a try/finally with no catch, so the Error thrown by
window.assertExistence() escapes the grab-op-end handler and GJS dumps a
stack trace, even though the finally block already handles the case.

Catch it around the assertion only, so errors from the tiling code below
are still reported, like _onWindowWorkspaceChanged() does.
@PhilMeyr
PhilMeyr force-pushed the fix/monitor-grace-period-timer-crash branch from c799dbb to 730fdd9 Compare September 11, 2026 09:52
@aleasto

aleasto commented Sep 11, 2026

Copy link
Copy Markdown
Member

Thank you!

@aleasto
aleasto merged commit f9dffa2 into ubuntu:main Sep 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants