/* Старт chat_core не должен игнорировать отказ обязательной подписки. */ #define _GNU_SOURCE #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_event.h" #include "../src/chat/member_sync.h" #include "../src/chat/db_sync.h" #include "../src/routing_layer/topo_group.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include #include #include #include static int fail_props, fail_routes, fail_malloc, fail_context, fail_rollback, fail_sync; enum fault { F_NONE, F_PREPARE, F_BIND, F_STEP, F_CHANGES, F_FINALIZE, F_EXEC, F_CATALOG }; static enum fault fault; static const char* target; static int argument, last_target; static sqlite3* reader; static struct nodeinfo_cbk_entry* preserved_callback; static unsigned ready; static uint64_t route_group; static int delete_from_route_event, route_inspect; static unsigned fail_route_event; static unsigned route_seen, route_events; extern int __real_member_sync_add_props_cbk(struct UTUN_INSTANCE*, node_props_changed_fn, void*); int __wrap_member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn callback, void* arg) { if (fail_props) { fail_props = 0; fail_malloc = 1; } return __real_member_sync_add_props_cbk(inst, callback, arg); } extern int __real_utun_add_nodeinfo_cbk(struct UTUN_INSTANCE*, nodeinfo_cbk_fn, void*); int __wrap_utun_add_nodeinfo_cbk(struct UTUN_INSTANCE* inst, nodeinfo_cbk_fn fn, void* arg) { if (fail_routes) { fail_routes = 0; fail_malloc = 2; } return __real_utun_add_nodeinfo_cbk(inst, fn, arg); } extern void* __real_u_malloc_impl(uint32_t, const char*); void* __wrap_u_malloc_impl(uint32_t bytes, const char* location) { if ((fail_malloc == 1 && strstr(location, "member_sync.c")) || (fail_malloc == 2 && strstr(location, "topo_group.c"))) { fail_malloc = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "lifecycle test: subscription allocation refused at %s", location); return NULL; } return __real_u_malloc_impl(bytes, location); } extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); void* __wrap_u_calloc_impl(uint32_t count, uint32_t bytes, const char* location) { if (fail_context && strstr(location, "chat_core.c")) { fail_context = 0; return NULL; } if (fail_route_event && strstr(location, "chat_member.c") && !--fail_route_event) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "lifecycle test: deferred route allocation refused at %s", location); return NULL; } return __real_u_calloc_impl(count, bytes, location); } static int matches(const char* sql) { return sql && target && !strncmp(sql, target, strlen(target)); } static int take(enum fault point, const char* sql, int index) { if (fault != point || (sql && !matches(sql)) || (point == F_BIND && index != argument)) return 0; fault = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "lifecycle test: SQL fault point=%d index=%d target=%s", point, index, target); 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 (take(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; } 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* value, int bytes, void (*destroy)(void*)) { return take(F_BIND, sqlite3_sql(st), index) ? SQLITE_NOMEM : __real_sqlite3_bind_text(st, index, value, bytes, destroy); } extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*)); int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* value, int bytes, void (*destroy)(void*)) { return take(F_BIND, sqlite3_sql(st), index) ? SQLITE_NOMEM : __real_sqlite3_bind_blob(st, index, value, bytes, destroy); } extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) { return take(F_BIND, sqlite3_sql(st), index) ? SQLITE_NOMEM : __real_sqlite3_bind_int64(st, index, value); } extern int __real_sqlite3_step(sqlite3_stmt*); int __wrap_sqlite3_step(sqlite3_stmt* st) { last_target = matches(sqlite3_sql(st)); return take(F_STEP, sqlite3_sql(st), 0) ? SQLITE_IOERR : __real_sqlite3_step(st); } extern int __real_sqlite3_changes(sqlite3*); int __wrap_sqlite3_changes(sqlite3* db) { return last_target && take(F_CHANGES, NULL, 0) ? 0 : __real_sqlite3_changes(db); } extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { int fail = st && take(F_FINALIZE, sqlite3_sql(st), 0), rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; } extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**); int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*fn)(void*, int, char**, char**), void* arg, char** error) { if (fail_rollback && !strcmp(sql, "ROLLBACK")) { fail_rollback = 0; return SQLITE_IOERR; } return take(F_EXEC, sql, 0) ? SQLITE_IOERR : __real_sqlite3_exec(db, sql, fn, arg, error); } extern int __real_sqlite3_get_table(sqlite3*, const char*, char***, int*, int*, char**); int __wrap_sqlite3_get_table(sqlite3* db, const char* sql, char*** result, int* rows, int* columns, char** error) { if (take(F_CATALOG, sql, 0)) { *result = NULL; *rows = 0; *columns = 0; if (error) *error = NULL; return SQLITE_NOMEM; } return __real_sqlite3_get_table(db, sql, result, rows, columns, error); } extern struct DB_SYNC_INSTANCE* __real_db_sync_instance_add(struct UTUN_INSTANCE*, const char*, uint64_t); struct DB_SYNC_INSTANCE* __wrap_db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table, uint64_t group) { if (fail_sync && !--fail_sync) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "lifecycle test: sync allocation refused group=%llu", (unsigned long long)group); return NULL; } return __real_db_sync_instance_add(inst, table, group); } static void snapshot(sqlite3* db, uint8_t out[32]) { sqlite3_int64 bytes = 0; unsigned char* image = sqlite3_serialize(db, "main", &bytes, 0); assert(image && bytes > 0 && SHA256(image, (size_t)bytes, out)); sqlite3_free(image); } static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int bytes) { if (type == CHAT_EVT_MEMBER_UPDATED && route_group && bytes > 0 && data[0] < 21 && bytes > data[0]) { char ch[21]; memcpy(ch, data + 1, data[0]); ch[data[0]] = 0; if (strtoull(ch, NULL, 10) == route_group) { route_events++; if (delete_from_route_event) { delete_from_route_event = 0; chat_core_delete_channel(inst, ch); } } } if (type != CHAT_EVT_DB_READY && type != CHAT_EVT_MY_NODE_ID) return; assert(chat_core_is_initialized(inst) && sqlite3_get_autocommit(inst->topo_sqlite_db)); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT node_id,x25519_pubkey,ed25519_pubkey FROM local_identity WHERE id=1", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_step(st) == SQLITE_ROW && (uint64_t)sqlite3_column_int64(st, 0) == inst->node_id); assert(sqlite3_column_bytes(st, 1) == 32 && !memcmp(sqlite3_column_blob(st, 1), inst->my_keys.public_key, 32)); assert(sqlite3_column_bytes(st, 2) == 32 && !memcmp(sqlite3_column_blob(st, 2), inst->my_ed25519_pubkey, 32)); assert(sqlite3_finalize(st) == SQLITE_OK); ready++; } static void refused(struct UTUN_INSTANCE* inst, const char* path) { uint8_t before[32], after[32]; snapshot(reader, before); ready = 0; size_t allocations = u_get_allocated_count(); assert(chat_core_init(inst, path) < 0 && !fault && !fail_props && !fail_routes && !fail_malloc && !fail_context); assert(!ready && !inst->chat_core && inst->core_started && sqlite3_get_autocommit(inst->topo_sqlite_db)); snapshot(reader, after); assert(!memcmp(before, after, 32) && inst->nodeinfo_cbks == preserved_callback); assert(u_get_allocated_count() == allocations); } static void retry(struct UTUN_INSTANCE* inst, const char* path) { ready = 0; assert(!chat_core_init(inst, path) && ready == 2 && !chat_core_init(inst, path) && ready == 2); assert(chat_core_init(inst, "different/chats.db") < 0 && ready == 2); chat_core_destroy(inst); assert(!inst->chat_core && inst->nodeinfo_cbks == preserved_callback); } static int deny_accounts(void* arg, int action, const char* name, const char* a, const char* db, const char* source) { (void)a; (void)db; (void)source; if (action == SQLITE_CREATE_TABLE && name && !strcmp(name, "accounts")) { (*(unsigned*)arg)++; return SQLITE_DENY; } return SQLITE_OK; } static int veto(void* arg) { (*(unsigned*)arg)++; return 1; } static void preserved(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, void* arg) { if (route_inspect) { assert(group->group_id == route_group && node->node_id == ((struct UTUN_INSTANCE*)arg)->node_id); route_seen++; } } static void sql_faults(struct UTUN_INSTANCE* inst, const char* path) { const char* queries[] = {"INSERT OR REPLACE INTO local_identity", "SELECT value FROM ui_state WHERE key='media_base'"}; unsigned cases = 0; for (size_t q = 0; q < 2; q++) for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) { if (q && (point == F_BIND || point == F_CHANGES)) continue; for (int index = 1; index <= (!q && point == F_BIND ? 6 : 1); index++) { target = queries[q]; fault = point; argument = index; refused(inst, path); retry(inst, path); cases++; } } const char* commands[] = {"BEGIN IMMEDIATE /* chat init */", "SAVEPOINT group_avatar_schema", "RELEASE group_avatar_schema", "CREATE TABLE IF NOT EXISTS media_files", "CREATE TABLE IF NOT EXISTS local_identity", "COMMIT /* chat init */"}; for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { target = commands[i]; fault = F_EXEC; refused(inst, path); retry(inst, path); cases++; } target = "SELECT channel_id,length"; fault = F_CATALOG; refused(inst, path); retry(inst, path); cases++; unsigned commits = 0; sqlite3_commit_hook(inst->topo_sqlite_db, veto, &commits); refused(inst, path); sqlite3_commit_hook(inst->topo_sqlite_db, NULL, NULL); assert(commits == 1); retry(inst, path); fail_context = 1; refused(inst, path); retry(inst, path); fail_props = 1; refused(inst, path); retry(inst, path); fail_routes = 1; refused(inst, path); retry(inst, path); target = NULL; printf("PASS lifecycle:%u SQL faults/retries,commit veto,context/properties/routes allocation;exact DB,callbacks,and ready preserved\n", cases); } static sqlite3_int64 number(sqlite3* db, const char* sql) { sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); sqlite3_int64 result = sqlite3_column_int64(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK); return result; } static void validation(struct UTUN_INSTANCE* inst, const char* path) { uint8_t before[32], after[32]; snapshot(reader, before); ready = 0; assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN;CREATE TABLE lifecycle_outer(value);INSERT INTO lifecycle_outer VALUES(7)", NULL, NULL, NULL) == SQLITE_OK); void* sync_before = inst->db_sync; assert(chat_service_start(inst) < 0 && inst->db_sync == sync_before); assert(chat_core_init(inst, path) < 0 && !inst->chat_core && !sqlite3_get_autocommit(inst->topo_sqlite_db)); assert(number(inst->topo_sqlite_db, "SELECT value FROM lifecycle_outer") == 7); assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); char long_path[513]; memset(long_path, 'x', sizeof(long_path)); long_path[512] = 0; assert(chat_core_init(inst, long_path) < 0 && chat_core_init(NULL, path) < 0 && chat_core_init(inst, NULL) < 0); chat_core_destroy(NULL); snapshot(reader, after); assert(!memcmp(before, after, 32) && !ready && !inst->chat_core); assert(number(reader, "SELECT count(*) FROM accounts") == 0); const char* modes[] = {"ABORT", "IGNORE"}; for (size_t i = 0; i < 2; i++) { char sql[256]; snprintf(sql, sizeof(sql), "CREATE TRIGGER reject_identity BEFORE INSERT ON local_identity BEGIN SELECT RAISE(%s%s);END", modes[i], i ? "" : ",'injected identity rejection'"); assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); refused(inst, path); assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_identity", NULL, NULL, NULL) == SQLITE_OK); retry(inst, path); } puts("PASS lifecycle:foreign transaction preserved,NULL/oversized path rejected,ABORT/IGNORE rollback,no demonstration contacts"); } static void activation(struct UTUN_INSTANCE* inst, const char* path) { ready = 0; assert(!chat_service_start(inst) && ready == 2); uint64_t groups[2] = {chat_core_create_channel_auto(inst, "first saved"), chat_core_create_channel_auto(inst, "second saved")}; assert(groups[0] && groups[1]); struct chat_msg_submit request = {.inst = inst, .data = (uint8_t*)"startup retained", .data_len = 16}; snprintf(request.channel_id, sizeof(request.channel_id), "%llu", (unsigned long long)groups[0]); strcpy(request.content_type, "text/plain"); assert(!chat_core_submit_message(inst, &request)); chat_service_stop(inst); char sql[256]; snprintf(sql, sizeof(sql), "SELECT rowid FROM channels WHERE channel_id='%llu'", (unsigned long long)groups[0]); sqlite3_int64 row = number(reader, sql); const char* invalid[] = {"'01'", "CAST(X'313200' AS TEXT)", "'9223372036854775808'"}; for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { snprintf(sql, sizeof(sql), "UPDATE channels SET channel_id=%s WHERE rowid=%lld", invalid[i], (long long)row); assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); refused(inst, path); snprintf(sql, sizeof(sql), "UPDATE channels SET channel_id='%llu' WHERE rowid=%lld", (unsigned long long)groups[0], (long long)row); assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); } ready = 0; fail_sync = 2; assert(chat_service_start(inst) < 0 && !fail_sync && !ready && !inst->chat_core && !inst->chat_started && inst->core_started); assert(!topo_groups_find(inst->topo_groups, groups[0]) && !topo_groups_find(inst->topo_groups, groups[1])); assert(inst->nodeinfo_cbks == preserved_callback && number(reader, "SELECT count(*) FROM channels") == 2); snprintf(sql, sizeof(sql), "SELECT count(*) FROM msg_%llu", (unsigned long long)groups[0]); assert(number(reader, sql) == 1); assert(!chat_service_start(inst) && ready == 2 && chat_core_count(inst, request.channel_id) == 1); assert(topo_groups_find(inst->topo_groups, groups[0]) && topo_groups_find(inst->topo_groups, groups[1])); chat_service_stop(inst); puts("PASS lifecycle:corrupt catalog rejected;second channel activation failure cleans all runtime,keeps data,and retries without false ready"); } static void route_notifications(struct UTUN_INSTANCE* inst, struct UASYNC* ua) { ready = 0; assert(!chat_service_start(inst) && ready == 2); route_group = chat_core_create_channel_auto(inst, "delete after route dispatch"); assert(route_group); uasync_poll(ua, 0); struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, route_group); assert(group && group->local_node); route_seen = route_events = 0; route_inspect = delete_from_route_event = 1; topo_fire_nodeinfo_cbk(inst, group, group->local_node); topo_fire_nodeinfo_cbk(inst, group, group->local_node); assert(group->local_node->node_id == inst->node_id && route_seen == 2 && delete_from_route_event && !route_events); route_inspect = 0; uasync_poll(ua, 0); assert(!delete_from_route_event && route_events == 1 && !topo_groups_find(inst->topo_groups, route_group)); route_group = chat_core_create_channel_auto(inst, "cancel pending route event"); assert(route_group); uasync_poll(ua, 0); group = topo_groups_find(inst->topo_groups, route_group); assert(group && group->local_node); route_events = 0; for (unsigned failure = 1; failure <= 2; failure++) { fail_route_event = failure; topo_fire_nodeinfo_cbk(inst, group, group->local_node); assert(!fail_route_event); uasync_poll(ua, 0); assert(!route_events); } topo_fire_nodeinfo_cbk(inst, group, group->local_node); assert(!route_events); route_group = 0; chat_service_stop(inst); uasync_poll(ua, 0); assert(!route_events); puts("PASS lifecycle:route observers and caller retain live group/node until dispatch returns;deferred event may delete channel"); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); assert(ua); char directory[] = "/tmp/utun-lifecycle-XXXXXX"; assert(mkdtemp(directory)); char config[512], database[512]; snprintf(config, sizeof(config), "[global]\nmy_node_name=lifecycle test\ndb_path=%s\n[ntp]\nenabled=no\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && !utun_core_start(inst)); chat_event_set_handler(inst, event); snprintf(database, sizeof(database), "%s/chats.db", directory); assert(sqlite3_open_v2(database, &reader, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); assert(!utun_add_nodeinfo_cbk(inst, preserved, inst)); preserved_callback = inst->nodeinfo_cbks; unsigned denied = 0; assert(sqlite3_set_authorizer(inst->topo_sqlite_db, deny_accounts, &denied) == SQLITE_OK); refused(inst, database); assert(denied == 1); assert(sqlite3_set_authorizer(inst->topo_sqlite_db, NULL, NULL) == SQLITE_OK); retry(inst, database); sql_faults(inst, database); validation(inst, database); activation(inst, database); route_notifications(inst, ua); uint8_t before[32], after[32]; snapshot(reader, before); ready = 0; target = "COMMIT /* chat init */"; fault = F_EXEC; fail_rollback = 1; inst->running = 1; assert(chat_core_init(inst, database) < 0 && fault == F_NONE && !fail_rollback && !inst->running && !inst->chat_core); assert(!ready && !sqlite3_get_autocommit(inst->topo_sqlite_db) && inst->nodeinfo_cbks == preserved_callback); snapshot(reader, after); assert(!memcmp(before, after, 32)); utun_instance_destroy(inst); snapshot(reader, after); assert(!memcmp(before, after, 32)); assert(sqlite3_close(reader) == SQLITE_OK); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); puts("PASS lifecycle:subscription failure aborts startup without ready events;core survives and retry succeeds"); return 0; }