Package: release.debian.org
Severity: normal
Tags: trixie
User: [email protected]
Usertags: pu

[ Reason ]
plasmashell crashes (segfault in ShellCorona::screenInvariants() ->
DesktopView::screenToFollow()) when a screen is removed, for example on monitor
sleep/wake or unplugging. Debian bug #1153390, KDE bug 468430. The crash comes
from a debug-only consistency check that is run by a timer. The timer is
compiled in because the Debian build does not define NDEBUG.

[ Impact ]
Users with several monitors, or monitors that sleep and wake, can lose their
Plasma shell (panel and desktop) when a screen disappears, until plasmashell is
restarted.

[ Tests ]
The upstream fix adds an autotest, 
testScreenRemovalBeforeAfterScreenOrderChanged,
which is part of the patch.
The source package builds (dpkg-buildpackage -S) and the patch applies cleanly
to 4:6.3.6-2 on top of the existing Debian patches. A full binary build of the
package and its test suite on trixie have NOT been completed yet; I will report
the result in this bug.

[ Risks ]
The patch only removes code (the #ifndef NDEBUG invariants timer and its
connections) and adds two Q_ASSERTs plus the autotest. The assertions are
compiled out in Debian builds (-DQT_NO_DEBUG), so the shipped binary gains no
new logic. One line from the upstream commit is dropped, because it uses a
debugMessage() helper that does not exist in 6.3.6.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [ ] the issue is verified as fixed in unstable
      NOT YET: unstable has 4:6.7.4-2 and is still affected. The fix is in
      upstream Plasma 6.8.0 (commit 54a9dba9) and has not reached unstable.
  [x] the fix is already in upstream (commit 54a9dba9, Plasma 6.8.0)

[ Changes ]
Adds 
debian/patches/upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch
(listed last in the series) and the changelog entry. See the attached debdiff.

