From bd105c630d2b3b8e8cfc2bb640ae2d75b951a0af Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 11:35:30 +0100 Subject: [PATCH] Validate avatar read recovery and bounded typed blobs --- .../src/avatars/groupavatarmanager.cpp | 15 +++-- tools/chatgui/tests/test_channel_settings.cpp | 59 +++++++++++++++++-- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/tools/chatgui/src/avatars/groupavatarmanager.cpp b/tools/chatgui/src/avatars/groupavatarmanager.cpp index 113ba486..1bfe050a 100644 --- a/tools/chatgui/src/avatars/groupavatarmanager.cpp +++ b/tools/chatgui/src/avatars/groupavatarmanager.cpp @@ -48,7 +48,7 @@ QImage GroupAvatarManager::image(const QString& channel, quint64 version, const bool read = false; const QByteArray bytes = m_db->getChannelAvatar(channel, &read); if (!read) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: database snapshot unavailable, cache unchanged channel=%s", qPrintable(channel)); return {}; + DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: database snapshot unavailable, image not cached channel=%s", qPrintable(channel)); return {}; } m_loading.insert(key); m_pool.start(QRunnable::create([this, key, bytes, hash, channel] { @@ -128,11 +128,14 @@ void GroupAvatarManager::upload(const QString& channel, const QString& source) { void GroupAvatarManager::remove(const QString& channel) { publish(channel, {}, ++m_generation[channel]); } void GroupAvatarManager::publish(const QString& channel, const QByteArray& bytes, quint64 generation) { const quint64 group = channel.toULongLong(); - if (!m_db || !m_db->isOpen() || !gui_bridge_is_uasync_ready() || !group || group > quint64(std::numeric_limits::max()) - || QString::number(group) != channel || bytes.size() > GROUP_AVATAR_MAX) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication unavailable channel=%s", qPrintable(channel)); + if (!m_db || !m_db->isOpen() || !gui_bridge_is_uasync_ready()) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires running chat and open DB channel=%s", qPrintable(channel)); emit status(channel, text("Chat is stopped")); return; } + if (!group || group > quint64(std::numeric_limits::max()) || QString::number(group) != channel || bytes.size() > GROUP_AVATAR_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid publication channel_bytes=%d image_bytes=%d", int(channel.toUtf8().size()), int(bytes.size())); + emit status(channel, text("Cannot update group avatar")); return; + } auto* q = (group_avatar_gui_request*)u_calloc(1, sizeof(group_avatar_gui_request)); if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication allocation failed"); emit status(channel, text("Cannot update group avatar")); return; } q->inst = gui_bridge_get_inst(); q->group = group; q->size = bytes.size(); @@ -168,7 +171,7 @@ void GroupAvatarManager::onResult(const uint8_t* bytes, int length) { if (m_generation.value(request.first) != request.second) { DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: ignoring stale publication result request=%llu", (unsigned long long)id); return; } - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: publication completed request=%llu channel=%s result=%u", (unsigned long long)id, - qPrintable(request.first), bytes[16]); + if (bytes[16]) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication failed request=%llu channel=%s", (unsigned long long)id, qPrintable(request.first)); + else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: publication completed request=%llu channel=%s", (unsigned long long)id, qPrintable(request.first)); emit status(request.first, bytes[16] ? text("Cannot update group avatar") : text("Group avatar updated")); } diff --git a/tools/chatgui/tests/test_channel_settings.cpp b/tools/chatgui/tests/test_channel_settings.cpp index 9964e032..4e6082bf 100644 --- a/tools/chatgui/tests/test_channel_settings.cpp +++ b/tools/chatgui/tests/test_channel_settings.cpp @@ -288,7 +288,13 @@ private slots: QCOMPARE(GroupAvatarManager::instance(), ¤t); } + void avatarReadFailureCanBeRetried_data() { + QTest::addColumn("prepare"); + QTest::newRow("prepare-denied") << true; + QTest::newRow("step-interrupted") << false; + } void avatarReadFailureCanBeRetried() { + QFETCH(bool, prepare); SettingsCore core; QVERIFY(core.start()); QImage image(32, 32, QImage::Format_RGB32); image.fill(Qt::green); QByteArray jpeg; QBuffer output(&jpeg); QVERIFY(output.open(QIODevice::WriteOnly)); QVERIFY(image.save(&output, "JPEG")); @@ -296,18 +302,59 @@ private slots: const auto row = core.db.getChannels()[0]; GroupAvatarManager avatars(&core.db); QSignalSpy changed(&avatars, &GroupAvatarManager::changed); bool denied = false; - QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, - [](void* context, int action, const char*, const char* column, const char*, const char*) { - if (action != SQLITE_READ || !column || strcmp(column, "avatar_data")) return SQLITE_OK; - *static_cast(context) = true; return SQLITE_DENY; - }, &denied), SQLITE_OK); - auto restore = qScopeGuard([&] { sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr); }); + if (prepare) QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, + [](void* context, int action, const char*, const char* column, const char*, const char*) { + if (action != SQLITE_READ || !column || strcmp(column, "avatar_data")) return SQLITE_OK; + *static_cast(context) = true; return SQLITE_DENY; + }, &denied), SQLITE_OK); + else sqlite3_progress_handler(core.inst->topo_sqlite_db, 1, [](void* context) { *static_cast(context) = true; return 1; }, &denied); + auto restore = qScopeGuard([&] { + sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr); sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr); + }); QVERIFY(avatars.image(core.channel, row.avatarVersion, row.avatarHash).isNull()); QVERIFY(denied); QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr), SQLITE_OK); + sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr); avatars.image(core.channel, row.avatarVersion, row.avatarHash); QTRY_VERIFY_WITH_TIMEOUT(!avatars.currentImage(core.channel).isNull(), 1000); QCOMPARE(changed.count(), 1); } + + void avatarBlobTypesAndLimits_data() { + QTest::addColumn("value"); QTest::addColumn("valid"); QTest::addColumn("bytes"); + QTest::newRow("empty-blob") << QByteArray("zeroblob(0)") << true << 0; + QTest::newRow("maximum-blob") << QByteArray("zeroblob(16384)") << true << 16384; + QTest::newRow("oversized-blob") << QByteArray("zeroblob(16385)") << false << 0; + QTest::newRow("null") << QByteArray("NULL") << false << 0; + QTest::newRow("text") << QByteArray("'jpeg'") << false << 0; + QTest::newRow("integer") << QByteArray("1") << false << 0; + } + void avatarBlobTypesAndLimits() { + QFETCH(QByteArray, value); QFETCH(bool, valid); QFETCH(int, bytes); + SettingsCore core; QVERIFY(core.start()); + const QByteArray sql = "UPDATE channels SET avatar_data=" + value; + QCOMPARE(sqlite3_exec(core.inst->topo_sqlite_db, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK); + bool ok = !valid; const QByteArray result = core.db.getChannelAvatar(core.channel, &ok); + QCOMPARE(ok, valid); QCOMPARE(result.size(), bytes); + QVERIFY(core.db.getChannelAvatar("missing", &ok).isEmpty()); QVERIFY(!ok); + } + +#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES + void avatarAllocationFailure_data() { + QTest::addColumn("source"); + QTest::newRow("request") << QByteArray("groupavatarmanager.cpp"); + QTest::newRow("bridge") << QByteArray("gui_bridge_impl.cpp"); + } + void avatarAllocationFailure() { + QFETCH(QByteArray, source); + SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); + QSignalSpy statuses(&avatars, &GroupAvatarManager::status); const auto baseline = u_get_allocated_count(); + allocationFile = source.constData(); failAllocation = 1; + auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); + avatars.remove(core.channel); QCOMPARE(failAllocation, 0); + QCOMPARE(statuses.count(), 1); QCOMPARE(statuses[0][1].toString(), QString("Cannot update group avatar")); + gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); + } +#endif }; QTEST_MAIN(ChannelSettingsTest)