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;