[ Other info ]
Thanks to Ian Turner, who reported the problem and supplied the backport to
6.3.6, and to Marco Martin, the upstream author.
The package is on mentors for review and sponsoring:
https://mentors.debian.net/package/plasma-workspace/
I am not a Debian Developer yet (working toward NM). I would like the
Debian Qt/KDE team to agree, and I will need a DD to upload to
stable-proposed-updates if this is accepted.
diff -Nru plasma-workspace-6.3.6/debian/changelog plasma-workspace-6.3.6/debian/changelog
--- plasma-workspace-6.3.6/debian/changelog	2025-07-21 16:21:10.000000000 +0000
+++ plasma-workspace-6.3.6/debian/changelog	2026-11-08 09:43:29.000000000 +0000
@@ -1,3 +1,14 @@
+plasma-workspace (4:6.3.6-2+deb13u1) trixie; urgency=medium
+
+  * Cherry-pick upstream commit 54a9dba9 (ShellCorona: remove the
+    invariants timer) to fix a plasmashell crash in
+    ShellCorona::screenInvariants() when a screen is removed. (Closes:
+    #1153390)
+    - Upstream fix by Marco Martin (KDE bug 468430).
+    - Thanks to Ian Turner for the bug report and the backport to 6.3.6.
+
+ -- Andrei Ionuț Ștefan <[email protected]>  Sun, 08 Nov 2026 09:43:29 +0000
+
 plasma-workspace (4:6.3.6-2) unstable; urgency=medium
 
   [ Aurélien COUDERC ]
diff -Nru plasma-workspace-6.3.6/debian/patches/series plasma-workspace-6.3.6/debian/patches/series
--- plasma-workspace-6.3.6/debian/patches/series	2025-07-21 16:18:35.000000000 +0000
+++ plasma-workspace-6.3.6/debian/patches/series	2026-11-08 09:43:29.000000000 +0000
@@ -30,3 +30,4 @@
 upstream_d1896f5a_applets-kicker-resolve-non-existing-files-to-their-url.patch
 upstream_b2e80af5_Only-resize-width-to-fit-date-elements-if-they-are-visible.patch
 upstream_ec837446_Do-not-emit-OOM-notifications-on-systemd-services-reload.patch
+upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch
diff -Nru plasma-workspace-6.3.6/debian/patches/upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch plasma-workspace-6.3.6/debian/patches/upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch
--- plasma-workspace-6.3.6/debian/patches/upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch	1970-01-01 00:00:00.000000000 +0000
+++ plasma-workspace-6.3.6/debian/patches/upstream_54a9dba9_ShellCorona-Remove-The-invariant-timer.patch	2026-11-08 09:43:29.000000000 +0000
@@ -0,0 +1,168 @@
+Description: ShellCorona: remove the invariants timer, as it is not deterministic enough
+ After a screen is removed, the (!NDEBUG-only) invariants timer can fire
+ before handleOutputOrderChanged() has updated the screen order, so
+ screenInvariants() dereferences a desktop view whose screen is gone and
+ plasmashell crashes in DesktopView::screenToFollow().
+ .
+ Backport of the upstream fix to 6.3.6. One line added by the upstream
+ commit is dropped: Q_ASSERT_X(... qUtf8Printable(debugMessage())), because
+ the debugMessage() helper does not exist in 6.3.6.
+ .
+ Thanks to Ian Turner, who reported the problem in Debian bug #1153390
+ and supplied the backport to 6.3.6.
+Author: Marco Martin <[email protected]>
+Origin: upstream, https://invent.kde.org/plasma/plasma-workspace/-/commit/54a9dba9ec549cc06bdefaa727b2fcc52874c65e
+Bug: https://bugs.kde.org/show_bug.cgi?id=468430
+Bug-Debian: https://bugs.debian.org/1153390
+Last-Update: 2026-11-09
+---
+--- a/shell/autotests/shelltest.cpp
++++ b/shell/autotests/shelltest.cpp
+@@ -57,6 +57,7 @@ private Q_SLOTS:
+     void testPanelInsertion();
+     void testSecondScreenInsertion();
+     void testRedundantScreenInsertion();
++    void testScreenRemovalBeforeAfterScreenOrderChanged();
+     void testScreenRemovalRecyclingViews();
+     void testMoveOutOfRedundant();
+     void testScreenRemoval();
+@@ -387,6 +388,63 @@ void ShellTest::testScreenRemoval()
+     QVERIFY(cont2Found);
+ }
+ 
++void ShellTest::testScreenRemovalBeforeAfterScreenOrderChanged()
++{
++    testSecondScreenInsertion();
++
++    QPointer<DesktopView> view0 = m_corona->m_desktopViewForScreen[0];
++    QPointer<QScreen> screen0 = view0->screenToFollow();
++    QVERIFY(screen0);
++    QCOMPARE(screen0->name(), u"WL-1"_s);
++
++    QPointer<DesktopView> view1 = m_corona->m_desktopViewForScreen[1];
++    QPointer<QScreen> screen1 = view1->screenToFollow();
++    QVERIFY(screen1);
++    QCOMPARE(screen1->name(), u"DP-1"_s);
++
++    QPointer<DesktopView> view2 = m_corona->m_desktopViewForScreen[2];
++    auto *screen2 = view2->screenToFollow();
++    QVERIFY(screen2);
++    QCOMPARE(screen2->name(), u"DP-2"_s);
++
++    QSignalSpy removedSpy(m_corona, &ShellCorona::screenRemoved);
++    QSignalSpy screenOrderChangedSpy(m_corona, &ShellCorona::screenOrderChanged);
++    QSignalSpy screen1DeletedSpy(screen1, &QObject::destroyed);
++    QSignalSpy view2DeletedSpy(view2, &QObject::destroyed);
++
++    exec([this] {
++        remove(output(1));
++    });
++
++    QTRY_COMPARE(removedSpy.size(), 1);
++    QTRY_COMPARE(screen1DeletedSpy.size(), 1);
++
++    // Before screenOrderChanged arrives, the desktop view for screen 1 is still kept
++    // for recycling, but the underlying QScreen object has already been destroyed.
++    QCOMPARE(removedSpy.takeFirst().at(0).value<int>(), 2);
++    QVERIFY(m_corona->m_desktopViewForScreen.contains(1));
++    QCOMPARE(m_corona->m_desktopViewForScreen[1], view1);
++    QCOMPARE(view1->screenToFollow(), nullptr);
++
++    // The next screen has not been remapped yet either; desktopForScreen() can no longer
++    // find the recycled view because it indexes through ScreenPool::idForScreen().
++    QCOMPARE(view2->screenToFollow(), screen2);
++    QCOMPARE(m_corona->desktopForScreen(screen1), nullptr);
++    QCOMPARE(m_corona->desktopForScreen(screen2), view1);
++
++    setScreenOrder({u"WL-1"_s, u"DP-2"_s}, true);
++
++    // When screenOrderChanged has finally arrived,
++    // there are 2 screens managed by view0 and view1 respectively,
++    // and view3 has been deleted
++    QTRY_COMPARE(screenOrderChangedSpy.size(), 1);
++    QTRY_COMPARE(view2DeletedSpy.size(), 1);
++
++    QCOMPARE(m_corona->m_desktopViewForScreen.size(), 2);
++    QCOMPARE(m_corona->m_desktopViewForScreen[0], view0);
++    QCOMPARE(m_corona->m_desktopViewForScreen[1], view1);
++}
++
+ void ShellTest::testScreenRemovalRecyclingViews()
+ {
+     // Create 2 new screens
+--- a/shell/shellcorona.cpp
++++ b/shell/shellcorona.cpp
+@@ -181,12 +181,6 @@ void ShellCorona::init()
+ 
+     connect(this, &ShellCorona::editModeChanged, this, &ShellCorona::availableScreenRegionChanged);
+ 
+-#ifndef NDEBUG
+-    m_invariantsTimer.setSingleShot(true);
+-    m_invariantsTimer.setInterval(qEnvironmentVariableIsSet("KDECI_BUILD") > 0 ? 30000ms : 1s);
+-    connect(&m_invariantsTimer, &QTimer::timeout, this, &ShellCorona::screenInvariants);
+-#endif
+-
+     m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(kPackage().filePath("defaults")), u"Desktop"_s);
+     m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), u"Desktop"_s);
+     m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, QStringLiteral("org.kde.plasma.desktop"));
+@@ -868,6 +862,8 @@ void ShellCorona::screenInvariants() con
+         return;
+     }
+ 
++    Q_ASSERT(!m_screenReorderInProgress);
++
+     QSet<QScreen *> managedScreens;
+     for (auto *desk : m_desktopViewForScreen) {
+         managedScreens.insert(desk->screenToFollow());
+@@ -1396,14 +1392,14 @@ void ShellCorona::handleScreenRemoved(QS
+     // There can't be a containment that has for instance screen 0 and another 2 but nothing on 1
+     // It's size() - 1 because at this point screenpool didn't remove it from screenOrder() yet
+     Q_EMIT screenRemoved(m_screenPool->screenOrder().size() - 1);
+-#ifndef NDEBUG
+-    m_invariantsTimer.start();
+-#endif
+ }
+ 
+ void ShellCorona::handleScreenOrderChanged(QList<QScreen *> screens)
+ {
++    Q_ASSERT(!m_screenReorderInProgress);
++
+     m_screenReorderInProgress = true;
++
+     // First: reassign existing views if applicable, otherwise remove them
+     auto allDesktops = m_desktopViewForScreen.values();
+     m_desktopViewForScreen.clear();
+@@ -1460,9 +1456,7 @@ void ShellCorona::addOutput(QScreen *scr
+         return;
+     }
+     Q_ASSERT(!screen->geometry().isNull());
+-#ifndef NDEBUG
+-    connect(screen, &QScreen::geometryChanged, &m_invariantsTimer, qOverload<>(&QTimer::start), Qt::UniqueConnection);
+-#endif
++
+     int insertPosition = m_screenPool->idForScreen(screen);
+     Q_ASSERT(insertPosition >= 0);
+ 
+@@ -1508,9 +1502,6 @@ void ShellCorona::addOutput(QScreen *scr
+         Q_EMIT availableScreenRectChanged(m_screenPool->idForScreen(screen));
+     }
+     Q_EMIT screenAdded(m_screenPool->idForScreen(screen));
+-#ifndef NDEBUG
+-    m_invariantsTimer.start();
+-#endif
+ }
+ 
+ void ShellCorona::checkAllDesktopsUiReady(bool ready)
+--- a/shell/shellcorona.h
++++ b/shell/shellcorona.h
+@@ -303,9 +303,7 @@ private:
+ 
+     QTimer m_waitingPanelsTimer;
+     QTimer m_appConfigSyncTimer;
+-#ifndef NDEBUG
+-    QTimer m_invariantsTimer;
+-#endif
++
+     // For getting the active window on Wayland
+     KWayland::Client::PlasmaWindowManagement *m_waylandWindowManagement = nullptr;
+     QPointer<KWayland::Client::PlasmaWindow> m_previousPlasmaWindow;

Reply via email to