Chaining two clients at different resolutions segfaults gnome-shell in meta_monitor_manager_rebuild (libmutter-18) and takes the whole desktop down; every later session then fails RemoteDesktop.CreateSession: ServiceUnknown and the client sits black, until GDM is restarted.
Found during the v0.28.0 RC validation on .21 (CachyOS, GNOME 50.4), then A/B'd against released 0.27.0 on the same box, same sequence, same hour — byte-identical crash. So it is not a 0.28.0 regression and 0.28.0 did not fix it; the trigger has been there all along. Field recipe: iPhone connects at 2868×1320 → app SIGTERM'd → Android connects at 2800×1260 within ~10 s. The host log shows virtual display: reused kept display was dead on first frame — torn down (A2 mark_failed) immediately before the segfault, so keep-alive display reuse is the path that re-modes Mutter here — no game launch and no mid-bringup mode switch needed, which is simpler than the trigger recorded in earlier notes.
Root cause
The crash is upstream mutter's, but we are the ones triggering it. TOPOLOGY_LOCK already serialized every topology-mutating D-Bus call — but Mutter's rebuilds outlive those calls, and two gaps let them overlap inside the shell:
Teardown was fire-and-forget.StopGuard::drop set an AtomicBool and returned; the session thread only noticed on its ≤200 ms park tick. The A2 dead-reuse path (reused kept display dead on first frame → mark_failed → re-create) therefore issued its fresh RecordVirtual with the doomed monitor's removal still pending — the fresh session could even win the lock before the old thread had woken to take it, adding a monitor while the dead one still stood.
The lock was released mid-rebuild.Stop / RecordVirtual / ApplyMonitorsConfig all return while the shell is still rebuilding, and an APPLY_TEMPORARY config auto-reverts asynchronously on top. The next lock holder therefore mutated a non-quiescent Mutter. This also explains the original 2026-08-08 add+add traces (~0.4–1 s apart).
Fix
Both in crates/pf-vdisplay/src/vdisplay/linux/mutter.rs:
StopGuard::drop is now synchronous — it waits (bounded 20 s, via a done-channel the session thread owns) for the thread to finish its Stop and settle, so "old monitor removed" strictly happens-before "next monitor created". The registry already drops these outside its pool lock and documents that the drop may block.
Every TOPOLOGY_LOCK section now ends with settle_topology() before the guard drops — poll GetCurrentState until a just-removed connector is actually gone, then until the config serial holds still across two consecutive reads (150 ms apart, 4 s deadline). Applied to setup, teardown, and all the early-abort paths (connect failure, opener-gave-up). Best-effort by design: a read error means the shell is gone, and the deadline stops an unrelated hotplug storm from parking a session — both degrade to exactly the old behavior.
Cost when Mutter is already quiet: one confirming read plus one 150 ms recheck per setup/teardown. The live_mutter_create_drop harness sheds its grace sleep — the synchronous drop is the teardown confirmation now.
Residual (documented on TOPOLOGY_LOCK)
The mid-stream mode-switch rebuild is create-before-drop by design (H2), so its RecordVirtual still lands while the superseded monitor and its temporary config exist. The settle makes Mutter quiescent at that instant but cannot remove the coexistence itself. If the crash ever reproduces with this in, that path is the suspect. Not reported upstream to mutter either — we stopped triggering the bug, we did not fix it.
Gates
scripts/xcheck.sh linux check and clippy — clean.
210 pf-vdisplay tests pass natively in the rust:1.96 container.
⏳On-glass validation on .21 is owed (no GNOME box reachable from the dev Mac): iPhone connect (2868×1320) → SIGTERM the app → Android connect (2800×1260) within ~10 s, several rounds. gnome-shell must survive and the Android session must come up after the A2 teardown+recreate.
Chaining two clients at different resolutions segfaults gnome-shell in `meta_monitor_manager_rebuild` (libmutter-18) and takes the whole desktop down; every later session then fails `RemoteDesktop.CreateSession: ServiceUnknown` and the client sits black, until GDM is restarted.
Found during the v0.28.0 RC validation on `.21` (CachyOS, GNOME 50.4), then **A/B'd against released 0.27.0 on the same box, same sequence, same hour — byte-identical crash**. So it is *not* a 0.28.0 regression and 0.28.0 did not fix it; the trigger has been there all along. Field recipe: iPhone connects at 2868×1320 → app SIGTERM'd → Android connects at 2800×1260 within ~10 s. The host log shows `virtual display: reused kept display was dead on first frame — torn down (A2 mark_failed)` immediately before the segfault, so **keep-alive display reuse** is the path that re-modes Mutter here — no game launch and no mid-bringup mode switch needed, which is simpler than the trigger recorded in earlier notes.
## Root cause
The crash is upstream mutter's, but we are the ones triggering it. `TOPOLOGY_LOCK` already serialized every topology-mutating D-Bus call — but Mutter's *rebuilds outlive those calls*, and two gaps let them overlap inside the shell:
1. **Teardown was fire-and-forget.** `StopGuard::drop` set an `AtomicBool` and returned; the session thread only noticed on its ≤200 ms park tick. The A2 dead-reuse path (reused kept display dead on first frame → `mark_failed` → re-create) therefore issued its fresh `RecordVirtual` with the doomed monitor's removal still pending — the fresh session could even win the lock *before* the old thread had woken to take it, adding a monitor while the dead one still stood.
2. **The lock was released mid-rebuild.** `Stop` / `RecordVirtual` / `ApplyMonitorsConfig` all return while the shell is still rebuilding, and an `APPLY_TEMPORARY` config auto-reverts asynchronously on top. The next lock holder therefore mutated a non-quiescent Mutter. This also explains the original 2026-08-08 add+add traces (~0.4–1 s apart).
## Fix
Both in `crates/pf-vdisplay/src/vdisplay/linux/mutter.rs`:
- **`StopGuard::drop` is now synchronous** — it waits (bounded 20 s, via a done-channel the session thread owns) for the thread to finish its Stop and settle, so "old monitor removed" strictly happens-before "next monitor created". The registry already drops these outside its pool lock and documents that the drop may block.
- **Every `TOPOLOGY_LOCK` section now ends with `settle_topology()`** before the guard drops — poll `GetCurrentState` until a just-removed connector is actually gone, then until the config serial holds still across two consecutive reads (150 ms apart, 4 s deadline). Applied to setup, teardown, and all the early-abort paths (`connect` failure, opener-gave-up). Best-effort by design: a read error means the shell is gone, and the deadline stops an unrelated hotplug storm from parking a session — both degrade to exactly the old behavior.
Cost when Mutter is already quiet: one confirming read plus one 150 ms recheck per setup/teardown. The `live_mutter_create_drop` harness sheds its grace sleep — the synchronous drop *is* the teardown confirmation now.
## Residual (documented on `TOPOLOGY_LOCK`)
The mid-stream mode-switch rebuild is create-before-drop by design (H2), so its `RecordVirtual` still lands while the superseded monitor and its temporary config exist. The settle makes Mutter quiescent at that instant but cannot remove the coexistence itself. If the crash ever reproduces with this in, that path is the suspect. Not reported upstream to mutter either — we stopped triggering the bug, we did not fix it.
## Gates
- `scripts/xcheck.sh linux check` and `clippy` — clean.
- **210 pf-vdisplay tests pass** natively in the `rust:1.96` container.
- ⏳ **On-glass validation on `.21` is owed** (no GNOME box reachable from the dev Mac): iPhone connect (2868×1320) → SIGTERM the app → Android connect (2800×1260) within ~10 s, several rounds. gnome-shell must survive and the Android session must come up after the A2 teardown+recreate.
Chaining two clients through a kept (keep-alive) Mutter display segfaults
gnome-shell in meta_monitor_manager_rebuild (libmutter-18) and takes the whole
desktop down; every later session then fails RemoteDesktop.CreateSession:
ServiceUnknown until GDM restarts. A/B'd on .21: byte-identical on released
0.27.0 and the 0.28.0 RC, so it was never a regression — the trigger has been
there all along.
TOPOLOGY_LOCK already serialized every topology-mutating D-Bus call, but two
gaps still let Mutter's REBUILDS overlap:
- Teardown was fire-and-forget: StopGuard::drop set a flag and returned, and
the session thread only noticed on its ≤200 ms park tick. The A2 dead-reuse
path (reused kept display dead on first frame → mark_failed → re-create)
therefore issued its fresh RecordVirtual with the doomed monitor's removal
still pending — the fresh session could even win the lock BEFORE the old
thread had woken to take it, adding a monitor while the dead one still stood.
The drop now waits (bounded, 20 s) for the session thread to finish.
- The lock was released while the shell was still rebuilding: Stop /
RecordVirtual / ApplyMonitorsConfig all return mid-rebuild, and a temporary
(APPLY_TEMPORARY) config auto-reverts asynchronously on top. Every locked
mutation section now ends with settle_topology() — poll GetCurrentState
until a removed connector is actually gone and the config serial holds still
across two consecutive reads — before the guard drops. Bounded at 4 s and
best-effort (a read error means the shell is gone; a hotplug storm must not
park sessions), degrading to exactly the old behavior.
Cost when Mutter is already quiet: one confirming read plus one 150 ms recheck
per setup/teardown. The live_mutter_create_drop harness sheds its grace sleep —
the synchronous drop IS the teardown confirmation now.
Not fixed here, documented on TOPOLOGY_LOCK: the mid-stream mode-switch rebuild
is create-before-drop by design (H2), so its RecordVirtual still lands while
the superseded monitor exists; the settle makes Mutter quiescent at that point
but cannot remove the coexistence itself.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Chaining two clients at different resolutions segfaults gnome-shell in
meta_monitor_manager_rebuild(libmutter-18) and takes the whole desktop down; every later session then failsRemoteDesktop.CreateSession: ServiceUnknownand the client sits black, until GDM is restarted.Found during the v0.28.0 RC validation on
.21(CachyOS, GNOME 50.4), then A/B'd against released 0.27.0 on the same box, same sequence, same hour — byte-identical crash. So it is not a 0.28.0 regression and 0.28.0 did not fix it; the trigger has been there all along. Field recipe: iPhone connects at 2868×1320 → app SIGTERM'd → Android connects at 2800×1260 within ~10 s. The host log showsvirtual display: reused kept display was dead on first frame — torn down (A2 mark_failed)immediately before the segfault, so keep-alive display reuse is the path that re-modes Mutter here — no game launch and no mid-bringup mode switch needed, which is simpler than the trigger recorded in earlier notes.Root cause
The crash is upstream mutter's, but we are the ones triggering it.
TOPOLOGY_LOCKalready serialized every topology-mutating D-Bus call — but Mutter's rebuilds outlive those calls, and two gaps let them overlap inside the shell:Teardown was fire-and-forget.
StopGuard::dropset anAtomicBooland returned; the session thread only noticed on its ≤200 ms park tick. The A2 dead-reuse path (reused kept display dead on first frame →mark_failed→ re-create) therefore issued its freshRecordVirtualwith the doomed monitor's removal still pending — the fresh session could even win the lock before the old thread had woken to take it, adding a monitor while the dead one still stood.The lock was released mid-rebuild.
Stop/RecordVirtual/ApplyMonitorsConfigall return while the shell is still rebuilding, and anAPPLY_TEMPORARYconfig auto-reverts asynchronously on top. The next lock holder therefore mutated a non-quiescent Mutter. This also explains the original 2026-08-08 add+add traces (~0.4–1 s apart).Fix
Both in
crates/pf-vdisplay/src/vdisplay/linux/mutter.rs:StopGuard::dropis now synchronous — it waits (bounded 20 s, via a done-channel the session thread owns) for the thread to finish its Stop and settle, so "old monitor removed" strictly happens-before "next monitor created". The registry already drops these outside its pool lock and documents that the drop may block.TOPOLOGY_LOCKsection now ends withsettle_topology()before the guard drops — pollGetCurrentStateuntil a just-removed connector is actually gone, then until the config serial holds still across two consecutive reads (150 ms apart, 4 s deadline). Applied to setup, teardown, and all the early-abort paths (connectfailure, opener-gave-up). Best-effort by design: a read error means the shell is gone, and the deadline stops an unrelated hotplug storm from parking a session — both degrade to exactly the old behavior.Cost when Mutter is already quiet: one confirming read plus one 150 ms recheck per setup/teardown. The
live_mutter_create_dropharness sheds its grace sleep — the synchronous drop is the teardown confirmation now.Residual (documented on
TOPOLOGY_LOCK)The mid-stream mode-switch rebuild is create-before-drop by design (H2), so its
RecordVirtualstill lands while the superseded monitor and its temporary config exist. The settle makes Mutter quiescent at that instant but cannot remove the coexistence itself. If the crash ever reproduces with this in, that path is the suspect. Not reported upstream to mutter either — we stopped triggering the bug, we did not fix it.Gates
scripts/xcheck.sh linux checkandclippy— clean.rust:1.96container..21is owed (no GNOME box reachable from the dev Mac): iPhone connect (2868×1320) → SIGTERM the app → Android connect (2800×1260) within ~10 s, several rounds. gnome-shell must survive and the Android session must come up after the A2 teardown+recreate.