From 4752158a5f40216eba0eea223cc635c1346dfddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:43:26 +0200 Subject: [PATCH] fix(networkjob): drain pending events before deleting an aborted reply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a QNetworkReply is abandoned we called reply->disconnect(); reply->abort(); reply->deleteLater(). The no-arg disconnect() only drops connections where the reply is the *sender*, so the reply's internal transfer-timeout timer - connected timeout() -> _q_transferTimedOut() -> abort() via a Qt::QueuedConnection with the reply as the *receiver* - is left in place. If that timer had already fired, a queued _q_transferTimedOut metacall was posted; deleteLater() does not flush already-posted metacalls, so after the reply was freed the metacall was delivered and abort()'s emit walked a dead object -> a use-after-free. On Windows this manifested as a crash about a second after waking from sleep or hibernation: the single-shot transfer timers frozen during sleep all fire at once into the reachability-driven teardown storm, making the race deterministic. Route both reply-freeing sites (adoptRequest and ~AbstractNetworkJob) through a small severAndDeleteReply() helper that additionally calls QCoreApplication::removePostedEvents(reply) before deleteLater(), draining any already-posted metacall targeting the reply. https://github.com/owncloud/client/issues/12600 Co-Authored-By: Claude Opus 4.8 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- changelog/unreleased/12600.md | 9 +++++++++ src/libsync/abstractnetworkjob.cpp | 28 ++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 changelog/unreleased/12600.md diff --git a/changelog/unreleased/12600.md b/changelog/unreleased/12600.md new file mode 100644 index 00000000000..e123057c4da --- /dev/null +++ b/changelog/unreleased/12600.md @@ -0,0 +1,9 @@ +Bugfix: Fix crash after waking the computer from sleep + +We've fixed a crash that could occur about a second after the computer resumed +from sleep or hibernation. A network reply's internal transfer-timeout timer +could deliver a queued call to a reply that had already been deleted during the +reconnect handling, causing a use-after-free. Replies are now drained of any +pending events before they are deleted. + +https://github.com/owncloud/client/issues/12600 diff --git a/src/libsync/abstractnetworkjob.cpp b/src/libsync/abstractnetworkjob.cpp index 151ae2ab594..d3e25d866f9 100644 --- a/src/libsync/abstractnetworkjob.cpp +++ b/src/libsync/abstractnetworkjob.cpp @@ -14,6 +14,7 @@ */ #include +#include #include #include "common/asserts.h" @@ -30,6 +31,25 @@ using namespace std::chrono_literals; namespace { constexpr int MaxRetryCount = 5; + +// Tear down a reply we are abandoning. +// +// reply->abort() also stops the reply's internal transfer-timeout QTimer, but a +// _q_transferTimedOut metacall that this timer already posted (it is connected +// via a Qt::QueuedConnection with the reply as the *receiver*, a role the plain +// reply->disconnect() does not cover) would survive deleteLater() and be +// delivered to freed memory - the wake-from-sleep use-after-free (#12600). +// removePostedEvents() drains any such pending call before we schedule deletion. +void severAndDeleteReply(QNetworkReply *reply) +{ + if (!reply) { + return; + } + reply->disconnect(); + reply->abort(); + QCoreApplication::removePostedEvents(reply); + reply->deleteLater(); +} } @@ -172,9 +192,7 @@ void AbstractNetworkJob::adoptRequest(QPointer reply) { std::swap(_reply, reply); if (reply) { - reply->disconnect(); - reply->abort(); - reply->deleteLater(); + severAndDeleteReply(reply.data()); } _request = _reply->request(); @@ -289,9 +307,7 @@ AbstractNetworkJob::~AbstractNetworkJob() qCCritical(lcNetworkJob) << "Deleting running job" << this; } if (_reply) { - _reply->disconnect(); - _reply->abort(); - _reply->deleteLater(); + severAndDeleteReply(_reply.data()); _reply.clear(); } }