/* Ошибки чтения DM не сбрасывают seq/историю/hidden и не разрешают создание вместо загрузки. */ #include #include #include #define OPENSSL_API_COMPAT 0x10100000L #include #include #include "../src/utun_instance.h" #include "../src/dm/dm_core.h" #include "../src/dm/dm_crypto.h" #include "../src/dm/dm_history.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_event.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "test_utils.h" enum fault { FAULT_NONE, FAULT_PREPARE, FAULT_BIND, FAULT_STEP, FAULT_FINALIZE, FAULT_XKEY, FAULT_EDKEY, FAULT_CRYPTO }; enum operation { OP_START, OP_RECEIVE, OP_SEND, OP_LIST, OP_INVITE, OP_FILE }; static enum fault fault; static const char* fault_query; static int injections, events, invites, invite_result; static unsigned checks; static int inject(enum fault kind, const char* sql) { if (fault != kind || injections || (sql && !strstr(sql, fault_query))) return 0; injections++; DEBUG_INFO(DEBUG_CATEGORY_DM, "DM load test: inject fault=%d query=%s", kind, sql ? sql : "X25519 derive"); return 1; } extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**); int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int bytes, sqlite3_stmt** st, const char** tail) { if (inject(FAULT_PREPARE, sql)) { *st = NULL; return SQLITE_IOERR; } return __real_sqlite3_prepare_v2(db, sql, bytes, st, tail); } extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*)); int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* text, int bytes, void (*destroy)(void*)) { if (inject(FAULT_BIND, sqlite3_sql(st))) return SQLITE_NOMEM; return __real_sqlite3_bind_text(st, index, text, bytes, destroy); } extern int __real_sqlite3_step(sqlite3_stmt*); int __wrap_sqlite3_step(sqlite3_stmt* st) { if (inject(FAULT_STEP, sqlite3_sql(st))) return SQLITE_IOERR; return __real_sqlite3_step(st); } extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { int failed = st && inject(FAULT_FINALIZE, sqlite3_sql(st)); int rc = __real_sqlite3_finalize(st); return failed ? SQLITE_IOERR : rc; } extern int __real_EVP_PKEY_derive(EVP_PKEY_CTX*, unsigned char*, size_t*); int __wrap_EVP_PKEY_derive(EVP_PKEY_CTX* ctx, unsigned char* shared, size_t* bytes) { return inject(FAULT_CRYPTO, NULL) ? 0 : __real_EVP_PKEY_derive(ctx, shared, bytes); } static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)inst; events++; if (type == CHAT_EVT_DM_INVITE_RESULT && len == 36) { invites++; memcpy(&invite_result, data + 8, 4); } } static sqlite3_int64 scalar(sqlite3* db, const char* sql) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_int64 value = sqlite3_step(st) == SQLITE_ROW ? sqlite3_column_int64(st, 0) : -1; sqlite3_finalize(st); return value; } /* Сравниваем все поля всех DM-строк, включая точные ключи/подписи/тела и файловые очереди. */ static int snapshot(sqlite3* db, uint8_t hash[32]) { const char* tables[] = { "dm_conversations", "dm_messages", "dm_outbox", "dm_history", "dm_deleted_messages", "dm_files", "dm_local_files" }; SHA256_CTX ctx; SHA256_Init(&ctx); for (size_t i = 0; i < sizeof(tables) / sizeof(tables[0]); i++) { char sql[128]; sqlite3_stmt* st = NULL; snprintf(sql, sizeof(sql), "SELECT * FROM %s ORDER BY rowid", tables[i]); if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; SHA256_Update(&ctx, tables[i], strlen(tables[i])); int rc; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { for (int col = 0; col < sqlite3_column_count(st); col++) { int type = sqlite3_column_type(st, col), bytes = sqlite3_column_bytes(st, col); SHA256_Update(&ctx, &type, sizeof(type)); SHA256_Update(&ctx, &bytes, sizeof(bytes)); if (bytes) SHA256_Update(&ctx, sqlite3_column_blob(st, col), (size_t)bytes); } } sqlite3_finalize(st); if (rc != SQLITE_DONE) return -1; } SHA256_Final(hash, &ctx); return 0; } static int keys(struct UTUN_INSTANCE* inst, struct UTUN_INSTANCE* peer, enum fault damage) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(inst->topo_sqlite_db, "UPDATE dm_conversations SET peer_x25519=?,peer_ed25519=? WHERE peer_node_id=?", -1, &st, NULL)) return -1; sqlite3_bind_blob(st, 1, peer->my_keys.public_key, damage == FAULT_XKEY ? 1 : 32, SQLITE_STATIC); sqlite3_bind_blob(st, 2, peer->my_ed25519_pubkey, damage == FAULT_EDKEY ? 1 : 32, SQLITE_STATIC); sqlite3_bind_int64(st, 3, (sqlite3_int64)peer->node_id); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE ? 0 : -1; } static int peer(struct UTUN_INSTANCE* inst, struct UTUN_INSTANCE* other, const char* group) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(inst->topo_sqlite_db, "INSERT INTO nodes(node_id,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?)", -1, &st, NULL)) return -1; sqlite3_bind_int64(st, 1, (sqlite3_int64)other->node_id); sqlite3_bind_blob(st, 2, other->my_keys.public_key, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 3, other->my_ed25519_pubkey, 32, SQLITE_STATIC); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE ? dm_start(inst, other->node_id, other->my_keys.public_key, other->my_ed25519_pubkey, "peer", group) : -1; } static struct UTUN_INSTANCE* start(struct UASYNC* ua, const char* directory, int index, struct SC_MYKEYS* identity) { char config[1536], pub[65], priv[65]; for (unsigned i = 0; i < 32; i++) { snprintf(pub + 2 * i, 3, "%02x", identity->public_key[i]); snprintf(priv + 2 * i, 3, "%02x", identity->private_key[i]); } snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\ndb_path=%s/%d\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", pub, priv, directory, index); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); if (!inst) return NULL; if (utun_core_start(inst) || chat_service_start(inst)) { utun_instance_destroy(inst); return NULL; } chat_event_set_handler(inst, event); return inst; } static int read_body(sqlite3* db, int seq, uint8_t* body, size_t* length) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, "SELECT body FROM dm_outbox WHERE seq=?", -1, &st, NULL)) return -1; sqlite3_bind_int(st, 1, seq); int rc = sqlite3_step(st), bytes = rc == SQLITE_ROW ? sqlite3_column_bytes(st, 0) : 0; if (bytes > 0 && (size_t)bytes <= *length) { memcpy(body, sqlite3_column_blob(st, 0), (size_t)bytes); *length = (size_t)bytes; } else rc = SQLITE_ERROR; sqlite3_finalize(st); return rc == SQLITE_ROW ? 0 : -1; } static int exercise(struct UTUN_INSTANCE* inst, struct UTUN_INSTANCE* other, const char* conv, uint64_t group, enum operation op, enum fault failure, const uint8_t* body, size_t bytes, const char* path) { sqlite3* db = inst->topo_sqlite_db; if (keys(inst, other, failure)) return -1; uint8_t before[32], after[32], receipt[DM_RECEIPT_SIZE], untouched[DM_RECEIPT_SIZE]; memset(receipt, 0x55, sizeof(receipt)); memcpy(untouched, receipt, sizeof(receipt)); if (snapshot(db, before)) return -1; int changes = sqlite3_total_changes(db), old_events = events, old_invites = invites; fault_query = op == OP_FILE ? "SELECT peer_x25519 FROM dm_conversations" : "SELECT peer_node_id, peer_x25519, peer_ed25519"; injections = 0; fault = failure; int rc = -1; if (op == OP_START) rc = dm_start(inst, other->node_id, other->my_keys.public_key, other->my_ed25519_pubkey, "changed", "99"); else if (op == OP_RECEIVE) rc = dm_accept_message(inst, body, bytes, receipt); else if (op == OP_SEND) rc = dm_send(inst, conv, "text", (const uint8_t*)"rejected", 8); else if (op == OP_LIST) { char json[8192]; size_t length; rc = dm_list_messages_json(inst, conv, 10, 0, json, sizeof(json), &length); } else if (op == OP_INVITE) { struct dm_invite_req* req = u_calloc(1, sizeof(*req)); if (!req) { fault = FAULT_NONE; return -1; } req->inst = inst; req->request_id = 1; req->peer_node_id = other->node_id; req->channel_id = group; snprintf(req->source_ch_id, sizeof(req->source_ch_id), "%llu", (unsigned long long)group); dm_invite_trampoline(req); rc = invites == old_invites + 1 && invite_result == DM_INVITE_START_ERROR ? -1 : 0; } else { struct attachment_info info = { .kind = ATTACHMENT_FILE, .name = "input.bin" }; rc = dm_send_file(inst, conv, path, &info, 0); } fault = FAULT_NONE; int expected_hits = failure == FAULT_XKEY || failure == FAULT_EDKEY ? 0 : 1; if (snapshot(db, after) || rc >= 0 || injections != expected_hits || memcmp(before, after, sizeof(before)) || sqlite3_total_changes(db) != changes || events != old_events + (op == OP_INVITE) || (op == OP_RECEIVE && memcmp(receipt, untouched, sizeof(receipt))) || !sqlite3_get_autocommit(db)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM load test: op=%d fault=%d result=%d injections=%d events=%d->%d changes=%d->%d", op, failure, rc, injections, old_events, events, changes, sqlite3_total_changes(db)); return -1; } if (keys(inst, other, FAULT_NONE)) return -1; checks++; DEBUG_INFO(DEBUG_CATEGORY_DM, "DM load test: preserved op=%d fault=%d check=%u", op, failure, checks); return 0; } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_DM, getenv("UTUN_TEST_DEBUG") ? DEBUG_LEVEL_DEBUG : DEBUG_LEVEL_INFO); size_t allocations = u_get_allocated_count(); struct UASYNC* ua = NULL; struct UTUN_INSTANCE* a = NULL; struct UTUN_INSTANCE* b = NULL; struct SC_MYKEYS identities[2]; char directory[512] = "/tmp/utun_dm_load_XXXXXX", conv[64], group_id[64], input[768]; uint8_t first[2048], second[2048], receipt[DM_RECEIPT_SIZE]; size_t first_len = sizeof(first), second_len = sizeof(second); int failed = 1; #define CHECK(condition) do { if (!(condition)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM load test: failed line=%d", __LINE__); goto done; } } while (0) CHECK(!test_mkdtemp(directory) && !sc_generate_keypair(&identities[0]) && !sc_generate_keypair(&identities[1])); ua = uasync_create(); CHECK(ua); a = start(ua, directory, 0, &identities[0]); b = start(ua, directory, 1, &identities[1]); CHECK(a && b); chat_core_create_channel_auto(b, "load test"); uint64_t group = (uint64_t)scalar(b->topo_sqlite_db, "SELECT channel_id FROM channels WHERE name='load test'"); CHECK(group && group != UINT64_MAX); snprintf(group_id, sizeof(group_id), "%llu", (unsigned long long)group); CHECK(!peer(a, b, group_id) && !peer(b, a, group_id)); snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(a->node_id, b->node_id)); CHECK(!dm_send(a, conv, "text", (const uint8_t*)"one", 3) && !dm_send(a, conv, "text", (const uint8_t*)"two", 3)); CHECK(!read_body(a->topo_sqlite_db, 1, first, &first_len) && !read_body(a->topo_sqlite_db, 2, second, &second_len)); CHECK(!dm_accept_message(b, second, second_len, receipt) && !dm_send(b, conv, "text", (const uint8_t*)"reply", 5)); snprintf(input, sizeof(input), "%s/input.bin", directory); FILE* file = fopen(input, "wb"); CHECK(file); int wrote = fputs("input", file), closed = fclose(file); CHECK(wrote >= 0 && !closed); for (int hidden = 0; hidden < 2; hidden++) { if (hidden) CHECK(!dm_history_delete(b, conv, 1)); for (enum operation op = OP_START; op <= OP_INVITE; op++) { for (enum fault failure = FAULT_PREPARE; failure <= FAULT_CRYPTO; failure++) CHECK(!exercise(b, a, conv, group, op, failure, first, first_len, input)); } if (!hidden) { enum fault media_faults[] = {FAULT_PREPARE, FAULT_STEP, FAULT_XKEY, FAULT_CRYPTO}; for (size_t i = 0; i < sizeof(media_faults) / sizeof(media_faults[0]); i++) CHECK(!exercise(b, a, conv, group, OP_FILE, media_faults[i], first, first_len, input)); } CHECK(scalar(b->topo_sqlite_db, "SELECT last_out_seq FROM dm_conversations") == 1); CHECK(scalar(b->topo_sqlite_db, "SELECT last_in_seq FROM dm_conversations") == 2); CHECK(dm_history_generation(b, conv) == (uint64_t)(hidden + 1)); } utun_instance_destroy(b); b = start(ua, directory, 1, &identities[1]); CHECK(b); CHECK(dm_history_generation(b, conv) == 2 && scalar(b->topo_sqlite_db, "SELECT hidden FROM dm_history") == 1); CHECK(!dm_start(b, a->node_id, a->my_keys.public_key, a->my_ed25519_pubkey, "peer", group_id)); CHECK(!dm_send(b, conv, "text", (const uint8_t*)"after restart", 13)); CHECK(scalar(b->topo_sqlite_db, "SELECT last_out_seq FROM dm_conversations") == 2); CHECK(!dm_accept_message(b, first, first_len, receipt) && !dm_verify_receipt(a, receipt, first, first_len)); CHECK(scalar(b->topo_sqlite_db, "SELECT last_in_seq FROM dm_conversations") == 2); /* Ошибка SELECT отсутствующей строки тоже не разрешает создание; успешный SELECT разрешает. */ utun_instance_destroy(b); b = start(ua, directory, 2, &identities[1]); CHECK(b); CHECK(scalar(b->topo_sqlite_db, "SELECT count(*) FROM dm_conversations") == 0); for (enum fault failure = FAULT_PREPARE; failure <= FAULT_FINALIZE; failure++) CHECK(!exercise(b, a, conv, group, OP_START, failure, first, first_len, input)); CHECK(!exercise(b, a, conv, group, OP_START, FAULT_CRYPTO, first, first_len, input)); CHECK(!peer(b, a, group_id) && scalar(b->topo_sqlite_db, "SELECT last_out_seq FROM dm_conversations") == 0); failed = 0; done: fault = FAULT_NONE; if (b) utun_instance_destroy(b); if (a) utun_instance_destroy(a); if (ua) uasync_destroy(ua, 0); if (allocations != u_get_allocated_count()) failed = 1; printf("test_dm_load: checks=%u allocations=%zu->%zu %s\n", checks, allocations, u_get_allocated_count(), failed ? "FAIL" : "PASS"); return failed; }