#24924·qBittorrent

SIGSEGV: use-after-free in LocalPeer::receivedConnection() when opening an already-present .torrent

Author: JJenkxCreated Sep 16, 2026Updated Sep 17, 2026
LabelsCrash

qBittorrent & operating system versions

qBittorrent:          v5.3.0beta1 (built from source at commit 3c4409d)
libtorrent-rasterbar: 2.1.1
Qt:                   6.11.2
OS:                   Arch Linux, kernel 7.2.4
Desktop:              KDE Plasma (Wayland)

What is the problem?

Opening a .torrent file that is already present in the session, while qBittorrent is already running, crashes the running instance with SIGSEGV. If the torrent is not already present, the same action works fine.

The crash is a use-after-free in LocalPeer::receivedConnection() (src/app/localpeer.cpp). The readyRead handler emits messageReceived() while still inside the socket's own slot, and that signal can open a modal dialog, whose nested event loop then destroys the socket out from under the signal emission that is still in progress.

Backtrace

Paths redacted; this is a source build, so frames 0–1 are the crash handler itself and the faulting frame is #3, reached from QIODevice::channelReadyRead:

qBittorrent version: v5.3.0beta1
Caught signal: SIGSEGV
 0# getStacktrace[abi:cxx11]() in <build>/qbittorrent
 1# 0x00000000002932B2 in <build>/qbittorrent        # (anonymous namespace)::abnormalExitHandler(int)
 2# 0x000000000003E6F0 in /usr/lib/libc.so.6
 3# 0x00000000001EEA53 in /usr/lib/libQt6Core.so.6
 4# QIODevice::channelReadyRead(int) in /usr/lib/libQt6Core.so.6
 5# QAbstractSocketPrivate::canReadNotification() in /usr/lib/libQt6Network.so.6
 6# 0x00000000000CB692 in /usr/lib/libQt6Network.so.6
 7# QApplicationPrivate::notify_helper(QObject*, QEvent*) in /usr/lib/libQt6Widgets.so.6
 8# QCoreApplication::notifyInternal2(QObject*, QEvent*) in /usr/lib/libQt6Core.so.6
 9# 0x000000000049A1D0 in /usr/lib/libQt6Core.so.6
10# 0x0000000000061C76 in /usr/lib/libglib-2.0.so.0
11# 0x0000000000063ED7 in /usr/lib/libglib-2.0.so.0
12# g_main_context_iteration in /usr/lib/libglib-2.0.so.0
13# QEventDispatcherGlib::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) in /usr/lib/libQt6Core.so.6
14# QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) in /usr/lib/libQt6Core.so.6
15# QCoreApplication::exec() in /usr/lib/libQt6Core.so.6
16# Application::exec() in <build>/qbittorrent
17# main in <build>/qbittorrent

Analysis

LocalPeer::receivedConnection() is src/app/localpeer.cpp lines 235–279 (at commit 3c4409d). The relevant part:

cpp
auto *buffer = new QByteArray;

connect(socket, &QLocalSocket::disconnected, this, [buffer, socket]   // L243
{
    socket->deleteLater();                                            // L245
    delete buffer;                                                    // L246
});
connect(socket, &QIODevice::readyRead, this, [this, buffer, socket]   // L248
{
    // ...
    socket->write(ACK);                                               // L274
    socket->disconnectFromServer();                                   // L275

    emit messageReceived(result.value()); // might take a long time to return   // L277
});

The existing comment on L277 understates the hazard: the problem is not that the call is slow, it is that it re-enters the event loop.

  1. messageReceived() is connected through ApplicationInstanceManager to Application::processMessage(), which adds the torrent. When the torrent is already in the session, that path opens a modal dialog (the "merge trackers" prompt). When it is not already present, no modal dialog is shown — which is exactly why the crash is specific to duplicate torrents.
  2. A modal dialog spins a nested event loop, entered while still inside this socket's readyRead slot, i.e. while QIODevice::channelReadyRead is part-way through delivering the signal.
  3. socket->disconnectFromServer() on L275 emits disconnected() synchronously when there is nothing left to write, so by the time L277 runs, the handler at L243 has already called socket->deleteLater().
  4. The nested event loop processes that deferred deletion, destroying the QLocalSocket.
  5. When the nested loop unwinds, Qt resumes iterating the connection list of an object that no longer exists → SIGSEGV inside QIODevice::channelReadyRead.

There is a second, related lifetime problem on the same lines. The disconnected handler calls delete buffer (L246) immediately, not deferred, while the readyRead connection is still active. Any further read delivered to that socket before its deletion is processed will buffer->append(...) into freed memory.

Steps to reproduce

  1. Start qBittorrent and add any torrent, so it is present in the session.
  2. With qBittorrent still running, open the same .torrent file again from outside the application, so it goes through the single-instance IPC path rather than the in-app add dialog — e.g. double-click it in a file manager, or run qbittorrent /path/to/that.torrent in a terminal.
  3. The running instance is asked to merge trackers, and crashes with SIGSEGV.

The mechanism is a race between the modal dialog's nested event loop and the deferred deletion of the socket, so it is timing-dependent and may not fire on every attempt. Adding a torrent that is not already present through the same path does not crash.

Log(s) & preferences file(s)

The crash produces no log output — the SIGSEGV backtrace (included above) is written to stderr by qBittorrent's own signal handler, and qbittorrent.log ends at the last normal entry before the crash. I would rather not attach my preferences file as it contains a WebUI credential hash; happy to provide any specific setting on request.

Additional context

Attribution: the analysis above and the patch below were written by Claude Opus (an AI assistant), working from the backtrace and the upstream source. I have reviewed and tested it on my own build, but I did not write it. Please weigh it accordingly, and disregard the patch entirely if the project does not accept AI-authored contributions — the crash and the backtrace stand on their own either way.

Suggested fix

Get off the socket's stack before anything can run a nested event loop: copy the message out, drop the readyRead connection so a further read cannot touch the freed buffer, and emit the signal from a queued invocation.

diff
--- a/src/app/localpeer.cpp
+++ b/src/app/localpeer.cpp
@@ -272,9 +272,15 @@ void LocalPeer::receivedConnection()
         }
 
         socket->write(ACK);
+
+        const QString message = result.value();
+        disconnect(socket, &QIODevice::readyRead, this, nullptr);
         socket->disconnectFromServer();
 
-        emit messageReceived(result.value()); // might take a long time to return
+        QMetaObject::invokeMethod(this, [this, message]
+        {
+            emit messageReceived(message);
+        }, Qt::QueuedConnection);
     });
 }

With this applied I have not seen the crash again.

Disclosure

My binary is a source build that carries unrelated local modifications (peer statistics and some GUI columns). src/app/localpeer.cpp is not among them — it is unmodified from commit 3c4409d, and nothing I changed touches the single-instance IPC, the add-torrent path, or Qt networking.

I have not reproduced this on a pristine upstream build, so please treat the repro steps as coming from a patched tree even though the affected file is untouched. The analysis is derived from the backtrace plus the code as it stands upstream, and should be verifiable by inspection independently of my build.