https://bugs.kde.org/show_bug.cgi?id=525482
Bug ID: 525482
Summary: Ghostwriter hangs on quit - self-deadlock in custom
message handler vs QObject::disconnect()
Classification: Applications
Product: ghostwriter
Version First 26.04.3
Reported In:
Platform: Other
OS: Linux
Status: REPORTED
Severity: normal
Priority: NOR
Component: general
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ---
SUMMARY
-------
Ghostwriter hangs completely (100% unresponsive, 0% CPU, must be killed) when
closing the main window / quitting the application, if a Qt runtime warning
happens to be emitted while MainWindow::quitApplication() is disconnecting
signals. The custom message handler self-deadlocks against Qt's own internal
object lock.
Reproducible in the current release (26.08.1) and master - verified by diffing
src/logging.cpp and rc/mainwindow.cpp between v26.08.1 and master, they are
identical, so this is not fixed in the latest release.
ENVIRONMENT
-----------
- ghostwriter 26.04.3 (Fedora 43 package, ghostwriter-26.04.3-1.fc43.x86_64)
- Qt 6.10.3 (qt6-qtbase-6.10.3-1.fc43, qt6-qtwebengine-6.10.3-1.fc43)
- Fedora Linux 43, kernel 7.1.13-100.fc43.x86_64
- GNOME / Wayland session (XDG_SESSION_TYPE=wayland)
- pandoc 3.6.4, multimarkdown 6.7.0, cmark 0.30.3 (as reported by ghostwriter's
own startup log)
STEPS TO REPRODUCE
-------------------
1. Open ghostwriter with a document.
2. Close the main window (or Quit from the menu), triggering
MainWindow::quitApplication().
3. Intermittently (timing/race-dependent - I could not find a 100%
deterministic trigger), the application does not exit: the window disappears
from window management state changes are inconsistent, but the process keeps
running at 0% CPU and never terminates. Every UI action is ignored. Must be
killed with `kill -9`.
I was not able to force the race on demand, but I could catch it live with gdb
once it happened during normal use, see below.
ROOT CAUSE (confirmed via gdb backtrace of the frozen process)
----------------------------------------------------------------
MainWindow::quitApplication() (src/mainwindow.cpp, current master lines
297-299) calls:
this->editor->document()->disconnect();
this->editor->disconnect();
this->htmlPreview->disconnect();
`QObject::disconnect()` internally can emit a Qt runtime warning via
`qWarning()` from *inside* `QMetaObjectPrivate::disconnect()`, i.e. while that
function is still holding Qt's internal per-object signal/slot lock (the
striped mutex returned by `signalSlotLock()`).
Ghostwriter installs a custom message handler (`ghostwriter::logMessage`,
src/logging.cpp) via `qInstallMessageHandler()`. On *every* log message
(including this warning), `logMessage()` constructs a
brand new `QTextStream stream(dest);`. Constructing a `QTextStream` on a
`FILE*` internally performs a `QObject::connect()` (via
`QObjectPrivate::connectImpl`), which needs to lock the very same internal
per-object mutex that the outer `QObject::disconnect()` call above is still
holding on the current thread.
Since that mutex is not recursive, the thread deadlocks on itself, permanently,
inside `QBasicMutex::lockInternal()`.
Confirmed backtrace of the frozen main thread (Thread 1), captured with `gdb -p
<pid> -batch -ex "thread 1" -ex bt`:
#0 syscall () from /lib64/libc.so.6
#1 QBasicMutex::lockInternal () from /lib64/libQt6Core.so.6
#2 QObjectPrivate::connectImpl(...) from /lib64/libQt6Core.so.6
#3 QObject::connectImpl(...) from /lib64/libQt6Core.so.6
#4 QTextStream::QTextStream(_IO_FILE*,
QFlags<QIODeviceBase::OpenModeFlag>) from /lib64/libQt6Core.so.6
#5 ghostwriter::logMessage(QtMsgType, QMessageLogContext const&, QString
const&) ()
#6 qt_message_print(...) from /lib64/libQt6Core.so.6
#7 qt_message(...) from /lib64/libQt6Core.so.6
#8 QMessageLogger::warning(char const*, ...) const from
/lib64/libQt6Core.so.6
#9 QMetaObjectPrivate::disconnect(...) [clone .constprop.0] from
/lib64/libQt6Core.so.6
#10 QObject::disconnect(QObject const*, char const*, QObject const*, char
const*) from /lib64/libQt6Core.so.6
#11 ghostwriter::MainWindow::quitApplication() [clone .part.0] ()
#12 QWidget::event(QEvent*) from /lib64/libQt6Widgets.so.6
#13 QApplicationPrivate::notify_helper(QObject*, QEvent*) from
/lib64/libQt6Widgets.so.6
#14 QCoreApplication::notifyInternal2(QObject*, QEvent*) from
/lib64/libQt6Core.so.6
#15 QWidgetPrivate::handleClose(QWidgetPrivate::CloseMode) from
/lib64/libQt6Widgets.so.6
#16 QWidgetWindow::closeEvent(QCloseEvent*) from /lib64/libQt6Widgets.so.6
#17 QWindow::event(QEvent*) from /lib64/libQt6Gui.so.6
#18 QApplicationPrivate::notify_helper(QObject*, QEvent*) from
/lib64/libQt6Widgets.so.6
#19 QCoreApplication::notifyInternal2(QObject*, QEvent*) from
/lib64/libQt6Core.so.6
#20 QGuiApplicationPrivate::processCloseEvent(...) from
/lib64/libQt6Gui.so.6
#21 QWindowSystemInterface::sendWindowSystemEvents(...) from
/lib64/libQt6Gui.so.6
#22 userEventSourceDispatch(...) from /lib64/libQt6Gui.so.6
#23 g_main_context_dispatch_unlocked.lto_priv () from
/lib64/libglib-2.0.so.0
#24 g_main_context_iterate_unlocked.isra ()
#25 g_main_context_iteration ()
#26 QEventDispatcherGlib::processEvents(...) from /lib64/libQt6Core.so.6
#27 QEventLoop::exec(...) from /lib64/libQt6Core.so.6
#28 QCoreApplication::exec() from /lib64/libQt6Core.so.6
#29 main ()
All 70 other threads were idle (parked on futex/poll), confirming this is a
genuine self-deadlock of the main thread, not a busy loop or I/O stall. Nothing
was ever written to stdout/stderr for this warning (checked via
journalctl), because the handler deadlocks before it can flush - consistent
with the backtrace above.
SUGGESTED FIX
-------------
`src/logging.cpp`'s `logMessage()` should not construct a `QObject`-backed
`QTextStream` on every call (which is what makes it re-entrancy-unsafe with
respect to Qt's own internal locking). Writing directly to the `FILE*` avoids
touching any QObject machinery and removes the reentrancy hazard entirely,
e.g.:
// instead of:
// QTextStream stream(dest);
// stream << text << Qt::endl;
// stream.flush();
fputs(qUtf8Printable(text), dest);
fputc('\n', dest);
fflush(dest);
(same idea applies to the `QtFatalMsg` branch further down, which also uses
`stream <<`).
I have not opened a merge request for this, since I could not build a reliable,
on-demand reproduction to validate the fix with certainty (the race depends on
a Qt-internal warning firing at exactly the right moment
during shutdown) — happy to test a candidate patch against my setup if someone
wants to propose one, or to submit an MR with the above change if that's useful
even without a guaranteed repro.
--
You are receiving this mail because:
You are watching all bug changes.