Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libqxmpp for openSUSE:Factory checked in at 2026-04-23 17:09:50 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libqxmpp (Old) and /work/SRC/openSUSE:Factory/.libqxmpp.new.11940 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libqxmpp" Thu Apr 23 17:09:50 2026 rev:47 rq:1348945 version:1.15.1 Changes: -------- --- /work/SRC/openSUSE:Factory/libqxmpp/libqxmpp.changes 2026-04-08 17:16:05.128796782 +0200 +++ /work/SRC/openSUSE:Factory/.libqxmpp.new.11940/libqxmpp.changes 2026-04-23 17:14:17.796588461 +0200 @@ -1,0 +2,12 @@ +Thu Apr 23 12:01:56 UTC 2026 - Christophe Marin <[email protected]> + +- Update to 1.15.1 + * Fix use-after-free in sendSensitive() coroutine lambdas + * Move sendSensitive() coroutine lambdas to QXmppClientPrivate + methods + * QXmppTask::then(): co_return continuation result, document + return value + * Client: Detect same-type listener replacement during + handleElement() + +------------------------------------------------------------------- Old: ---- qxmpp-1.15.0.tar.xz qxmpp-1.15.0.tar.xz.sig New: ---- qxmpp-1.15.1.tar.xz qxmpp-1.15.1.tar.xz.sig ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libqxmpp.spec ++++++ --- /var/tmp/diff_new_pack.tfDZhE/_old 2026-04-23 17:14:19.440656168 +0200 +++ /var/tmp/diff_new_pack.tfDZhE/_new 2026-04-23 17:14:19.448656498 +0200 @@ -20,7 +20,7 @@ %define sover 9 Name: libqxmpp -Version: 1.15.0 +Version: 1.15.1 Release: 0 Summary: Qt XMPP Library License: LGPL-2.1-or-later ++++++ qxmpp-1.15.0.tar.xz -> qxmpp-1.15.1.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/CHANGELOG.md new/qxmpp-1.15.1/CHANGELOG.md --- old/qxmpp-1.15.0/CHANGELOG.md 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/CHANGELOG.md 2026-04-10 16:18:25.000000000 +0200 @@ -4,6 +4,14 @@ SPDX-License-Identifier: CC0-1.0 --> +QXmpp 1.15.1 (April 10, 2026) +----------------------------- + + - Fix use-after-free in sendSensitive() coroutine lambdas causing encryption results to be silently lost (@lnj) + - Move sendSensitive() coroutine lambdas to QXmppClientPrivate methods to avoid closure lifetime issues (@lnj) + - QXmppTask::then(): co_return continuation result so chaining with non-void continuations compiles (@lnj) + - Fix QXmppPromise::finish() to construct T instead of assigning (@lnj) + QXmpp 1.15.0 (April 5, 2026) ---------------------------- @@ -19,6 +27,11 @@ - CMake: Rename `BUILD_TESTS` option to `BUILD_TESTING` (@lnj, !738) - Drop Qt 5 support, require Qt 6.4 (@lnj, !759) +QXmpp 1.14.7 (April 10, 2026) +----------------------------- + + - Fix `Sasl2Manager` listener being replaced after FAST→password fallback retry (@lnj) + QXmpp 1.14.6 (April 3, 2026) ---------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/CMakeLists.txt new/qxmpp-1.15.1/CMakeLists.txt --- old/qxmpp-1.15.0/CMakeLists.txt 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/CMakeLists.txt 2026-04-10 16:18:25.000000000 +0200 @@ -3,7 +3,7 @@ # SPDX-License-Identifier: CC0-1.0 cmake_minimum_required(VERSION 3.16) -project(qxmpp VERSION 1.15.0) +project(qxmpp VERSION 1.15.1) set(SO_VERSION 9) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/docs/doap.xml new/qxmpp-1.15.1/docs/doap.xml --- old/qxmpp-1.15.0/docs/doap.xml 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/docs/doap.xml 2026-04-10 16:18:25.000000000 +0200 @@ -783,12 +783,26 @@ </implements> <release> <Version> + <revision>1.15.1</revision> + <created>2026-04-10</created> + <file-release rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.15.1.tar.xz'/> + </Version> + </release> + <release> + <Version> <revision>1.15.0</revision> <created>2026-04-05</created> <file-release rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.15.0.tar.xz'/> </Version> </release> <release> + <Version> + <revision>1.14.7</revision> + <created>2026-04-10</created> + <file-release rdf:resource='https://download.kde.org/unstable/qxmpp/qxmpp-1.14.7.tar.xz'/> + </Version> + </release> + <release> <Version> <revision>1.14.6</revision> <created>2026-04-03</created> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/src/base/QXmppTask.h new/qxmpp-1.15.1/src/base/QXmppTask.h --- old/qxmpp-1.15.0/src/base/QXmppTask.h 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/src/base/QXmppTask.h 2026-04-10 16:18:25.000000000 +0200 @@ -209,16 +209,16 @@ { if (shared()) { sharedData().finished = true; - sharedData().result = std::forward<U>(value); + sharedData().result.emplace(std::forward<U>(value)); } else { if (auto *task = inlineData().task) { inlineData().task->inlineData().finished = true; - inlineData().task->inlineData().result = std::forward<U>(value); + inlineData().task->inlineData().result.emplace(std::forward<U>(value)); } else { // finish called without generating task detachData(); sharedData().finished = true; - sharedData().result = std::forward<U>(value); + sharedData().result.emplace(std::forward<U>(value)); } } invokeHandle(); @@ -229,7 +229,7 @@ /// /// If a task is cancelled, no call to `finish()` is needed and no continuation is resumed. /// - /// \since QXmpp 1.11 + /// \since QXmpp 1.15 /// bool cancelled() const { @@ -445,16 +445,29 @@ /// deleted. This way your lambda will never be executed after your object has been deleted. /// \param continuation A function accepting a result in the form of `T &&`. /// + /// \returns A new QXmppTask that finishes with the value returned by \p continuation, allowing + /// further chaining via another `.then()` or `co_await`. If \p continuation returns `void`, + /// the returned task is `QXmppTask<void>` which finishes once the continuation has run. + /// template<typename Continuation> auto then(const QObject *context, Continuation continuation) -> QXmppTask<QXmpp::Private::InvokeContinuationResult<Continuation, T>> { + using Result = QXmpp::Private::InvokeContinuationResult<Continuation, T>; QXmppTask<T> task = std::move(*this); if constexpr (std::is_void_v<T>) { co_await task.withContext(context); - continuation(); + if constexpr (std::is_void_v<Result>) { + continuation(); + } else { + co_return continuation(); + } } else { - continuation(co_await task.withContext(context)); + if constexpr (std::is_void_v<Result>) { + continuation(co_await task.withContext(context)); + } else { + co_return continuation(co_await task.withContext(context)); + } } } @@ -467,7 +480,7 @@ /// /// \returns reference to this task /// - /// \since QXmpp 1.11 + /// \since QXmpp 1.15 /// QXmppTask<T> &withContext(const QObject *c) { @@ -491,7 +504,7 @@ /// If there is a waiting coroutine, it is cancelled immediately. Any continuation set in the /// future also won't be executed. /// - /// \since QXmpp 1.11 + /// \since QXmpp 1.15 /// void cancel() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppClient.cpp new/qxmpp-1.15.1/src/client/QXmppClient.cpp --- old/qxmpp-1.15.0/src/client/QXmppClient.cpp 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/src/client/QXmppClient.cpp 2026-04-10 16:18:25.000000000 +0200 @@ -499,36 +499,38 @@ /// QXmppTask<QXmpp::SendResult> QXmppClient::sendSensitive(QXmppStanza &&stanza, const std::optional<QXmppSendStanzaParams> ¶ms) { - auto sendEncryptedMessage = [this](auto &&task) -> QXmppTask<QXmpp::SendResult> { - auto result = co_await task.withContext(this); - if (std::holds_alternative<QXmppError>(result)) { - co_return std::get<QXmppError>(std::move(result)); - } - QByteArray xml; - QXmlStreamWriter writer(&xml); - std::get<std::unique_ptr<QXmppMessage>>(result)->toXml(&writer, QXmpp::ScePublic); - - co_return co_await d->stream->streamAckManager().send(QXmppPacket(xml, true)); - }; - - auto sendEncryptedIq = [this](auto &&task) -> QXmppTask<QXmpp::SendResult> { - auto result = co_await task.withContext(this); - if (std::holds_alternative<QXmppError>(result)) { - co_return std::get<QXmppError>(std::move(result)); - } - co_return co_await d->stream->streamAckManager().send(QXmppPacket(*std::get<std::unique_ptr<QXmppIq>>(result))); - }; - if (d->encryptionExtension) { if (dynamic_cast<QXmppMessage *>(&stanza)) { - return sendEncryptedMessage(d->encryptionExtension->encryptMessage(dynamic_cast<QXmppMessage &&>(stanza), params)); + return d->sendEncryptedMessage(d->encryptionExtension->encryptMessage(dynamic_cast<QXmppMessage &&>(stanza), params)); } else if (dynamic_cast<QXmppIq *>(&stanza)) { - return sendEncryptedIq(d->encryptionExtension->encryptIq(dynamic_cast<QXmppIq &&>(stanza), params)); + return d->sendEncryptedIq(d->encryptionExtension->encryptIq(dynamic_cast<QXmppIq &&>(stanza), params)); } } return d->stream->streamAckManager().send(stanza); } +QXmppTask<QXmpp::SendResult> QXmppClientPrivate::sendEncryptedMessage(QXmppTask<QXmppE2eeExtension::MessageEncryptResult> task) +{ + auto result = co_await task.withContext(q); + if (std::holds_alternative<QXmppError>(result)) { + co_return std::get<QXmppError>(std::move(result)); + } + QByteArray xml; + QXmlStreamWriter writer(&xml); + std::get<std::unique_ptr<QXmppMessage>>(result)->toXml(&writer, QXmpp::ScePublic); + + co_return co_await stream->streamAckManager().send(QXmppPacket(xml, true)); +} + +QXmppTask<QXmpp::SendResult> QXmppClientPrivate::sendEncryptedIq(QXmppTask<QXmppE2eeExtension::IqEncryptResult> task) +{ + auto result = co_await task.withContext(q); + if (std::holds_alternative<QXmppError>(result)) { + co_return std::get<QXmppError>(std::move(result)); + } + co_return co_await stream->streamAckManager().send(QXmppPacket(*std::get<std::unique_ptr<QXmppIq>>(result))); +} + /// /// Sends a packet always without end-to-end-encryption. /// @@ -600,43 +602,44 @@ /// QXmppTask<QXmppClient::IqResult> QXmppClient::sendSensitiveIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> ¶ms) { - auto sendEncryptedIq = [this, params](QXmppIq &&iq) -> QXmppTask<IqResult> { - auto encryptResult = co_await d->encryptionExtension->encryptIq(std::move(iq), params); + return d->encryptionExtension ? d->sendSensitiveIq(std::move(iq), params) : d->stream->sendIq(std::move(iq)); +} - if (std::holds_alternative<QXmppError>(encryptResult)) { - co_return std::get<QXmppError>(std::move(encryptResult)); - } +QXmppTask<QXmppClient::IqResult> QXmppClientPrivate::sendSensitiveIq(QXmppIq iq, std::optional<QXmppSendStanzaParams> params) +{ + auto encryptResult = co_await encryptionExtension->encryptIq(std::move(iq), params); - auto encryptedIq = std::get<std::unique_ptr<QXmppIq>>(std::move(encryptResult)); - auto sendResult = co_await d->stream->sendIq(std::move(*encryptedIq)).withContext(this); + if (std::holds_alternative<QXmppError>(encryptResult)) { + co_return std::get<QXmppError>(std::move(encryptResult)); + } - if (std::holds_alternative<QXmppError>(sendResult)) { - co_return std::get<QXmppError>(std::move(sendResult)); - } - auto responseIqElement = std::get<QDomElement>(sendResult); + auto encryptedIq = std::get<std::unique_ptr<QXmppIq>>(std::move(encryptResult)); + auto sendResult = co_await stream->sendIq(std::move(*encryptedIq)).withContext(q); - // iq sent, response received - if (!isIqResponse(responseIqElement)) { - co_return QXmppError { u"Invalid IQ response received."_s, QXmpp::SendError::EncryptionError }; - } - if (!d->encryptionExtension) { - co_return QXmppError { u"No decryption extension found."_s, QXmpp::SendError::EncryptionError }; - } + if (std::holds_alternative<QXmppError>(sendResult)) { + co_return std::get<QXmppError>(std::move(sendResult)); + } + auto responseIqElement = std::get<QDomElement>(sendResult); - // try to decrypt the result (should be encrypted) - auto decryptResult = co_await d->encryptionExtension->decryptIq(responseIqElement); + // iq sent, response received + if (!isIqResponse(responseIqElement)) { + co_return QXmppError { u"Invalid IQ response received."_s, QXmpp::SendError::EncryptionError }; + } + if (!encryptionExtension) { + co_return QXmppError { u"No decryption extension found."_s, QXmpp::SendError::EncryptionError }; + } - co_return map<IqResult>( - [&](QXmppE2eeExtension::NotEncrypted) -> IqResult { - // the IQ response from the other entity was not encrypted - // then report IQ response without modifications - // TODO: should we return an QXmppError instead? - return responseIqElement; - }, - std::move(decryptResult)); - }; + // try to decrypt the result (should be encrypted) + auto decryptResult = co_await encryptionExtension->decryptIq(responseIqElement); - return d->encryptionExtension ? sendEncryptedIq(std::move(iq)) : d->stream->sendIq(std::move(iq)); + co_return map<QXmppClient::IqResult>( + [&](QXmppE2eeExtension::NotEncrypted) -> QXmppClient::IqResult { + // the IQ response from the other entity was not encrypted + // then report IQ response without modifications + // TODO: should we return an QXmppError instead? + return responseIqElement; + }, + std::move(decryptResult)); } /// diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppClient_p.h new/qxmpp-1.15.1/src/client/QXmppClient_p.h --- old/qxmpp-1.15.0/src/client/QXmppClient_p.h 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/src/client/QXmppClient_p.h 2026-04-10 16:18:25.000000000 +0200 @@ -19,14 +19,15 @@ #ifndef QXMPPCLIENT_P_H #define QXMPPCLIENT_P_H +#include "QXmppE2eeExtension.h" #include "QXmppOutgoingClient.h" #include "QXmppPresence.h" +#include "QXmppSendResult.h" #include <chrono> class QXmppClient; class QXmppClientExtension; -class QXmppE2eeExtension; class QXmppLogger; class QTimer; @@ -37,6 +38,10 @@ void resendPresence(); + QXmppTask<QXmpp::SendResult> sendEncryptedMessage(QXmppTask<QXmppE2eeExtension::MessageEncryptResult> task); + QXmppTask<QXmpp::SendResult> sendEncryptedIq(QXmppTask<QXmppE2eeExtension::IqEncryptResult> task); + QXmppTask<QXmppClient::IqResult> sendSensitiveIq(QXmppIq iq, std::optional<QXmppSendStanzaParams> params); + /// Current presence of the client QXmppPresence clientPresence; QList<QXmppClientExtension *> extensions; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppOutgoingClient.cpp new/qxmpp-1.15.1/src/client/QXmppOutgoingClient.cpp --- old/qxmpp-1.15.0/src/client/QXmppOutgoingClient.cpp 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/src/client/QXmppOutgoingClient.cpp 2026-04-10 16:18:25.000000000 +0200 @@ -685,7 +685,11 @@ // if we receive any kind of data, stop the timeout timer d->pingManager.onDataReceived(); - auto index = d->listener.index(); + // Remember the listener generation so we can tell whether a synchronous task continuation + // inside handleElement() installed a replacement listener. std::variant::index() is not + // sufficient here because the replacement may be of the same type (e.g. Sasl2Manager retrying + // with password auth after a FAST token rejection, which re-runs setListener<Sasl2Manager>). + const uint generation = d->listenerGeneration; switch (visit(overloaded { [&](auto *manager) { return manager->handleElement(nodeRecv); }, @@ -706,8 +710,9 @@ return; } case Finished: - // if the job is done, set OutgoingClient, but do not override a continuation job - if (d->listener.index() == index) { + // if the job is done, fall back to OutgoingClient — but not if a continuation already + // installed a new listener during the handleElement() call above. + if (d->listenerGeneration == generation) { d->listener = this; } return; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/src/client/QXmppOutgoingClient_p.h new/qxmpp-1.15.1/src/client/QXmppOutgoingClient_p.h --- old/qxmpp-1.15.0/src/client/QXmppOutgoingClient_p.h 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/src/client/QXmppOutgoingClient_p.h 2026-04-10 16:18:25.000000000 +0200 @@ -206,6 +206,8 @@ std::optional<Bind2Bound> bind2Bound; std::variant<QXmppOutgoingClient *, StarttlsManager, NonSaslAuthManager, SaslManager, Sasl2Manager, C2sStreamManager *, BindManager> listener; + // Incremented every time setListener() installs a new listener to detect a replacements. + uint listenerGeneration = 0; FastTokenManager fastTokenManager; C2sStreamManager c2sStreamManager; CarbonManager carbonManager; @@ -216,6 +218,7 @@ T &setListener(Args... args) { listener = T { args... }; + ++listenerGeneration; return std::get<T>(listener); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/tests/TestClient.h new/qxmpp-1.15.1/tests/TestClient.h --- old/qxmpp-1.15.0/tests/TestClient.h 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/tests/TestClient.h 2026-04-10 16:18:25.000000000 +0200 @@ -9,6 +9,7 @@ #include "QXmppClientExtension.h" // needed for qDeleteAll(d->extensions) #include "QXmppClient_p.h" #include "QXmppOutgoingClient.h" +#include "QXmppSasl_p.h" #include "util.h" @@ -38,6 +39,17 @@ QXmppOutgoingClient *stream() const { return d->stream; } QXmppOutgoingClientPrivate *streamPrivate() const { return d->stream->d.get(); } + // Test wrappers for the private outgoing-client entry points exercised by the SASL2 + FAST + // listener-replacement regression test. + void startSasl2Auth(const QXmpp::Private::Sasl2::StreamFeature &feature) + { + d->stream->startSasl2Auth(feature); + } + void handlePacketReceived(const QDomElement &el) + { + d->stream->handlePacketReceived(el); + } + template<typename String> void inject(const String &xml) { inject(xmlToDom(xml)); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qxmpp-1.15.0/tests/tst_qxmppclient.cpp new/qxmpp-1.15.1/tests/tst_qxmppclient.cpp --- old/qxmpp-1.15.0/tests/tst_qxmppclient.cpp 2026-04-05 02:32:57.000000000 +0200 +++ new/qxmpp-1.15.1/tests/tst_qxmppclient.cpp 2026-04-10 16:18:25.000000000 +0200 @@ -15,6 +15,8 @@ #include "QXmppPromise.h" #include "QXmppRegisterIq.h" #include "QXmppRosterManager.h" +#include "QXmppSasl2UserAgent.h" +#include "QXmppSaslManager_p.h" #include "QXmppSasl_p.h" #include "QXmppStreamFeatures.h" #include "QXmppVCardManager.h" @@ -38,6 +40,8 @@ Q_SLOT void testE2eeExtension(); Q_SLOT void testTaskDirect(); Q_SLOT void testTaskStore(); + Q_SLOT void testTaskOptionalNullopt(); + Q_SLOT void testTaskThenChainSuspended(); Q_SLOT void testChainIq(); Q_SLOT void colorGeneration(); #if QT_GUI_LIB @@ -46,6 +50,7 @@ // outgoing client Q_SLOT void csiManager(); + Q_SLOT void sasl2FastFallbackKeepsListener(); Q_SLOT void credentialsSerialization(); }; @@ -234,6 +239,48 @@ QVERIFY(!p.task().hasResult()); } +void tst_QXmppClient::testTaskOptionalNullopt() +{ + // Regression test: finishing a promise whose T is std::optional<X> + // with std::nullopt must produce an engaged result holding an empty + // inner optional, not disengage the internal storage. takeResult() + // previously dereferenced an empty optional and crashed. + QXmppPromise<std::optional<int>> p; + auto task = p.task(); + p.finish(std::nullopt); + + QVERIFY(task.isFinished()); + QVERIFY(task.hasResult()); + QVERIFY(!task.takeResult().has_value()); +} + +void tst_QXmppClient::testTaskThenChainSuspended() +{ + // Regression test: QXmppTask::then() with a non-void-returning continuation + // must co_return the continuation's value, so the chained task carries it + // through. This also exercises the suspension path: the source task is not + // yet finished when then() is called, so the then() coroutine actually + // suspends and is resumed later by promise.finish(). + QXmppPromise<int> sourcePromise; + auto sourceTask = sourcePromise.task(); + + auto chainedTask = sourceTask.then(this, [](int &&value) -> QString { + return QString::number(value * 2); + }); + + // The source has not been finished yet, so the chained task must still be + // suspended on the inner co_await. + QVERIFY(!chainedTask.isFinished()); + + sourcePromise.finish(21); + + // After finishing the source, then() resumes, runs the continuation and + // co_returns its result into the chained task. + QVERIFY(chainedTask.isFinished()); + QVERIFY(chainedTask.hasResult()); + QCOMPARE(chainedTask.takeResult(), u"42"_s); +} + using DiscoResult = std::variant<QXmppDiscoveryIq, QXmppError>; static QXmppTask<DiscoResult> parseIqResult(QXmppTask<QXmppClient::IqResult> &&sendTask, QObject *context) @@ -320,6 +367,93 @@ client.expectNoPacket(); } +// Regression test for the SASL2 + FAST listener-replacement bug. +// +// When a stored FAST token (XEP-0484) is rejected by the server, QXmppOutgoingClient retries +// SASL2 authentication with a password-based mechanism. The retry happens from inside the +// failed task's .then() continuation, which calls startSasl2Auth() recursively, which calls +// setListener<Sasl2Manager>() — installing a NEW Sasl2Manager into d->listener while the OLD +// Sasl2Manager's handleElement() call is still on the stack. +// +// Before the fix, handlePacketReceived() compared d->listener.index() before/after the call to +// decide whether to fall back to OutgoingClient as the active listener. Both old and new +// listeners were Sasl2Manager — same variant index — so the check failed to notice the +// replacement and overwrote the new Sasl2Manager with OutgoingClient. The next stanza (the +// SCRAM challenge) then landed on the wrong handler and produced +// "Unexpected element received while handling client session." A monotonic listener generation +// counter, captured before the call and re-checked after, fixes this. +void tst_QXmppClient::sasl2FastFallbackKeepsListener() +{ + TestClient client; + auto &config = client.stream()->configuration(); + config.setUser(u"bowman"_s); + config.setPassword(u"1234"_s); + config.setDomain(u"example.org"_s); + config.setDisabledSaslMechanisms({}); + config.setSasl2UserAgent(QXmppSasl2UserAgent { + QUuid::fromString(u"d4565fa7-4d72-4749-b3d3-740edbf87770"_s), + u"QXmpp"_s, + u"HAL 9000"_s, + }); + + // Pre-populate a (stale) FAST token, as if from a previous session. + config.credentialData().htToken = HtToken { + SaslHtMechanism { IanaHashAlgorithm::Sha3_512, SaslHtMechanism::None }, + u"old-invalid-token"_s, + QDateTime::fromString(u"2024-07-11T14:00:00Z"_s, Qt::ISODate), + }; + + Sasl2::StreamFeature sasl2Feature { + { u"PLAIN"_s }, + {}, + FastFeature { { u"HT-SHA3-512-NONE"_s }, false }, + false, + }; + + // Kick off SASL2 auth. The first attempt picks the FAST HT mechanism. + client.startSasl2Auth(sasl2Feature); + + QVERIFY(std::holds_alternative<Sasl2Manager>(client.streamPrivate()->listener)); + auto firstAuth = client.takePacket(); + QVERIFY(firstAuth.contains(u"mechanism=\"HT-SHA3-512-NONE\""_s)); + QVERIFY(firstAuth.contains(u"<fast xmlns=\"urn:xmpp:fast:0\"/>"_s)); + + // Server rejects the token. This synchronously runs the failed task's .then() continuation, + // which retries by calling startSasl2Auth() → setListener<Sasl2Manager>(). With the fix in + // place, handlePacketReceived() must NOT overwrite the new Sasl2Manager with OutgoingClient. + client.handlePacketReceived(xmlToDom( + "<failure xmlns='urn:xmpp:sasl:2'>" + "<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>" + "</failure>")); + + // Critical assertion: the listener is still a Sasl2Manager (the second one). Without the + // generation-counter fix this would be QXmppOutgoingClient* and the next stanza would be + // rejected as "Unexpected element received while handling client session." + QVERIFY(std::holds_alternative<Sasl2Manager>(client.streamPrivate()->listener)); + + // The retry should have sent a second <authenticate>, this time with PLAIN, no <fast/>, + // and a fresh FAST token request. + auto secondAuth = client.takePacket(); + QVERIFY(secondAuth.contains(u"mechanism=\"PLAIN\""_s)); + QVERIFY(!secondAuth.contains(u"<fast xmlns=\"urn:xmpp:fast:0\"/>"_s)); + QVERIFY(secondAuth.contains(u"<request-token xmlns=\"urn:xmpp:fast:0\" mechanism=\"HT-SHA3-512-NONE\"/>"_s)); + // Stale token must still be present — server may have been temporarily misconfigured. + QVERIFY(config.credentialData().htToken.has_value()); + QCOMPARE(config.credentialData().htToken->secret, u"old-invalid-token"_s); + + // Server now accepts the password attempt and provides a fresh token. The same Sasl2Manager + // instance handles this success element. + client.handlePacketReceived(xmlToDom( + "<success xmlns='urn:xmpp:sasl:2'>" + "<authorization-identifier>[email protected]</authorization-identifier>" + "<token xmlns='urn:xmpp:fast:0' token='new-valid-token' expiry='2024-08-01T14:00:00Z'/>" + "</success>")); + + QVERIFY(client.streamPrivate()->isAuthenticated); + QVERIFY(config.credentialData().htToken.has_value()); + QCOMPARE(config.credentialData().htToken->secret, u"new-valid-token"_s); +} + void tst_QXmppClient::credentialsSerialization() { QByteArray xml =
