Bug 446581

Summary: On X11, plasmashell and Firefox hang in QXcbClipboard::waitForClipboardEvent() when Firefox attempts to update the clipboard and show a notification at the same time
Product: [Plasma] plasmashell Reporter: nyanpasu64 <nyanpasu64>
Component: Clipboard widget & pop-upAssignee: Plasma Bugs List <plasma-bugs>
Status: CONFIRMED ---    
Severity: normal CC: 2724167997, ad.ruckel, alexander.s.m, calvin.f.hoy, chris.longros, edarocha1324, grgbrn, hardpenguin13, jamesxmcintosh, kdedev, MurzNN, nate, oleksandr, piotrekkr, plasma-bugs, post, postix, qydwhotmail, silvan.calarco, stransky
Priority: NOR    
Version: 5.23.4   
Target Milestone: 1.0   
Platform: Other   
OS: Linux   
URL: https://bugreports.qt.io/browse/QTBUG-67822?jql=text%20~%20%22waitForClipboardEvent%22
See Also: https://bugs.kde.org/show_bug.cgi?id=337961
https://bugs.kde.org/show_bug.cgi?id=480312
Latest Commit: Version Fixed In:
Sentry Crash Report:

Description nyanpasu64 2021-12-06 21:06:07 UTC
SUMMARY
On KDE Plasma X11, with Klipper (clipboard history) added to plasmashell, and Firefox with "Copy Selected Tabs to Clipboard" installed, trying to use the extension often causes Firefox and plasmashell (and any app trying to paste) to deadlock and hang.

