From 5215163a37c26d01f839b8a9fbbece03d076db31 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 22 Oct 2026 19:25:24 +0200 Subject: [PATCH] Share authenticated media fixtures for adversarial delivery regressions --- tests/media_delivery_fixture.h | 184 +++++++++++++++++++++++++++++++ tests/test_media_delivery_full.c | 1 + tests/test_media_query.c | 178 +----------------------------- 3 files changed, 186 insertions(+), 177 deletions(-) create mode 100644 tests/media_delivery_fixture.h diff --git a/tests/media_delivery_fixture.h b/tests/media_delivery_fixture.h new file mode 100644 index 00000000..5c5c538a --- /dev/null +++ b/tests/media_delivery_fixture.h @@ -0,0 +1,184 @@ +/* Общий стенд media: настоящие SQLite/router callbacks и Ed25519, управляемая доставка пакетов. */ +#ifndef TEST_MEDIA_DELIVERY_FIXTURE_H +#define TEST_MEDIA_DELIVERY_FIXTURE_H +/* Регрессии QUERY: реальные обработчики сервиса и SQLite, управляемая доставка/ошибки транспорта. + * Стенд сохраняет владение router: пакет потребляется даже при ошибке. Ответы доставляются отдельно, + * поэтому можно проверить задержки, дубликаты, чужие ответы и состояние между пачками. */ +#include "test_utils.h" +#include "../src/utun_instance.h" +#include "../src/chat/chat_core_priv.h" +#include "../src/media_delivery/media_index.h" +#include "../src/media_delivery/media_download.h" +#include "../src/routing_layer/etcp_router.h" +#include "../src/transport_layer/etcp.h" +#include "../src/transport_layer/secure_channel.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/platform_compat.h" +#include +#include +#include +#include +#ifdef __SANITIZE_ADDRESS__ +#include +#endif + +#define GROUP 111 +#define SERVER 0x100 +#define CLIENT 0x200 +#define MAX_WIRE 2048 +#define REQUIRE(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "QUERY test line=%d: %s", __LINE__, #x); exit(1); } } while (0) + +struct packet { uint64_t from, to, group; size_t len; uint8_t data[MAX_WIRE]; }; +static struct packet packets[128]; +static int packet_count, query_count, request_count, done_count, done_err, fail_send, fail_rng; +static struct UASYNC* ua; +static char directory[256]; +static void* injected; +static uint8_t* poison_tail; +static uint8_t author_key[32] = {1}; + +int __real_RAND_bytes(unsigned char* out, int count); +int __wrap_RAND_bytes(unsigned char* out, int count) { + if (fail_rng) return 0; + return __real_RAND_bytes(out, count); +} + +int __wrap_etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t dst, + struct ll_entry* entry, int force, int mode) { + (void)force; (void)mode; + int cmd = entry->dgram[1], rc = cmd == fail_send ? -1 : 0; + if (cmd == MEDIA_SUBCMD_BLOCK_REQ) request_count++; + if (cmd == MEDIA_SUBCMD_QUERY) query_count++; + if (!rc && (cmd == MEDIA_SUBCMD_QUERY || cmd == MEDIA_SUBCMD_QUERY_RESP)) { + REQUIRE(packet_count < 128 && entry->len - 1 <= MAX_WIRE); + struct packet* p = &packets[packet_count++]; + p->from = inst->node_id; p->to = dst; p->group = group; p->len = entry->len - 1; + memcpy(p->data, entry->dgram + 1, p->len); + } + queue_dgram_free(entry); queue_entry_free(entry); return rc; +} + +/* u_malloc округляет выделение: ASAN защищает именно логическую границу входного payload. */ +static void release_injected(uint8_t* data) { +#ifdef __SANITIZE_ADDRESS__ + if (data == injected) __asan_unpoison_memory_region(poison_tail, 64); +#endif + injected = NULL; poison_tail = NULL; u_free(data); +} + +static void receive(struct UTUN_INSTANCE* inst, const struct packet* p) { + struct ll_entry* entry = queue_entry_new(0); REQUIRE(entry); + size_t bytes = ROUTER_SVC_PAYLOAD_OFF + p->len; + entry->dgram = u_calloc(1, bytes + 64); REQUIRE(entry->dgram); + entry->len = (uint16_t)bytes; entry->dgram_free_fn = release_injected; + entry->dgram[0] = ETCP_RT_ID_MEDIA_DELIVERY; + memcpy(entry->dgram + ROUTER_SVC_SRC_OFF, &p->from, 8); + memcpy(entry->dgram + ROUTER_SVC_GROUP_OFF, &p->group, 8); + memcpy(entry->dgram + ROUTER_SVC_PAYLOAD_OFF, p->data, p->len); + injected = entry->dgram; poison_tail = entry->dgram + bytes; +#ifdef __SANITIZE_ADDRESS__ + __asan_poison_memory_region(poison_tail, 64); +#endif + struct ETCP_CONN conn = {.instance = inst}; + inst->router_bindings.callbacks[ETCP_RT_ID_MEDIA_DELIVERY](&conn, entry); + REQUIRE(!injected); +} + +static struct packet take_packet(void) { + REQUIRE(packet_count > 0); + struct packet result = packets[0]; + memmove(packets, packets + 1, (size_t)--packet_count * sizeof(*packets)); + return result; +} + +static struct UTUN_INSTANCE* instance(uint64_t node) { + struct UTUN_INSTANCE* inst = u_calloc(1, sizeof(*inst)); REQUIRE(inst); + inst->ua = ua; inst->node_id = node; + inst->chat_core = u_calloc(1, sizeof(struct chat_core_ctx)); REQUIRE(inst->chat_core); + REQUIRE(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); + REQUIRE(!media_index_init(inst->topo_sqlite_db) && !media_delivery_init(inst)); + uint8_t pubkey[32]; size_t length = sizeof(pubkey); + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, author_key, 32); REQUIRE(key); + REQUIRE(EVP_PKEY_get_raw_public_key(key, pubkey, &length) == 1 && length == 32); EVP_PKEY_free(key); + REQUIRE(sqlite3_exec(inst->topo_sqlite_db, "CREATE TABLE nodes(node_id INTEGER PRIMARY KEY,ed25519_pubkey BLOB)", + NULL, NULL, NULL) == SQLITE_OK); + sqlite3_stmt* st = NULL; + REQUIRE(sqlite3_prepare_v2(inst->topo_sqlite_db, "INSERT INTO nodes VALUES(256,?)", -1, &st, NULL) == SQLITE_OK); + REQUIRE(sqlite3_bind_blob(st, 1, pubkey, 32, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE); + REQUIRE(sqlite3_finalize(st) == SQLITE_OK); + return inst; +} + +static void destroy(struct UTUN_INSTANCE* inst) { + media_delivery_destroy(inst); sqlite3_close(inst->topo_sqlite_db); + u_free(inst->chat_core); u_free(inst); uasync_poll(ua, 0); +} + +static void completed(void* arg, int err) { (void)arg; done_count++; done_err = err; } + +static struct media_index_result result(int count) { + struct media_index_result r = {.file_size = count, .block_size = 1, .num_blocks = count}; + memset(r.media_id, 0xAA, 16); + r.block_ids = u_calloc(count, 16); r.block_sigs = u_calloc(count, 64); REQUIRE(r.block_ids && r.block_sigs); + EVP_MD_CTX* hash = EVP_MD_CTX_new(); REQUIRE(hash && EVP_DigestInit_ex(hash, EVP_sha256(), NULL) == 1); + for (int i = 0; i < count; i++) { + uint32_t id = (uint32_t)i + 1; uint8_t byte = (uint8_t)i; memcpy(r.block_ids + i * 16, &id, 4); + REQUIRE(sc_ed25519_sign(author_key, &byte, 1, r.block_sigs + i * 64) == SC_OK); + REQUIRE(EVP_DigestUpdate(hash, &byte, 1) == 1); + } + unsigned length; REQUIRE(EVP_DigestFinal_ex(hash, r.content_hash, &length) == 1 && length == 32); EVP_MD_CTX_free(hash); + return r; +} + +static struct media_download* download(struct UTUN_INSTANCE* inst) { + return inst->md.downloads && inst->md.downloads->head ? (struct media_download*)inst->md.downloads->head->data : NULL; +} + +static void start(struct UTUN_INSTANCE* inst, struct media_index_result* r) { + char path[512]; snprintf(path, sizeof(path), "%s/result.bin", directory); + REQUIRE(!media_download_start(inst, GROUP, r, path, directory, SERVER, completed, NULL, NULL, NULL)); +} + +static void seed(struct UTUN_INSTANCE* inst, const struct media_index_result* r, int count, int holders, int distinct) { + sqlite3_stmt* st = NULL; + REQUIRE(sqlite3_prepare_v2(inst->topo_sqlite_db, + "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp,status) VALUES(?,111,?,?,1,1)", -1, &st, NULL) == SQLITE_OK); + for (int i = 0; i < count; i++) for (int h = 0; h < holders; h++) { + REQUIRE(sqlite3_bind_blob(st, 1, r->block_ids + i * 16, 16, SQLITE_STATIC) == SQLITE_OK); + REQUIRE(sqlite3_bind_int64(st, 2, 1000 + (distinct ? i % distinct : h)) == SQLITE_OK); + REQUIRE(sqlite3_bind_int(st, 3, i) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE); + REQUIRE(sqlite3_reset(st) == SQLITE_OK); + } + REQUIRE(sqlite3_finalize(st) == SQLITE_OK); +} + +static void reset(void) { + packet_count = query_count = request_count = done_count = done_err = fail_send = fail_rng = 0; +} + +static struct packet answer(struct UTUN_INSTANCE* server) { + struct packet query = take_packet(); REQUIRE(query.data[0] == MEDIA_SUBCMD_QUERY); + receive(server, &query); + struct packet response = take_packet(); REQUIRE(response.data[0] == MEDIA_SUBCMD_QUERY_RESP); + return response; +} + +static void deliver_block(struct UTUN_INSTANCE* c, struct media_download* dl, int bi) { + struct media_download_block* b = &dl->blocks[bi]; + struct packet p = {.from = b->holders[b->holder_idx], .to = CLIENT, .group = GROUP, .len = MEDIA_BLOCK_CHUNK_HDR_SIZE + 1}; + struct media_pkt_block_chunk* chunk = (struct media_pkt_block_chunk*)p.data; + chunk->subcmd = MEDIA_SUBCMD_BLOCK_CHUNK; chunk->chunk = bi; chunk->data_len = 1; + memcpy(chunk->media_id, dl->media_id, 16); memcpy(chunk->block_id, dl->blocks[bi].block_id, 16); + memcpy(chunk->transfer_id, b->transfer_id, 16); + p.data[MEDIA_BLOCK_CHUNK_HDR_SIZE] = (uint8_t)bi; receive(c, &p); + memset(p.data, 0, sizeof(p.data)); p.len = MEDIA_BLOCK_DONE_SIZE; + struct media_pkt_block_done* done = (struct media_pkt_block_done*)p.data; + done->subcmd = MEDIA_SUBCMD_BLOCK_DONE; done->chunk = bi; done->total_size = 1; + memcpy(done->media_id, dl->media_id, 16); memcpy(done->block_id, dl->blocks[bi].block_id, 16); + memcpy(done->transfer_id, b->transfer_id, 16); + receive(c, &p); +} + + +#endif diff --git a/tests/test_media_delivery_full.c b/tests/test_media_delivery_full.c index 2817c1e5..f251aeb7 100644 --- a/tests/test_media_delivery_full.c +++ b/tests/test_media_delivery_full.c @@ -373,6 +373,7 @@ static int create_truncated_file(void) { static void phase_b2_truncated_last_block(void) { TEST("truncated last block served from correct offset"); { if (create_truncated_file() != 0) { FAIL("create_truncated_file"); return; } + assert(!media_index_init(g_inst[1]->topo_sqlite_db)); char destination[1024]; snprintf(destination, sizeof(destination), "%s/media/truncated.bin", g_db_dir[1]); g_download_done = g_download_error = 0; diff --git a/tests/test_media_query.c b/tests/test_media_query.c index 82626e6c..6766a96a 100644 --- a/tests/test_media_query.c +++ b/tests/test_media_query.c @@ -1,165 +1,4 @@ -/* Регрессии QUERY: реальные обработчики сервиса и SQLite, управляемая доставка/ошибки транспорта. - * Стенд сохраняет владение router: пакет потребляется даже при ошибке. Ответы доставляются отдельно, - * поэтому можно проверить задержки, дубликаты, чужие ответы и состояние между пачками. */ -#include "test_utils.h" -#include "../src/utun_instance.h" -#include "../src/chat/chat_core_priv.h" -#include "../src/media_delivery/media_index.h" -#include "../src/media_delivery/media_download.h" -#include "../src/routing_layer/etcp_router.h" -#include "../src/transport_layer/etcp.h" -#include "../src/transport_layer/secure_channel.h" -#include "../lib/debug_config.h" -#include "../lib/mem.h" -#include "../lib/platform_compat.h" -#include -#include -#include -#include -#ifdef __SANITIZE_ADDRESS__ -#include -#endif - -#define GROUP 111 -#define SERVER 0x100 -#define CLIENT 0x200 -#define MAX_WIRE 2048 -#define REQUIRE(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "QUERY test line=%d: %s", __LINE__, #x); exit(1); } } while (0) - -struct packet { uint64_t from, to, group; size_t len; uint8_t data[MAX_WIRE]; }; -static struct packet packets[128]; -static int packet_count, query_count, request_count, done_count, done_err, fail_send, fail_rng; -static struct UASYNC* ua; -static char directory[256]; -static void* injected; -static uint8_t* poison_tail; -static uint8_t author_key[32] = {1}; - -int __real_RAND_bytes(unsigned char* out, int count); -int __wrap_RAND_bytes(unsigned char* out, int count) { - if (fail_rng) return 0; - return __real_RAND_bytes(out, count); -} - -int __wrap_etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t dst, - struct ll_entry* entry, int force, int mode) { - (void)force; (void)mode; - int cmd = entry->dgram[1], rc = cmd == fail_send ? -1 : 0; - if (cmd == MEDIA_SUBCMD_BLOCK_REQ) request_count++; - if (cmd == MEDIA_SUBCMD_QUERY) query_count++; - if (!rc && (cmd == MEDIA_SUBCMD_QUERY || cmd == MEDIA_SUBCMD_QUERY_RESP)) { - REQUIRE(packet_count < 128 && entry->len - 1 <= MAX_WIRE); - struct packet* p = &packets[packet_count++]; - p->from = inst->node_id; p->to = dst; p->group = group; p->len = entry->len - 1; - memcpy(p->data, entry->dgram + 1, p->len); - } - queue_dgram_free(entry); queue_entry_free(entry); return rc; -} - -/* u_malloc округляет выделение: ASAN защищает именно логическую границу входного payload. */ -static void release_injected(uint8_t* data) { -#ifdef __SANITIZE_ADDRESS__ - if (data == injected) __asan_unpoison_memory_region(poison_tail, 64); -#endif - injected = NULL; poison_tail = NULL; u_free(data); -} - -static void receive(struct UTUN_INSTANCE* inst, const struct packet* p) { - struct ll_entry* entry = queue_entry_new(0); REQUIRE(entry); - size_t bytes = ROUTER_SVC_PAYLOAD_OFF + p->len; - entry->dgram = u_calloc(1, bytes + 64); REQUIRE(entry->dgram); - entry->len = (uint16_t)bytes; entry->dgram_free_fn = release_injected; - entry->dgram[0] = ETCP_RT_ID_MEDIA_DELIVERY; - memcpy(entry->dgram + ROUTER_SVC_SRC_OFF, &p->from, 8); - memcpy(entry->dgram + ROUTER_SVC_GROUP_OFF, &p->group, 8); - memcpy(entry->dgram + ROUTER_SVC_PAYLOAD_OFF, p->data, p->len); - injected = entry->dgram; poison_tail = entry->dgram + bytes; -#ifdef __SANITIZE_ADDRESS__ - __asan_poison_memory_region(poison_tail, 64); -#endif - struct ETCP_CONN conn = {.instance = inst}; - inst->router_bindings.callbacks[ETCP_RT_ID_MEDIA_DELIVERY](&conn, entry); - REQUIRE(!injected); -} - -static struct packet take_packet(void) { - REQUIRE(packet_count > 0); - struct packet result = packets[0]; - memmove(packets, packets + 1, (size_t)--packet_count * sizeof(*packets)); - return result; -} - -static struct UTUN_INSTANCE* instance(uint64_t node) { - struct UTUN_INSTANCE* inst = u_calloc(1, sizeof(*inst)); REQUIRE(inst); - inst->ua = ua; inst->node_id = node; - inst->chat_core = u_calloc(1, sizeof(struct chat_core_ctx)); REQUIRE(inst->chat_core); - REQUIRE(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); - REQUIRE(!media_index_init(inst->topo_sqlite_db) && !media_delivery_init(inst)); - uint8_t pubkey[32]; size_t length = sizeof(pubkey); - EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, author_key, 32); REQUIRE(key); - REQUIRE(EVP_PKEY_get_raw_public_key(key, pubkey, &length) == 1 && length == 32); EVP_PKEY_free(key); - REQUIRE(sqlite3_exec(inst->topo_sqlite_db, "CREATE TABLE nodes(node_id INTEGER PRIMARY KEY,ed25519_pubkey BLOB)", - NULL, NULL, NULL) == SQLITE_OK); - sqlite3_stmt* st = NULL; - REQUIRE(sqlite3_prepare_v2(inst->topo_sqlite_db, "INSERT INTO nodes VALUES(256,?)", -1, &st, NULL) == SQLITE_OK); - REQUIRE(sqlite3_bind_blob(st, 1, pubkey, 32, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE); - REQUIRE(sqlite3_finalize(st) == SQLITE_OK); - return inst; -} - -static void destroy(struct UTUN_INSTANCE* inst) { - media_delivery_destroy(inst); sqlite3_close(inst->topo_sqlite_db); - u_free(inst->chat_core); u_free(inst); uasync_poll(ua, 0); -} - -static void completed(void* arg, int err) { (void)arg; done_count++; done_err = err; } - -static struct media_index_result result(int count) { - struct media_index_result r = {.file_size = count, .block_size = 1, .num_blocks = count}; - memset(r.media_id, 0xAA, 16); - r.block_ids = u_calloc(count, 16); r.block_sigs = u_calloc(count, 64); REQUIRE(r.block_ids && r.block_sigs); - EVP_MD_CTX* hash = EVP_MD_CTX_new(); REQUIRE(hash && EVP_DigestInit_ex(hash, EVP_sha256(), NULL) == 1); - for (int i = 0; i < count; i++) { - uint32_t id = (uint32_t)i + 1; uint8_t byte = (uint8_t)i; memcpy(r.block_ids + i * 16, &id, 4); - REQUIRE(sc_ed25519_sign(author_key, &byte, 1, r.block_sigs + i * 64) == SC_OK); - REQUIRE(EVP_DigestUpdate(hash, &byte, 1) == 1); - } - unsigned length; REQUIRE(EVP_DigestFinal_ex(hash, r.content_hash, &length) == 1 && length == 32); EVP_MD_CTX_free(hash); - return r; -} - -static struct media_download* download(struct UTUN_INSTANCE* inst) { - return inst->md.downloads && inst->md.downloads->head ? (struct media_download*)inst->md.downloads->head->data : NULL; -} - -static void start(struct UTUN_INSTANCE* inst, struct media_index_result* r) { - char path[512]; snprintf(path, sizeof(path), "%s/result.bin", directory); - REQUIRE(!media_download_start(inst, GROUP, r, path, directory, SERVER, completed, NULL, NULL, NULL)); -} - -static void seed(struct UTUN_INSTANCE* inst, const struct media_index_result* r, int count, int holders, int distinct) { - sqlite3_stmt* st = NULL; - REQUIRE(sqlite3_prepare_v2(inst->topo_sqlite_db, - "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp,status) VALUES(?,111,?,?,1,1)", -1, &st, NULL) == SQLITE_OK); - for (int i = 0; i < count; i++) for (int h = 0; h < holders; h++) { - REQUIRE(sqlite3_bind_blob(st, 1, r->block_ids + i * 16, 16, SQLITE_STATIC) == SQLITE_OK); - REQUIRE(sqlite3_bind_int64(st, 2, 1000 + (distinct ? i % distinct : h)) == SQLITE_OK); - REQUIRE(sqlite3_bind_int(st, 3, i) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE); - REQUIRE(sqlite3_reset(st) == SQLITE_OK); - } - REQUIRE(sqlite3_finalize(st) == SQLITE_OK); -} - -static void reset(void) { - packet_count = query_count = request_count = done_count = done_err = fail_send = fail_rng = 0; -} - -static struct packet answer(struct UTUN_INSTANCE* server) { - struct packet query = take_packet(); REQUIRE(query.data[0] == MEDIA_SUBCMD_QUERY); - receive(server, &query); - struct packet response = take_packet(); REQUIRE(response.data[0] == MEDIA_SUBCMD_QUERY_RESP); - return response; -} +#include "media_delivery_fixture.h" static void test_bounds_and_identity(void) { reset(); struct UTUN_INSTANCE* s = instance(SERVER); struct UTUN_INSTANCE* c = instance(CLIENT); @@ -262,21 +101,6 @@ static void test_partial_and_timeout(void) { puts("partial results and QUERY timeout during another block's progress: PASS"); } -static void deliver_block(struct UTUN_INSTANCE* c, struct media_download* dl, int bi) { - struct media_download_block* b = &dl->blocks[bi]; - struct packet p = {.from = b->holders[b->holder_idx], .to = CLIENT, .group = GROUP, .len = MEDIA_BLOCK_CHUNK_HDR_SIZE + 1}; - struct media_pkt_block_chunk* chunk = (struct media_pkt_block_chunk*)p.data; - chunk->subcmd = MEDIA_SUBCMD_BLOCK_CHUNK; chunk->chunk = bi; chunk->data_len = 1; - memcpy(chunk->media_id, dl->media_id, 16); memcpy(chunk->block_id, dl->blocks[bi].block_id, 16); - memcpy(chunk->transfer_id, b->transfer_id, 16); - p.data[MEDIA_BLOCK_CHUNK_HDR_SIZE] = (uint8_t)bi; receive(c, &p); - memset(p.data, 0, sizeof(p.data)); p.len = MEDIA_BLOCK_DONE_SIZE; - struct media_pkt_block_done* done = (struct media_pkt_block_done*)p.data; - done->subcmd = MEDIA_SUBCMD_BLOCK_DONE; done->chunk = bi; done->total_size = 1; - memcpy(done->media_id, dl->media_id, 16); memcpy(done->block_id, dl->blocks[bi].block_id, 16); - memcpy(done->transfer_id, b->transfer_id, 16); - receive(c, &p); -} static void test_peer_reuse_and_completion(void) { reset(); struct UTUN_INSTANCE* s = instance(SERVER); struct UTUN_INSTANCE* c = instance(CLIENT);