From eea2b22c4e17208c84654a2b56068b80930ae8df Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 15:58:22 +0200 Subject: [PATCH] test(media): cover malformed local announcements and SQLite failures --- src/media_delivery/media_delivery.c | 11 +- tests/Makefile.am | 4 + tests/test_media_delivery_sql.c | 175 +++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c index 3b195500..5d3162b6 100644 --- a/src/media_delivery/media_delivery.c +++ b/src/media_delivery/media_delivery.c @@ -1617,19 +1617,20 @@ void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst) { media_delivery_refresh_roles(inst); sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(db, "SELECT id,media_id,block_id,chunk,chat_id FROM media_files WHERE node_id=?", - -1, &st, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: announce_local_blocks prepare failed: %s", MD_ID, sqlite3_errmsg(db)); + int rc = sqlite3_prepare_v2(db, "SELECT id,media_id,block_id,chunk,chat_id FROM media_files WHERE node_id=?", -1, &st, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "%s: announce_local_blocks prepare failed rc=%d: %s", MD_ID, rc, sqlite3_errmsg(db)); return; } - int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); + rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); uint64_t rows = 0, eligible = 0, skipped = 0; if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { rows++; - sqlite3_int64 row = sqlite3_column_int64(st, 0), chunk = sqlite3_column_int64(st, 3); + sqlite3_int64 row = sqlite3_column_int64(st, 0); int media_type = sqlite3_column_type(st, 1), block_type = sqlite3_column_type(st, 2); int chunk_type = sqlite3_column_type(st, 3), chat_type = sqlite3_column_type(st, 4); + sqlite3_int64 chunk = sqlite3_column_int64(st, 3); int media_bytes = sqlite3_column_bytes(st, 1), block_bytes = sqlite3_column_bytes(st, 2); int chat_bytes = sqlite3_column_bytes(st, 4); const uint8_t* media = media_type == SQLITE_BLOB && media_bytes == 16 ? sqlite3_column_blob(st, 1) : NULL; diff --git a/tests/Makefile.am b/tests/Makefile.am index 4fbdb7bc..e98e11a0 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -929,6 +929,10 @@ test_media_delivery_sql_SOURCES = test_media_delivery_sql.c media_delivery_fixtu test_media_delivery_sql_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/lib test_media_delivery_sql_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_media_delivery_sql_LDFLAGS = -Wl,--wrap=etcp_route_send -Wl,--wrap=RAND_bytes +if OS_LINUX +test_media_delivery_sql_LDFLAGS += -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_int64 \ + -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_finalize +endif test_media_delivery_download_SOURCES = test_media_delivery_download.c test_media_delivery_download_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/src/media_async -I$(top_srcdir)/lib -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/chat diff --git a/tests/test_media_delivery_sql.c b/tests/test_media_delivery_sql.c index 741054e1..764697d0 100644 --- a/tests/test_media_delivery_sql.c +++ b/tests/test_media_delivery_sql.c @@ -1,5 +1,46 @@ /* Проверяем настоящую схему media_delivery: restart, сброс старого кеша и сохранность media_files. */ #include "media_delivery_fixture.h" +#include "../src/routing_layer/topo_group.h" + +#ifdef __linux__ +enum sql_fault { SQL_NONE, SQL_PREPARE, SQL_BIND, SQL_STEP_FIRST, SQL_STEP_AFTER, SQL_FINALIZE }; +static enum sql_fault sql_fault; +static sqlite3_stmt* fault_statement; +static unsigned fault_steps, fault_injected; +int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**); +int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); +int __real_sqlite3_step(sqlite3_stmt*); +int __real_sqlite3_finalize(sqlite3_stmt*); + +/* Ошибки внедряются только в внешний обход; вложенные SQL анонса остаются настоящими. */ +int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* command, int size, sqlite3_stmt** st, const char** tail) { + int target = sql_fault && !strncmp(command, "SELECT id,media_id,block_id,chunk,chat_id FROM media_files", 54); + if (target && sql_fault == SQL_PREPARE) { *st = NULL; fault_injected++; return SQLITE_NOMEM; } + int rc = __real_sqlite3_prepare_v2(db, command, size, st, tail); + if (target && rc == SQLITE_OK) fault_statement = *st; + return rc; +} +int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int column, sqlite3_int64 value) { + if (st == fault_statement && sql_fault == SQL_BIND) { fault_injected++; return SQLITE_NOMEM; } + return __real_sqlite3_bind_int64(st, column, value); +} +int __wrap_sqlite3_step(sqlite3_stmt* st) { + if (st == fault_statement) { + unsigned step = fault_steps++; + if (sql_fault == SQL_STEP_FIRST || (sql_fault == SQL_STEP_AFTER && step == 1)) { + fault_injected++; return SQLITE_IOERR; + } + } + return __real_sqlite3_step(st); +} +int __wrap_sqlite3_finalize(sqlite3_stmt* st) { + int target = st && st == fault_statement; + if (target) fault_statement = NULL; + int rc = __real_sqlite3_finalize(st); + if (target && sql_fault == SQL_FINALIZE) { fault_injected++; return SQLITE_IOERR; } + return rc; +} +#endif static void sql(sqlite3* db, const char* command) { REQUIRE(sqlite3_exec(db, command, NULL, NULL, NULL) == SQLITE_OK); @@ -11,6 +52,138 @@ static sqlite3_int64 scalar(sqlite3* db, const char* command) { sqlite3_int64 value = sqlite3_column_int64(st, 0); REQUIRE(sqlite3_finalize(st) == SQLITE_OK); return value; } +#define ANN_MEDIA "x'0102030405060708090a0b0c0d0e0f10'" +#define ANN_BLOCK "x'2122232425262728292a2b2c2d2e2f30'" +#define ANN_BLOCK_SECOND "x'2122232425262728292a2b2c2d2e2f31'" + +/* Данные нарочно добавляются в обход media_index writers: проверяем повреждённую, но допустимую схемой БД. */ +static void insert_announcement(sqlite3* db, const char* media, const char* block, const char* chunk, const char* channel) { + char command[1024]; + int n = snprintf(command, sizeof(command), "INSERT INTO media_files" + "(media_id,block_id,content_hash,chat_id,location,node_id,node_sign,timestamp,file_size,chunk_size,chunk,offset)" + " VALUES(%s,%s,zeroblob(32),%s,'announcement.bin',256,zeroblob(64),1,1,1,%s,0)", + media ? media : ANN_MEDIA, block ? block : ANN_BLOCK, channel ? channel : "'111'", chunk ? chunk : "0"); + REQUIRE(n > 0 && (size_t)n < sizeof(command)); sql(db, command); +} + +/* Только удалённый CLIENT имеет роль supernode: refresh не открывает CM и не маскирует чтение плохих строк. */ +static void add_announcement_group(struct UTUN_INSTANCE* inst, uint64_t id) { + if (!inst->topo_groups) { + inst->topo_groups = u_calloc(1, sizeof(*inst->topo_groups)); REQUIRE(inst->topo_groups); + inst->topo_groups->instance = inst; + inst->topo_groups->group_list = queue_new(ua, 16, 0, 8, "announcement_groups"); REQUIRE(inst->topo_groups->group_list); + } + struct ll_entry* e = queue_entry_new(sizeof(struct TOPO_GROUP) - sizeof(struct ll_entry)); REQUIRE(e); + struct TOPO_GROUP* g = (struct TOPO_GROUP*)e; + g->instance = inst; g->group_id = id; g->group_type = TOPO_GROUP_TYPE_CHAT; + snprintf(g->channel_id, sizeof(g->channel_id), "%llu", (unsigned long long)id); + queue_data_put_with_index(inst->topo_groups->group_list, e); + char command[512]; + snprintf(command, sizeof(command), "CREATE TABLE peers_%s(node_id INTEGER PRIMARY KEY,adm_tags TEXT);" + "INSERT INTO peers_%s VALUES(256,'{}'),(512,'{\"supernode\":\"yes\"}')", g->channel_id, g->channel_id); + sql(inst->topo_sqlite_db, command); +} + +static void check_announcement(const struct packet* packet, uint64_t group, uint32_t chunk, uint8_t last_block) { + REQUIRE(packet->from == SERVER && packet->to == CLIENT && packet->group == group && packet->len == MEDIA_HAVE_BLOCK_SIZE); + const struct media_pkt_have_block* hb = (const struct media_pkt_have_block*)packet->data; + REQUIRE(hb->subcmd == MEDIA_SUBCMD_HAVE_BLOCK && hb->group_id == group && hb->chunk == chunk && hb->timestamp == 1); + for (unsigned i = 0; i < 16; i++) { + REQUIRE(hb->media_id[i] == i + 1); + REQUIRE(hb->block_id[i] == (i == 15 ? last_block : i + 0x21)); + } +} + +/* Проверяем не только отсутствие crash: каждый HAVE_BLOCK обязан точно соответствовать валидной исходной строке. */ +static void test_announcements(void) { + struct UTUN_INSTANCE* a = instance(SERVER); + add_announcement_group(a, GROUP); add_announcement_group(a, 1); add_announcement_group(a, UINT64_MAX); + const struct { + const char *name, *media, *block, *chunk, *channel; + uint64_t expected_group; + uint32_t expected_chunk; + } cases[] = { + {"valid", NULL, NULL, NULL, NULL, GROUP, 0}, + {"media0", "zeroblob(0)", NULL}, {"media1", "zeroblob(1)", NULL}, + {"media15", "zeroblob(15)", NULL}, {"media17", "zeroblob(17)", NULL}, + {"media_text", "'1234567890123456'", NULL}, {"media_int", "1", NULL}, {"media_float", "1.5", NULL}, + {"block0", NULL, "zeroblob(0)"}, {"block1", NULL, "zeroblob(1)"}, + {"block15", NULL, "zeroblob(15)"}, {"block17", NULL, "zeroblob(17)"}, + {"block_text", NULL, "'1234567890123456'"}, {"block_int", NULL, "1"}, {"block_float", NULL, "1.5"}, + {"chunk_negative", NULL, NULL, "-1"}, {"chunk_overflow", NULL, NULL, "4294967296"}, + {"chunk_text", NULL, NULL, "'invalid'"}, {"chunk_float", NULL, NULL, "1.5"}, {"chunk_blob", NULL, NULL, "x'00'"}, + {"chunk_max", NULL, NULL, "4294967295", NULL, GROUP, UINT32_MAX}, + {"channel_empty", NULL, NULL, NULL, "''"}, {"channel_zero", NULL, NULL, NULL, "'0'"}, + {"channel_leading_zero", NULL, NULL, NULL, "'0111'"}, {"channel_suffix", NULL, NULL, NULL, "'111junk'"}, + {"channel_plus", NULL, NULL, NULL, "'+111'"}, {"channel_negative", NULL, NULL, NULL, "'-111'"}, + {"channel_left_space", NULL, NULL, NULL, "' 111'"}, {"channel_right_space", NULL, NULL, NULL, "'111 '"}, + {"channel_blob", NULL, NULL, NULL, "x'313131'"}, {"channel_float", NULL, NULL, NULL, "1.5"}, + {"channel_nul", NULL, NULL, NULL, "CAST(x'313131006a756e6b' AS TEXT)"}, + {"channel_overflow", NULL, NULL, NULL, "'18446744073709551616'"}, + {"channel_too_long", NULL, NULL, NULL, "'184467440737095516150'"}, + {"channel_one", NULL, NULL, NULL, "'1'", 1, 0}, + {"channel_max", NULL, NULL, NULL, "'18446744073709551615'", UINT64_MAX, 0}, + }; + unsigned valid = 0; + debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_WARN); + for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + sql(a->md.db, "DELETE FROM media_files"); + insert_announcement(a->md.db, cases[i].media, cases[i].block, cases[i].chunk, cases[i].channel); + reset(); capture_blocks = 1; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "announcement case=%s expected_packets=%d", cases[i].name, cases[i].expected_group != 0); + media_delivery_announce_local_blocks(a); + REQUIRE(packet_count == (cases[i].expected_group ? 1 : 0)); + if (cases[i].expected_group) { check_announcement(&packets[0], cases[i].expected_group, cases[i].expected_chunk, 0x30); valid++; } + REQUIRE(sqlite3_next_stmt(a->md.db, NULL) == NULL); + } + sql(a->md.db, "DELETE FROM media_files"); + insert_announcement(a->md.db, NULL, NULL, NULL, NULL); + insert_announcement(a->md.db, "x'01'", "x'ff'", "1", NULL); + insert_announcement(a->md.db, NULL, ANN_BLOCK_SECOND, "4294967295", "'18446744073709551615'"); + reset(); capture_blocks = 1; + media_delivery_announce_local_blocks(a); REQUIRE(packet_count == 2); + for (int i = 0; i < packet_count; i++) { + const struct media_pkt_have_block* hb = (const struct media_pkt_have_block*)packets[i].data; + check_announcement(&packets[i], hb->chunk ? UINT64_MAX : GROUP, hb->chunk ? UINT32_MAX : 0, hb->chunk ? 0x31 : 0x30); + } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "announcement validation cases=%zu valid=%u mixed=2/3 PASS", sizeof(cases) / sizeof(cases[0]), valid); + +#ifdef __linux__ + sql(a->md.db, "DELETE FROM media_files"); + insert_announcement(a->md.db, NULL, NULL, NULL, NULL); + insert_announcement(a->md.db, NULL, ANN_BLOCK_SECOND, "1", NULL); + for (enum sql_fault fault = SQL_PREPARE; fault <= SQL_FINALIZE; fault++) { + const char* logfile = "test_media_announcement_fault.log"; + reset(); capture_blocks = 1; sql_fault = fault; fault_steps = fault_injected = 0; + debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_INFO); + debug_enable_file_output(logfile, 1); + media_delivery_announce_local_blocks(a); + debug_disable_file_output(); + REQUIRE(fault_injected == 1 && fault_statement == NULL); + int expected = fault == SQL_FINALIZE ? 2 : fault == SQL_STEP_AFTER ? 1 : 0; + REQUIRE(packet_count == expected && sqlite3_next_stmt(a->md.db, NULL) == NULL); + FILE* file = fopen(logfile, "rb"); REQUIRE(file); + char log[8192]; size_t n = fread(log, 1, sizeof(log) - 1, file); log[n] = 0; + REQUIRE(!ferror(file) && fgetc(file) == EOF && !fclose(file) && !remove(logfile)); + REQUIRE(strstr(log, "[ERROR]") && !strstr(log, "local block scan complete")); + REQUIRE(strstr(log, fault == SQL_PREPARE ? "announce_local_blocks prepare failed rc=" : "SQL phase=enumerate local blocks")); + if (fault != SQL_PREPARE) REQUIRE(strstr(log, "local block scan incomplete")); + sql_fault = SQL_NONE; reset(); capture_blocks = 1; + media_delivery_announce_local_blocks(a); REQUIRE(packet_count == 2 && sqlite3_next_stmt(a->md.db, NULL) == NULL); + check_announcement(&packets[0], GROUP, ((const struct media_pkt_have_block*)packets[0].data)->chunk, packets[0].data[48]); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "announcement SQL fault=%d injected=1 packets_before_error=%d recovery=2 PASS", fault, expected); + } +#endif + struct TOPO_GROUPS* groups = a->topo_groups; + destroy(a); + while (groups->group_list->head) { + struct ll_entry* e = groups->group_list->head; + queue_remove_data(groups->group_list, e); queue_entry_free(e); + } + queue_free(groups->group_list); u_free(groups); + debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_ERROR); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); @@ -54,7 +227,7 @@ int main(void) { REQUIRE(!media_delivery_init(a) && memcmp(epoch, a->md.source_epoch, 16)); REQUIRE(!scalar(a->md.db, "SELECT COUNT(*) FROM super_sync")); REQUIRE(scalar(a->md.db, "SELECT COUNT(*) FROM media_files") == 1); - destroy(a); uasync_destroy(ua, 0); + destroy(a); test_announcements(); uasync_destroy(ua, 0); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media catalog SQL restart/reset/preservation: PASS"); return 0; }