STEPS TO REPRODUCE
1. Install Firefox and https://addons.mozilla.org/en-US/firefox/addon/copy-selected-tabs-to-clipboar/.
2. Add the Clipboard applet to a plasmashell panel. (Unsure if necessary, my System Tray applet also has a clipboard icon! I'm not sure how to show/hide it.)
3. With Plasma X11 running (doesn't happen on Wayland), right-click a Firefox tab, expand the "Copy to Clipboard" menu, and click "URL".

OBSERVED RESULT
Around 50% of the time, Firefox and plasmashell (and any app I try pasting into) hang for 10 seconds. Afterwards, plasmashell gives up trying to grab clipboard contents, the notification gets shown, and the clipboard is updated but Klipper is missing the clipboard entry.

EXPECTED RESULT
The notification gets shown, the clipboard is updated, and Klipper contains the new entry.

SOFTWARE/OS VERSIONS
Operating System: Arch Linux
KDE Plasma Version: 5.23.4
KDE Frameworks Version: 5.88.0
Qt Version: 5.15.2
Kernel Version: 5.15.4-zen1-1-zen (64-bit)
Graphics Platform: X11
Processors: 12 × AMD Ryzen 5 5600X 6-Core Processor
Memory: 15.6 GiB of RAM
Graphics Processor: NVIDIA GeForce GT 730/PCIe/SSE2

Firefox 94.0.2 (64-bit)
Copy Selected Tabs to Clipboard: 1.4.3

ADDITIONAL INFORMATION

When you use the extension to copy a tab list, Firefox sends a "clipboard changed" event (through X11 I assume, not sure) and attempts to send a notification through D-Bus (`notify_notification_show () at /usr/lib/libnotify.so.4` blocks on `g_dbus_connection_send_message_with_reply_sync`).

Meanwhile plasmashell can't process the notification and unblock Firefox, because the Klipper plasmashell plugin is blocked trying to grab the clipboard contents (`QMimeData::text()` blocks on `QXcbClipboard::getSelection` and `QXcbClipboard::waitForClipboardEvent`). IDK exactly how X11 clipboards work, but I assume plasmashell/Klipper asks Xorg to ask Firefox for clipboard contents. But Firefox can't provide clipboard contents until it's done showing a notification, and plasmashell won't show the notification until it gets the clipboard contents.

And both apps lock up for 10 seconds (I think plasmashell takes two 5-second timeouts to give up). And in the end, plasmashell gives up trying to grab clipboard contents, processes the notification, and both programs get unblocked (but Klipper is missing the clipboard entry).

Which app is at fault and should be fixed? Firefox? plasmashell? "Copy Selected Tabs to Clipboard"? Klipper?

Firefox backtrace during hang (recorded a few months ago):
(gdb) bt
#0  0x00007f8086af6b2f in poll () at /usr/lib/libc.so.6
#1  0x00007f808549d819 in g_main_context_poll (priority=<optimized out>, n_fds=1, fds=0x7f80412e7488, timeout=<optimized out>, context=0x7f8032c6a190) at ../glib/glib/gmain.c:4478
#2  g_main_context_iterate.constprop.0 (context=0x7f8032c6a190, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>) at ../glib/glib/gmain.c:4170
#3  0x00007f8085447663 in g_main_loop_run (loop=0x7f803989b4a0) at ../glib/glib/gmain.c:4373
#4  0x00007f8085324a14 in g_dbus_connection_send_message_with_reply_sync (connection=0x7f8086769d00, message=0x7f802610f470, flags=G_DBUS_SEND_MESSAGE_FLAGS_NONE, timeout_msec=-1, out_serial=0x0, cancellable=0x0, error=0x7fff9df3cd40) at ../glib/gio/gdbusconnection.c:2179
#5  0x00007f808533276d in g_dbus_connection_call_sync_internal (connection=0x7f8086769d00, bus_name=0x7f803989b3d0 ":1.36749", object_path=0x7f8038014740 "/org/freedesktop/Notifications", interface_name=0x7f80380144e0 "org.freedesktop.Notifications", method_name=0x7f8045e7d24b "Notify", parameters=0x7f803a81eca0, reply_type=0x7f808538d99c, flags=G_DBUS_CALL_FLAGS_NONE, timeout_msec=-1, fd_list=0x0, out_fd_list=0x0, cancellable=0x0, error=0x7fff9df3d088) at ../glib/gio/gdbusconnection.c:6121
#6  0x00007f808533ba77 in g_dbus_proxy_call_sync_internal (proxy=0x7f803804e650, method_name=<optimized out>, parameters=0x7f803a81eca0, flags=G_DBUS_CALL_FLAGS_NONE, timeout_msec=<optimized out>, fd_list=fd_list@entry=0x0, out_fd_list=0x0, cancellable=0x0, error=0x7fff9df3d088) at ../glib/gio/gdbusproxy.c:2845
#7  0x00007f808533bc48 in g_dbus_proxy_call_sync (proxy=<optimized out>, method_name=<optimized out>, parameters=<optimized out>, flags=<optimized out>, timeout_msec=<optimized out>, cancellable=<optimized out>, error=0x7fff9df3d088) at ../glib/gio/gdbusproxy.c:3037
#8  0x00007f8045e7b75d in notify_notification_show () at /usr/lib/libnotify.so.4
#9  0x00007f807ff40a11 in  () at /usr/lib/firefox/libxul.so
...
#30 0x000055aafdb4ebdd in  ()
#31 0x00007f8086a2ab25 in __libc_start_main () at /usr/lib/libc.so.6
#32 0x000055aafdbca4ae in _start ()

plasmashell backtrace during hang:
(gdb) bt
#0  0x00007ffff49e28ca in __futex_abstimed_wait_common64 () at /usr/lib/libpthread.so.0
#1  0x00007ffff49dc574 in pthread_cond_timedwait@@GLIBC_2.3.2 () at /usr/lib/libpthread.so.0
#2  0x00007ffff5a4e9a4 in QWaitConditionPrivate::wait_relative(QDeadlineTimer) (deadline=..., this=0x555555650de0) at thread/qwaitcondition_unix.cpp:136
#3  QWaitConditionPrivate::wait(QDeadlineTimer) (deadline=..., deadline=..., this=0x555555650de0) at thread/qwaitcondition_unix.cpp:144
#4  QWaitCondition::wait(QMutex*, QDeadlineTimer) (this=<optimized out>, mutex=0x5555556332f8, deadline=...) at thread/qwaitcondition_unix.cpp:225
#5  0x00007ffff5a4eabd in QWaitCondition::wait(QMutex*, unsigned long) (this=0x555555633300, mutex=0x5555556332f8, time=<optimized out>) at thread/qwaitcondition_unix.cpp:209
#6  0x00007ffff0960906 in QXcbEventQueue::waitForNewEvents(QXcbEventNode const*, unsigned long) (this=this@entry=0x555555633290, sinceFlushedTail=sinceFlushedTail@entry=0x7ffff0a45ed8 <QXcbEventQueue::qXcbEventNodeFactory(xcb_generic_event_t*)::qXcbNodePool+24>, time=3417) at qxcbeventqueue.cpp:362
#7  0x00007ffff09348f4 in QXcbClipboard::waitForClipboardEvent(unsigned int, int, bool) (this=this@entry=0x7fffec0047a0, window=window@entry=90177613, type=type@entry=31, checkManager=checkManager@entry=false) at qxcbclipboard.cpp:815
#8  0x00007ffff0934ff0 in QXcbClipboard::getSelection(unsigned int, unsigned int, unsigned int, unsigned int) (this=0x7fffec0047a0, selection=326, target=target@entry=590, property=333, time=194042584, time@entry=0) at qxcbclipboard.cpp:906
#9  0x00007ffff0937132 in QXcbClipboard::getDataInFormat(unsigned int, unsigned int) (fmtAtom=590, modeAtom=<optimized out>, this=<optimized out>) at qxcbclipboard.cpp:891
#10 QXcbClipboardMime::retrieveData_sys(QString const&, QVariant::Type) const (this=<optimized out>, fmt=..., type=<optimized out>) at qxcbclipboard.cpp:147
#11 0x00007ffff60396b8 in QInternalMimeData::retrieveData(QString const&, QVariant::Type) const (this=0x5555563594b0, mimeType=..., type=QVariant::String) at kernel/qinternalmimedata.cpp:113
#12 0x00007ffff5c635e0 in QMimeDataPrivate::retrieveTypedData(QString const&, QMetaType::Type) const (this=0x555555cd36a0, format=..., type=QMetaType::QString) at kernel/qmimedata.cpp:119
#13 0x00007ffff5c64281 in QMimeData::text() const (this=<optimized out>) at kernel/qmimedata.cpp:409
#14 0x00007fffc809ea4b in HistoryItem::create(QMimeData const*) () at /usr/lib/qt/plugins/plasma/dataengine/plasma_engine_clipboard.so
#15 0x00007fffc80899be in Klipper::applyClipChanges(QMimeData const*) () at /usr/lib/qt/plugins/plasma/dataengine/plasma_engine_clipboard.so
#16 0x00007fffc808bf51 in Klipper::checkClipData(bool) () at /usr/lib/qt/plugins/plasma/dataengine/plasma_engine_clipboard.so
#17 0x00007ffff5c73915 in QtPrivate::QSlotObjectBase::call(QObject*, void**) (a=0x7fffffffdb70, r=<optimized out>, this=0x7fffec00b4c0) at ../../include/QtCore/../../src/corelib/kernel/qobjectdefs_impl.h:398
#18 doActivate<false>(QObject*, int, void**) (sender=0x7fffec008410, signal_index=3, argv=0x7fffffffdb70) at kernel/qobject.cpp:3886
#19 0x00007fffc80b2363 in SystemClipboard::changed(QClipboard::Mode) () at /usr/lib/qt/plugins/plasma/dataengine/plasma_engine_clipboard.so
#20 0x00007ffff5c73915 in QtPrivate::QSlotObjectBase::call(QObject*, void**) (a=0x7fffffffdc80, r=<optimized out>, this=0x555556815b00) at ../../include/QtCore/../../src/corelib/kernel/qobjectdefs_impl.h:398
#21 doActivate<false>(QObject*, int, void**) (sender=0x555555e685f0, signal_index=3, argv=0x7fffffffdc80) at kernel/qobject.cpp:3886
#22 0x00007ffff5c6cee7 in QMetaObject::activate(QObject*, QMetaObject const*, int, void**) (sender=<optimized out>, m=m@entry=0x7ffff65b1a00 <QClipboard::staticMetaObject>, local_signal_index=local_signal_index@entry=0, argv=argv@entry=0x7fffffffdc80) at kernel/qobject.cpp:3946
#23 0x00007ffff649ed63 in QClipboard::changed(QClipboard::Mode) (this=<optimized out>, _t1=<optimized out>) at .moc/moc_qclipboard.cpp:168
#24 0x00007ffff6029670 in QClipboard::emitChanged(QClipboard::Mode) (this=<optimized out>, mode=<optimized out>) at kernel/qclipboard.cpp:608
#25 0x00007ffff600a4fd in QPlatformClipboard::emitChanged(QClipboard::Mode) (this=<optimized out>, mode=<optimized out>) at kernel/qplatformclipboard.cpp:125
#26 0x00007ffff093525f in QXcbClipboard::handleXFixesSelectionRequest(xcb_xfixes_selection_notify_event_t*) (this=<optimized out>, event=event@entry=0x7fffec00c660) at qxcbclipboard.cpp:679
#27 0x00007ffff0938d56 in QXcbConnection::handleXcbEvent(xcb_generic_event_t*) (this=this@entry=0x55555563f620, event=event@entry=0x7fffec00c660) at qxcbconnection.cpp:685
#28 0x00007ffff093a2d1 in QXcbConnection::processXcbEvents(QFlags<QEventLoop::ProcessEventsFlag>) (this=0x55555563f620, flags=...) at qxcbconnection.cpp:1014
#29 0x00007ffff09617f8 in xcbSourceDispatch(GSource*, GSourceFunc, gpointer) (source=<optimized out>) at qxcbeventdispatcher.cpp:103
#30 0x00007ffff3f090ec in g_main_dispatch (context=0x7fffec005000) at ../glib/glib/gmain.c:3381
#31 g_main_context_dispatch (context=0x7fffec005000) at ../glib/glib/gmain.c:4099
#32 0x00007ffff3f5e889 in g_main_context_iterate.constprop.0 (context=context@entry=0x7fffec005000, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>) at ../glib/glib/gmain.c:4175
#33 0x00007ffff3f06735 in g_main_context_iteration (context=0x7fffec005000, may_block=1) at ../glib/glib/gmain.c:4240
#34 0x00007ffff5c95b2a in QEventDispatcherGlib::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) (this=0x5555556ea750, flags=...) at kernel/qeventdispatcher_glib.cpp:423
#35 0x00007ffff5c3aabb in QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) (this=this@entry=0x7fffffffdf40, flags=..., flags@entry=...) at ../../include/QtCore/../../src/corelib/global/qflags.h:69
#36 0x00007ffff5c432a8 in QCoreApplication::exec() () at ../../include/QtCore/../../src/corelib/global/qflags.h:121
#37 0x0000555555573136 in main ()
Comment 1 Fushan Wen 2021-12-07 02:47:40 UTC
Addressed in https://github.com/piroor/copy-selected-tabs-to-clipboard/pull/28
Comment 2 nyanpasu64 2021-12-07 05:37:06 UTC
(In reply to Fushan Wen from comment #1)
> Addressed in
> https://github.com/piroor/copy-selected-tabs-to-clipboard/pull/28

I tested that PR and it doesn't fix the hang on my machine, and I don't think that removing a call to await prevents the deadlock I described. Firefox still blocks on sending a notification, and plasmashell still blocks on reading clipboard data.
Comment 3 nyanpasu64 2021-12-07 06:02:49 UTC
Given my limited expertise, I *think* that it's reasonable for Firefox to copy something to the clipboard and then show a notification and expect it to finish synchronously, and it's unreasonable for plasmashell to assume that apps will respond to clipboard content requests, and to stop processing notifications while waiting for clipboard contents. So perhaps plasmashell and Klipper should be rewritten to not block while waiting for clipboard contents, but that's quite involved (perhaps asynchronously, which may require bypassing QXcbClipboard entirely, perhaps having background threads request clipboard contents).

Alternatively Firefox could use an asynchronous function instead of libnotify's `notify_notification_show`, to respond to clipboard requests while trying to show a notification. I'm not sure how difficult that is, AKA how many round-trips d-bus's notification protocol requires, and how many states need to be created in Firefox's state machine.
Comment 4 hardpenguin13 2022-01-17 21:59:56 UTC
I can confirm this affects me as well when making a screenshot in Firefox and then trying to copy it to Klipper.

Steps to reproduce:

1. CTRL+SHIFT+S to make a screenshot
2. Select desired area
3. Select "Copy" button under the selected area.

Should I start another bug? The description of the observed result here fits perfectly. I use Firefox with XFCE on another computer and there was no such problem problem.

What is more, I think this proves the bug is not related to the "Copy Selected Tabs to Cliboard" Firefox addon but rather it is KDE related.
Comment 5 hardpenguin13 2022-01-17 22:01:59 UTC
Following up with details:

Operating System: Debian GNU/Linux
KDE Plasma Version: 5.23.5
KDE Frameworks Version: 5.88.0
Qt Version: 5.15.2
Kernel Version: 5.15.0-2-amd64 (64-bit)
Graphics Platform: X11

Firefox version: 96.0 (64-bits)
Comment 6 Fushan Wen 2022-06-07 15:32:02 UTC
*** Bug 453562 has been marked as a duplicate of this bug. ***
Comment 7 Ralf Jung 2022-06-07 16:03:01 UTC
> So perhaps plasmashell and Klipper should be rewritten to not block while waiting for clipboard contents

Blocking the main plasmashell thread sounds like a bad idea in general, since it always risks a freeze of the entire shell. I've experienced clipboard-related freezes in Plasma for around a decade now so it would be amazing to see this fixed at some point. :)
Comment 8 nyanpasu64 2022-06-14 08:33:31 UTC
## Popping up menus upon copying URLs

At https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L825-833, you call URLGrabber::checkNewData() (which pops up a menu if Klipper's Action Menu is enabled and you're not copying from Firefox), *then* skip assigning lastURLGrabberText if the text matches. I believe the intent was to skip calling URLGrabber::checkNewData() if the text matches, but that would mean copying the same text twice doesn't popup a menu the second time. Should this be changed to the design intent (which changes clipboard behavior), or should we remove m_lastURLGrabberTextSelection and m_lastURLGrabberTextClipboard altogether?

## Fixing the bug through asynchronous work

On the topic of this bug, QMimeData::text() is blocking and there's no continuation-based alternative (even though the underlying xcb is asynchronous).

- The hard solution is to get text through raw xcb (and I'm not familiar with X so I can't do this without a lot of new learning).
- The easiest solution I can think of, which minimizes learning unknown APIs and code changes, is to move fetching clipboard contents (which can block) to either a new thread or a job in QThreadPool::globalInstance().
- This job will call HistoryItem::create(clipData), and sends the data back to a new Klipper::onClipData(HistoryItemPtr) method called in the main thread, which inserts the resulting HistoryItemPtr into history (hopefully in the same order as you copied text).
- How do we send a HistoryItemPtr back to Klipper on the main thread?
    - Add our own message queue which workers push onto, and having the Qt event loop poll it and invoke Klipper (probably difficult to achieve).
    - The worker job holds a QPointer<Klipper> and sends a custom QEvent subclass there. Overload QObject::event() in Klipper to dispatch on our new event ID. (Creates a use-after-free if the main thread deletes Klipper after it checks it's alive but before it sends a message.)
    - The worker job holds a QPointer<Klipper> and calls QMetaObject::invokeMethod(Klipper*, &Klipper::onClipData, HistoryItemPtr) (which internally sends a QMetaCallEvent, but Qt handles invoking the right method for us). (Creates a use-after-free if the main thread deletes Klipper after it checks it's alive but before it sends a message.)
    - Give the worker job a signal clipDataReady(HistoryItemPtr), and connect it to Klipper::onClipData(HistoryItemPtr) with a QueuedConnection. (I don't like dynamically adding connections, but it's immune to use-after-free I hope. But QRunnable can't have signals unless you inherit from QObject *and* QRunnable! https://forum.qt.io/post/357008)
    - Let the worker job update a QFutureInterface (stable but undocumented internal Qt type, similar to the documented QPromise in Qt 6), wrap a QFutureWatcher around the QFuture, and connect to Klipper::onClipData(HistoryItemPtr). At this point it's so complicated I'd rather inherit from QObject and QRunnable.
    - Should we wait for all worker threads to finish before destroying Klipper? Probably doesn't matter, it's fine to destroy the signal connections with Klipper, and let the background threads work until the process exits. (Does QApplication::exec() wait for all QThreadPool tasks to finish? I don't know. I sure hope that we don't get stuck background threads preventing plasmashell termination.)

The least bad solution I've come up with is a worker object inheriting from QObject and QRunnable, emitting a clipDataReady signal before exiting. Is there a better solution? I hope so.

## Reentrancy

Also we will need to make sure that splitting functionality across asynchronous callbacks does not interact incorrectly with Klipper::m_locklevel (apparently designed to prevent reentrant calls?).

I'm not sure all the ways Klipper::m_locklevel is manipulated, but the check in Klipper::applyClipChanges() (https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L657) seems to be completely redundant. Klipper::newClipData() (https://invent.kde.org/plasma/plasma-workspace/-/blob/master/klipper/klipper.cpp#L681) returns early if m_locklevel is nonzero, before calling -> Klipper::checkClipData -> Klipper::applyClipChanges, with no increments to m_locklevel along the way. The latter two methods have no other callers or references, but are bizarrely protected methods despite Klipper having no subclasses in all of plasma-workspace (and I hope it has no external subclasses in other Qt projects, https://lxr.kde.org/search?%21v=kf5-qt5&_filestring=&_string=Klipper&_casesensitive=1 didn't return anything). Because there is no other path to call Klipper::applyClipChanges() except through Klipper::newClipData() (which returns if m_locklevel != 0), then m_locklevel must be 0 in Klipper::applyClipChanges(). But I'd make this into an assertion to verify my reasoning, rather than remove this check entirely.

So in any case I don't *think* Klipper::m_locklevel matters.
Comment 9 nyanpasu64 2022-06-14 08:55:29 UTC
Problem: QMimeData is non-copyable, and obtained from KSystemClipboard (either QtClipboard or WaylandClipboard), and the main thread might erase it while the worker thread is in the middle of calling QMimeData::text() to request data from another process. Do we need to move all KSystemClipboard access to another thread, and only send contents to the main thread? Or is qGuiApp->clipboard() only safe to call (or call methods on QClipboard) from the main thread? In which case I see no solution without abandoning KSystemClipboard/QClipboard altogether.
Comment 10 Fushan Wen 2022-09-29 15:00:04 UTC
*** Bug 459766 has been marked as a duplicate of this bug. ***
Comment 11 chris.longros 2022-10-22 13:00:49 UTC
Temporary solution that worked for me is to disable Firefox notifications:

https://support.mozilla.org/en-US/questions/1283277
https://support.mozilla.org/en-US/questions/1345694
Comment 12 Nate Graham 2022-12-05 19:18:46 UTC
*** Bug 462622 has been marked as a duplicate of this bug. ***
Comment 13 Fushan Wen 2023-02-11 15:16:04 UTC
*** Bug 360262 has been marked as a duplicate of this bug. ***
Comment 14 Piotrek 2024-05-12 13:50:10 UTC
Still happens with Firefox 125.0.1 and KDE Framework Version 5.115.0, Qt Version 5.15.12 (built against 5.15.12). Workaround with setting Firefox flag in about:config 

alerts.useSystemBackend=false

but this is a workaround and should be fixed properly.
Comment 15 Nate Graham 2024-06-21 02:41:42 UTC
*** Bug 393804 has been marked as a duplicate of this bug. ***
Comment 16 TraceyC 2024-10-01 18:52:38 UTC
This seems somewhat related to this other bug, although the crashes are different
https://bugs.kde.org/show_bug.cgi?id=480312