test(media): cover malformed local announcements and SQLite failures
This commit is contained in:
1 parent
81fc4291db
commit
eea2b22c4e
3 files changed
+184
-6
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user