Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/app/SingleInstanceGuard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ void SingleInstanceGuard::release()
if (m_server) {
m_server->close();
QLocalServer::removeServer(m_server->serverName());
m_server.reset();
// Not deleted here: release() is called from newerBuildStarted
// (main.cpp's restart), i.e. from inside a client socket's
// readyRead -- and the client is the server's child. Deleting
// the server took the socket with it while Qt was still in its
// read notification, which then emitted channelReadyRead() on
// freed memory (crash 2026-09-28 11:01:33, second build started
// beside the running one). close() already stops listening and
// frees the name, the object itself can go once the stack is
// unwound.
m_server.release()->deleteLater();
}
if (m_lock) {
m_lock->unlock();
Expand Down Expand Up @@ -102,8 +111,11 @@ bool SingleInstanceGuard::tryAcquire()
m_server = std::make_unique<QLocalServer>(this);
QLocalServer::removeServer(serverName); // leftover socket file from a crash
m_server->listen(serverName);
connect(m_server.get(), &QLocalServer::newConnection, this, [this] {
while (QLocalSocket* client = m_server->nextPendingConnection()) {
// The server by pointer, not m_server: handle() below may run
// release() right here, which empties m_server while this loop is
// still asking for the next connection (none, once closed).
connect(m_server.get(), &QLocalServer::newConnection, this, [this, server = m_server.get()] {
while (QLocalSocket* client = server->nextPendingConnection()) {
const auto handle = [this, client] {
const QByteArray line = client->readAll().trimmed();
if (!line.startsWith(kRaiseVerb)) {
Expand Down
44 changes: 44 additions & 0 deletions tests/test_single_instance.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <QtTest>

#include <QLocalSocket>
#include <QPointer>
#include <QSignalSpy>
#include <QTemporaryDir>

Expand All @@ -16,6 +18,7 @@ class TestSingleInstance : public QObject
private slots:
void secondStartOnTheSameDirectoryHandsOverAndIsRefused();
void aStartFromANewerBuildAsksForARestart();
void releasingFromTheRestartSignalDoesNotPullTheSocketFromUnderItsOwnRead();
void differentDirectoriesDoNotInterfere();
void aFinishedInstanceFreesTheDirectory();
};
Expand Down Expand Up @@ -63,6 +66,47 @@ void TestSingleInstance::aStartFromANewerBuildAsksForARestart()
QCOMPARE(activated.count(), 1);
}

// Absturz 2026-09-28 11:01:33 (Contestprogramm-2026-09-28-110149.ips):
// a second build was started beside the running one, main.cpp's restart
// handler ran release() straight from newerBuildStarted -- that deleted
// the QLocalServer and with it the client socket, while the socket was
// still inside its own readyRead. Qt emits channelReadyRead() on it right
// after, into freed memory: EXC_BAD_ACCESS at 0x30 in doActivate. Under
// AddressSanitizer with free_fill_byte=0 (freed memory zeroed, like the
// crash) the unfixed code dies here at exactly the reported offsets.
void TestSingleInstance::releasingFromTheRestartSignalDoesNotPullTheSocketFromUnderItsOwnRead()
{
QTemporaryDir dir;
SingleInstanceGuard first(dir.path());
first.setBuildStamp(QStringLiteral("1000"));
QVERIFY(first.tryAcquire());
int restarts = 0;
bool socketOutlivedRelease = false;
// The same order as main.cpp's restart lambda: release() first.
connect(&first, &SingleInstanceGuard::newerBuildStarted, this, [&] {
// The connection that brought the line -- this slot runs inside
// its readyRead, so it must still exist when release() returns
// (only an ASan build notices the use itself, and only with
// free_fill_byte; this check fails on every platform).
const QPointer<QLocalSocket> client = first.findChild<QLocalSocket*>();
QVERIFY(client);
first.release();
socketOutlivedRelease = !client.isNull();
++restarts;
});

SingleInstanceGuard rebuilt(dir.path());
rebuilt.setBuildStamp(QStringLiteral("2000"));
QVERIFY(!rebuilt.tryAcquire());
QTRY_COMPARE(restarts, 1);
QVERIFY(socketOutlivedRelease);
// Let the deferred clean-up run, then the directory is free again.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QTest::qWait(50);
SingleInstanceGuard successor(dir.path());
QVERIFY(successor.tryAcquire());
}

void TestSingleInstance::differentDirectoriesDoNotInterfere()
{
QTemporaryDir a;
Expand Down
Loading