diff --git a/src/app/SingleInstanceGuard.cpp b/src/app/SingleInstanceGuard.cpp index 050dcf5..3c54cff 100644 --- a/src/app/SingleInstanceGuard.cpp +++ b/src/app/SingleInstanceGuard.cpp @@ -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(); @@ -102,8 +111,11 @@ bool SingleInstanceGuard::tryAcquire() m_server = std::make_unique(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)) { diff --git a/tests/test_single_instance.cpp b/tests/test_single_instance.cpp index 5e50041..ade8353 100644 --- a/tests/test_single_instance.cpp +++ b/tests/test_single_instance.cpp @@ -1,5 +1,7 @@ #include +#include +#include #include #include @@ -16,6 +18,7 @@ class TestSingleInstance : public QObject private slots: void secondStartOnTheSameDirectoryHandsOverAndIsRefused(); void aStartFromANewerBuildAsksForARestart(); + void releasingFromTheRestartSignalDoesNotPullTheSocketFromUnderItsOwnRead(); void differentDirectoriesDoNotInterfere(); void aFinishedInstanceFreesTheDirectory(); }; @@ -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 client = first.findChild(); + 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;