From 01c2ebd7d8ffbdc228d81a0f71af5440124dcae7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 04:43:31 +0100 Subject: [PATCH] Verify channel SQL failure rollback and complete signed profiles through an independent reader --- tests/test_chat_channel.c | 87 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/tests/test_chat_channel.c b/tests/test_chat_channel.c index 6a7b42d0..3e42ce20 100644 --- a/tests/test_chat_channel.c +++ b/tests/test_chat_channel.c @@ -100,7 +100,8 @@ static void snapshot(sqlite3* db, uint8_t out[32]) { const char* queries[] = { "SELECT channel_id,name,owner_node_id,x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature FROM channels ORDER BY channel_id", "SELECT namespace,level,prefix64,hash FROM merkle_tree_hash ORDER BY namespace,level,prefix64", - "SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name" + "SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name", + "SELECT type,name,tbl_name,sql FROM sqlite_temp_master ORDER BY type,name" }; EVP_MD_CTX* hash = EVP_MD_CTX_new(); assert(hash && EVP_DigestInit_ex(hash, EVP_sha256(), NULL) == 1); for (unsigned query = 0; query < sizeof(queries) / sizeof(queries[0]); query++) { @@ -146,11 +147,87 @@ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int for (int i = 1; i <= 5; i++) assert(sqlite3_column_int(st, i) == (i == 5 ? 64 : 32)); assert(sqlite3_finalize(st) == SQLITE_OK); uint8_t key[32]; assert(member_sync_author_key(inst, channel, inst->node_id, key) == MS_AUTHOR_ACTIVE); - assert(!memcmp(key, inst->my_ed25519_pubkey, 32)); channel_events++; + assert(!memcmp(key, inst->my_ed25519_pubkey, 32)); + char query[256]; snprintf(query, sizeof(query), "SELECT join_sig,update_sig,update_ts,userinfo,signature,source FROM peers_%s WHERE node_id=?", channel); + assert(sqlite3_prepare_v2(reader, query, -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); + assert(sqlite3_column_bytes(st, 0) == 64 && sqlite3_column_bytes(st, 1) == 64 && sqlite3_column_bytes(st, 4) == 64); + assert(sqlite3_column_int64(st, 2) > 0 && !sqlite3_column_int(st, 5)); + uint8_t profile[MS_USERINFO_MAX + 73], signature[64]; + int bytes = member_sync_build_update_msg(sqlite3_column_blob(st, 0), (uint64_t)sqlite3_column_int64(st, 2), + (const char*)sqlite3_column_text(st, 3), profile, sizeof(profile)); + assert(bytes > 0); memcpy(signature, sqlite3_column_blob(st, 1), 64); assert(sqlite3_finalize(st) == SQLITE_OK); + assert(sc_ed25519_verify(inst->my_ed25519_pubkey, profile, (size_t)bytes, signature) == SC_OK); + uint8_t writer[32], committed[32]; snapshot(inst->topo_sqlite_db, writer); snapshot(reader, committed); + assert(!memcmp(writer, committed, 32)); channel_events++; +} + +static void refused(struct UTUN_INSTANCE* inst, const char* name) { + uint8_t before[32], after[32]; snapshot(reader, before); + unsigned events = channel_events; int count = channels(), groups = queue_entry_count(inst->topo_groups->group_list); + uint64_t created = chat_core_create_channel_auto(inst, name); + if (created || sql_fault != Q_NONE) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, + "channel test: expected refusal name=%s ID=%llu pending_fault=%d", name, (unsigned long long)created, sql_fault); + assert(!created && sql_fault == Q_NONE && sqlite3_get_autocommit(inst->topo_sqlite_db)); + snapshot(reader, after); assert(!memcmp(before, after, 32) && channels() == count && channel_events == events); + assert(queue_entry_count(inst->topo_groups->group_list) == groups); +} + +static void retry(struct UTUN_INSTANCE* inst) { + unsigned events = channel_events; int count = channels(); + assert(chat_core_create_channel_auto(inst, "successful SQL retry") && channels() == count + 1 && channel_events == events + 1); +} + +static int veto(void* arg) { (*(unsigned*)arg)++; return 1; } + +static void sql_regressions(struct UTUN_INSTANCE* inst) { + const char* queries[] = {"SELECT 1 FROM channels WHERE channel_id=?", "INSERT INTO channels(channel_id, name, owner_node_id,", + "INSERT INTO \"peers_"}; + unsigned cases = 0; + for (unsigned query = 0; query < 3; query++) { + sql_prefix = queries[query]; sql_suffix = NULL; + for (enum sql_fault point = Q_PREPARE; point <= Q_FINALIZE; point++) { + if (point == Q_CHANGES && !query) continue; + int bindings = point == Q_BIND ? (query == 0 ? 1 : query == 1 ? 8 : 11) : 1; + for (int bind = 1; bind <= bindings; bind++) { + sql_fault = point; sql_bind = bind; refused(inst, "SQL statement refusal"); retry(inst); cases++; + } + } + } + const char* exec[] = {"BEGIN IMMEDIATE /* channel creation */", "SAVEPOINT channel_put", "CREATE TABLE IF NOT EXISTS \"peers_", + "CREATE INDEX IF NOT EXISTS \"idx_peers_", "CREATE INDEX IF NOT EXISTS \"idx_peers_", + "CREATE TABLE IF NOT EXISTS feedback_", "RELEASE channel_put", "COMMIT /* channel creation */"}; + for (unsigned i = 0; i < sizeof(exec) / sizeof(exec[0]); i++) { + sql_prefix = exec[i]; sql_suffix = i == 3 ? "_type_rtt\"" : i == 4 ? "_storage\"" : NULL; + sql_fault = Q_EXEC; refused(inst, "SQL transaction/schema refusal"); retry(inst); cases++; + } + sql_prefix = sql_suffix = NULL; + for (int allocation = 1; allocation <= 3; allocation++) { + allocations_until_failure = allocation; sql_fault = Q_ALLOCATE; + refused(inst, "SQL schema allocation refusal"); retry(inst); cases++; + } + const char* triggers[] = { + "CREATE TEMP TRIGGER reject_create BEFORE INSERT ON channels BEGIN SELECT RAISE(ABORT,'channel create'); END", + "CREATE TEMP TRIGGER reject_create BEFORE INSERT ON channels BEGIN SELECT RAISE(IGNORE); END" + }; + for (unsigned i = 0; i < 2; i++) { + assert(sqlite3_exec(inst->topo_sqlite_db, triggers[i], NULL, NULL, NULL) == SQLITE_OK); + refused(inst, "channel trigger refusal"); + assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_create", NULL, NULL, NULL) == SQLITE_OK); retry(inst); + } + unsigned commits = 0; sqlite3_commit_hook(inst->topo_sqlite_db, veto, &commits); + refused(inst, "channel commit veto"); assert(commits == 1); sqlite3_commit_hook(inst->topo_sqlite_db, NULL, NULL); retry(inst); + uint8_t before[32], after[32]; snapshot(reader, before); unsigned events = channel_events; + assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN;INSERT INTO ui_state(key,value) VALUES('foreign-create','1')", NULL, NULL, NULL) == SQLITE_OK); + assert(!chat_core_create_channel_auto(inst, "foreign transaction") && !sqlite3_get_autocommit(inst->topo_sqlite_db)); + snapshot(reader, after); assert(!memcmp(before, after, 32) && events == channel_events); + assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); retry(inst); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel SQL:%u faults/retries,ABORT/IGNORE/veto,foreign transaction; exact committed descriptions/schema/roots and runtime count preserved", cases); } int main(void) { - debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); size_t baseline = u_get_allocated_count(); + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_INFO); + size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); char directory[] = "/tmp/utun-channel-create-XXXXXX"; assert(mkdtemp(directory)); char config[512], database[256]; @@ -171,7 +248,9 @@ int main(void) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "channel test: profile bind failure ID=%llu channels=%d events=%u", (unsigned long long)created, channels(), channel_events); assert(!fail_profile_binding && !created && channels() == 1 && channel_events == 1); - utun_instance_destroy(inst); active = NULL; assert(channels() == 1 && sqlite3_close(reader) == SQLITE_OK); reader = NULL; + sql_regressions(inst); + int committed = channels(); + utun_instance_destroy(inst); active = NULL; assert(channels() == committed && sqlite3_close(reader) == SQLITE_OK); reader = NULL; uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel creation: failed root signature leaves no channel/event; retry publishes valid membership after commit"); return 0;