From 872067623102e3b27320ee3a6375d383206a5a45 Mon Sep 17 00:00:00 2001 From: Ralph Martin Fischer Date: Wed, 30 Sep 2026 06:35:59 +0200 Subject: [PATCH] Absturz beim Start einer zweiten Fassung neben der laufenden behoben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Absturzbericht Contestprogramm-2026-09-28-110149.ips: 0.1.1 lief seit 08:57, um 11:01:33 EXC_BAD_ACCESS bei 0x30 im Hauptthread, in doActivate <- QIODevice::channelReadyRead <- canReadNotification <- QReadNotifier. Zwei Minuten vorher lag /Applications/Contestprogramm- neu.app zum Testen bereit, und Martin startete sie neben der laufenden. Der Weg: die neue Fassung meldet sich beim SingleInstanceGuard der laufenden mit einem anderen Baustempel. Der liest die Zeile im readyRead des QLocalSocket, sendet newerBuildStarted, main.cpp startet neu und ruft dafür release() auf. release() löschte den QLocalServer sofort, und mit ihm den Client-Socket (dessen Kind), während der noch in seinem eigenen readyRead steckte. Qt sendet direkt danach channelReadyRead() auf demselben Socket: in freigegebenen Speicher. macOS nullt den, d_ptr ist 0, Lesezugriff auf 0x30. Die vier TCP-Clients (ON4KST, Cluster, rigctld, rotctld) scheiden aus: sie leben als Wert-Member von AppController so lange wie das Programm, ihre Sockets werden nur geschlossen, nie gelöscht. Der Fix: release() schließt den Server wie bisher (lauscht nicht mehr, Name frei, Sperre frei), löscht das Objekt aber per deleteLater(), also erst, wenn der Stapel abgebaut ist. Die Annahmeschleife hält den Server als Zeiger statt über m_server, das release() mittendrin leert. Nachgestellt, vorher und nachher: - Test releasingFromTheRestartSignalDoesNotPullTheSocketFromUnderItsOwnRead: ohne Fix rot auf jeder Plattform (Socket überlebt release() nicht); unter ASan mit free_fill_byte=0 Absturz an genau den Offsets des Berichts (QtCore+0xf2f80, Zugriff 0x30). - Live mit Sandbox-Datenordner: installierte Fassung laufen lassen, zweite Fassung daneben starten -> Status 139, neuer Absturzbericht mit denselben fünf Qt-Offsets wie Martins. Mit Fix 3/3 sauberer Neustart, Status 0, kein Bericht. Co-Authored-By: Claude Opus 5.5 --- src/app/SingleInstanceGuard.cpp | 18 +++++++++++--- tests/test_single_instance.cpp | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) 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;