test(media): cover malformed local announcements and SQLite failures

This commit is contained in:
evgeny committed 2026-10-24 17:26:11 +02:00
1 parent 81fc4291db
commit eea2b22c4e
3 files changed
+184 -6

No files matched your search

+6 -5
View File
@@ -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;
+4
View File
@@ -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
+174 -1
View File
@@ -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;
}