/* Создание канала: настоящие ключи/подписи/SQLite; отказ не публикует незавершённый канал. */ #define _GNU_SOURCE #include #include #include #include #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_core_priv.h" #include "../src/chat/chat_event.h" #include "../src/chat/chat_headless_control.h" #include "../src/chat/member_sync.h" #include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/topo_group.h" #include "../lib/platform_compat.h" #include "../lib/socket_compat.h" #include "../lib/debug_config.h" #include "../lib/mem.h" static unsigned fail_sign_number, channel_events; static struct UTUN_INSTANCE* active; static sqlite3* reader; static int fail_profile_binding; enum sql_fault { Q_NONE, Q_PREPARE, Q_BIND, Q_STEP, Q_CHANGES, Q_FINALIZE, Q_EXEC, Q_ALLOCATE }; static enum sql_fault sql_fault; static const char *sql_prefix, *sql_suffix; static int sql_bind, allocations_until_failure, failed_rollback, last_target; enum crypto_fault { K_NONE, K_CONTEXT, K_INITIALIZE, K_GENERATE, K_PUBLIC, K_PUBLIC_SIZE, K_PRIVATE, K_PRIVATE_SIZE, K_RANDOM, K_DESCRIPTION, K_INVITATION, K_PROFILE }; static enum crypto_fault crypto_fault; static int crypto_type; static EVP_PKEY_CTX* crypto_context; static EVP_PKEY* crypto_key; static int take_crypto(enum crypto_fault point) { if (crypto_fault != point) return 0; crypto_fault = K_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: crypto fault type=%d stage=%d", crypto_type, point); return 1; } extern EVP_PKEY_CTX* __real_EVP_PKEY_CTX_new_id(int, ENGINE*); EVP_PKEY_CTX* __wrap_EVP_PKEY_CTX_new_id(int type, ENGINE* engine) { if (type == crypto_type && take_crypto(K_CONTEXT)) return NULL; EVP_PKEY_CTX* ctx = __real_EVP_PKEY_CTX_new_id(type, engine); if (type == crypto_type && crypto_fault != K_NONE) crypto_context = ctx; return ctx; } extern int __real_EVP_PKEY_keygen_init(EVP_PKEY_CTX*); int __wrap_EVP_PKEY_keygen_init(EVP_PKEY_CTX* ctx) { if (ctx == crypto_context && take_crypto(K_INITIALIZE)) return 0; return __real_EVP_PKEY_keygen_init(ctx); } extern int __real_EVP_PKEY_keygen(EVP_PKEY_CTX*, EVP_PKEY**); int __wrap_EVP_PKEY_keygen(EVP_PKEY_CTX* ctx, EVP_PKEY** key) { if (ctx == crypto_context && take_crypto(K_GENERATE)) return 0; int rc = __real_EVP_PKEY_keygen(ctx, key); if (ctx == crypto_context && rc == 1 && crypto_fault != K_NONE) crypto_key = *key; return rc; } extern int __real_EVP_PKEY_get_raw_public_key(const EVP_PKEY*, unsigned char*, size_t*); int __wrap_EVP_PKEY_get_raw_public_key(const EVP_PKEY* key, unsigned char* data, size_t* length) { if (key == crypto_key && take_crypto(K_PUBLIC)) return 0; int rc = __real_EVP_PKEY_get_raw_public_key(key, data, length); if (key == crypto_key && take_crypto(K_PUBLIC_SIZE)) *length = 31; return rc; } extern int __real_EVP_PKEY_get_raw_private_key(const EVP_PKEY*, unsigned char*, size_t*); int __wrap_EVP_PKEY_get_raw_private_key(const EVP_PKEY* key, unsigned char* data, size_t* length) { if (key == crypto_key && take_crypto(K_PRIVATE)) return 0; int rc = __real_EVP_PKEY_get_raw_private_key(key, data, length); if (key == crypto_key && take_crypto(K_PRIVATE_SIZE)) *length = 31; return rc; } extern int __real_RAND_bytes(unsigned char*, int); int __wrap_RAND_bytes(unsigned char* data, int length) { if (length == 8 && take_crypto(K_RANDOM)) return 0; return __real_RAND_bytes(data, length); } static int sql_target(const char* sql) { return sql_prefix && !strncmp(sql, sql_prefix, strlen(sql_prefix)) && (!sql_suffix || strstr(sql, sql_suffix)); } static int take_sql(enum sql_fault point, const char* sql, int index) { if (sql_fault != point || (sql && !sql_target(sql)) || (point == Q_BIND && index != sql_bind)) return 0; sql_fault = Q_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: SQL fault stage=%d bind=%d", point, index); return 1; } 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 (*cb)(void*, int, char**, char**), void* arg, char** error) { if (failed_rollback && !strcmp(sql, "ROLLBACK")) { failed_rollback = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: rollback failed"); return SQLITE_IOERR; } if (take_sql(Q_EXEC, sql, 0)) return SQLITE_IOERR; return __real_sqlite3_exec(db, sql, cb, arg, error); } 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 length, sqlite3_stmt** st, const char** tail) { if (take_sql(Q_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; } return __real_sqlite3_prepare_v2(db, sql, length, st, tail); } 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 length, void (*destroy)(void*)) { const char* prefix = "INSERT INTO \"peers_"; if (fail_profile_binding && index == 6 && !strncmp(sqlite3_sql(st), prefix, strlen(prefix))) { fail_profile_binding = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: injected root profile signature bind failure"); return SQLITE_NOMEM; } if (take_sql(Q_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_blob(st, index, value, length, destroy); } 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 length, void (*destroy)(void*)) { if (take_sql(Q_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_text(st, index, value, length, 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) { if (take_sql(Q_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_int64(st, index, value); } extern int __real_sqlite3_bind_null(sqlite3_stmt*, int); int __wrap_sqlite3_bind_null(sqlite3_stmt* st, int index) { if (take_sql(Q_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM; return __real_sqlite3_bind_null(st, index); } extern int __real_sqlite3_step(sqlite3_stmt*); int __wrap_sqlite3_step(sqlite3_stmt* st) { const char* sql = sqlite3_sql(st); last_target = sql_target(sql); if (take_sql(Q_STEP, sql, 0)) return SQLITE_IOERR; return __real_sqlite3_step(st); } extern int __real_sqlite3_changes(sqlite3*); int __wrap_sqlite3_changes(sqlite3* db) { if (last_target && take_sql(Q_CHANGES, NULL, 0)) return 0; return __real_sqlite3_changes(db); } extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { int fail = st && take_sql(Q_FINALIZE, sqlite3_sql(st), 0); int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; } extern void* __real_u_malloc_impl(uint32_t, const char*); void* __wrap_u_malloc_impl(uint32_t size, const char* location) { if (sql_fault == Q_ALLOCATE && strstr(location, "topo_node_sqlite.c") && !--allocations_until_failure) { sql_fault = Q_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: schema allocation failed bytes=%u", size); return NULL; } return __real_u_malloc_impl(size, location); } /* Проверяем логические bytes описаний,всей схемы и Merkle через независимое соединение. */ 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_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++) { assert(EVP_DigestUpdate(hash, &query, sizeof(query)) == 1); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, queries[query], -1, &st, NULL) == SQLITE_OK); int rc; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { for (int column = 0; column < sqlite3_column_count(st); column++) { int type = sqlite3_column_type(st, column), bytes = sqlite3_column_bytes(st, column); const void* value = sqlite3_column_blob(st, column); assert(!bytes || value); assert(EVP_DigestUpdate(hash, &type, sizeof(type)) == 1 && EVP_DigestUpdate(hash, &bytes, sizeof(bytes)) == 1); if (bytes) assert(EVP_DigestUpdate(hash, value, (size_t)bytes) == 1); } } assert(rc == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK); } unsigned length; assert(EVP_DigestFinal_ex(hash, out, &length) == 1 && length == 32); EVP_MD_CTX_free(hash); } extern int __real_EVP_DigestSign(EVP_MD_CTX*, unsigned char*, size_t*, const unsigned char*, size_t); int __wrap_EVP_DigestSign(EVP_MD_CTX* ctx, unsigned char* signature, size_t* length, const unsigned char* data, size_t size) { if (data && fail_sign_number && active && size == 112 && !memcmp(data + 72, active->my_keys.public_key, 32) && !--fail_sign_number) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: injected signature failure bytes=%zu", size); return 0; } if (data && active && size == MEMBER_SIGN_MSG_SIZE && !memcmp(data, active->my_keys.public_key, 32) && !memcmp(data + 32, MEMBER_SIGN_SUFFIX, MEMBER_SIGN_SUFFIX_LEN) && take_crypto(K_INVITATION)) return 0; if (data && size >= 80 && !memcmp(data + 72, "{\"name\":", 8) && take_crypto(K_PROFILE)) return 0; const uint8_t* zero = data ? memchr(data, 0, size) : NULL; if (zero && zero - data < 20 && (size_t)(zero - data + 1) + sizeof("crypto refusal") <= size && !memcmp(zero + 1, "crypto refusal", sizeof("crypto refusal")) && take_crypto(K_DESCRIPTION)) return 0; return __real_EVP_DigestSign(ctx, signature, length, data, size); } static int channels(void) { sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT count(*) FROM channels", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); int count = sqlite3_column_int(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK); return count; } /* Обработчик видит канал и проверенного участника уже после commit. */ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int size) { if (type != CHAT_EVT_CHANNEL_UPDATED) return; assert(sqlite3_get_autocommit(inst->topo_sqlite_db) && size == data[0] + 1 && data[0] < 20); char channel[20]; memcpy(channel, data + 1, data[0]); channel[data[0]] = 0; sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT owner_node_id,length(x25519_pubkey),length(x25519_privkey)," "length(ed25519_pubkey),length(ed25519_privkey),length(signature) FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); assert((uint64_t)sqlite3_column_int64(st, 0) == inst->node_id); 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)); 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 && crypto_fault == K_NONE && !fail_sign_number && 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 descriptor_regressions(void) { uint8_t x[32] = {1}, ed[32] = {2}, out[CHAT_CHANNEL_DESCRIPTION_MAX], expected[CHAT_CHANNEL_DESCRIPTION_MAX]; const char* id = "9223372036854775807"; char name[257]; memset(name, 'n', 255); name[255] = 0; uint64_t owner = UINT64_C(0x0123456789abcdef); size_t offset = 0; memcpy(expected, id, 20); offset += 20; memcpy(expected + offset, name, 256); offset += 256; memcpy(expected + offset, &owner, 8); offset += 8; memcpy(expected + offset, x, 32); offset += 32; memcpy(expected + offset, ed, 32); offset += 32; assert(offset == sizeof(out) && chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out)) == (int)offset); assert(!memcmp(out, expected, sizeof(out))); memset(out, 0xa5, sizeof(out)); memset(expected, 0xa5, sizeof(expected)); assert(chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out) - 1) < 0 && !memcmp(out, expected, sizeof(out))); name[255] = 'n'; name[256] = 0; assert(chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out)) < 0 && !memcmp(out, expected, sizeof(out))); const char* invalid[] = {"", "0", "01", "-1", "1x", " 1", "9223372036854775808", "18446744073709551615", "12345678901234567890"}; for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { assert(chat_channel_description_data(invalid[i], "", owner, x, ed, out, sizeof(out)) < 0); assert(!memcmp(out, expected, sizeof(out))); } assert(chat_channel_description_data("1", "", owner, x, ed, out, 75) == 75); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel description: canonical ID range,255-byte name,capacity,unchanged error output,exact shared wire"); } static void direct_refused(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req) { uint8_t before[32], after[32]; snapshot(reader, before); unsigned events = channel_events; int groups = queue_entry_count(inst->topo_groups->group_list); assert(chat_core_create_channel(inst, req) < 0 && sqlite3_get_autocommit(inst->topo_sqlite_db)); snapshot(reader, after); assert(!memcmp(before, after, 32) && events == channel_events); assert(groups == queue_entry_count(inst->topo_groups->group_list)); } static void direct_regressions(struct UTUN_INSTANCE* inst) { struct chat_channel_create original = {.inst = inst, .owner_node_id = inst->node_id}, request; sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT channel_id,name,x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature " "FROM channels ORDER BY channel_id LIMIT 1", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); snprintf(original.channel_id, sizeof(original.channel_id), "%s", sqlite3_column_text(st, 0)); snprintf(original.name, sizeof(original.name), "%s", sqlite3_column_text(st, 1)); void* keys[] = {original.x25519_pubkey, original.x25519_privkey, original.ed25519_pubkey, original.ed25519_privkey, original.signature}; for (unsigned i = 0; i < 5; i++) { int bytes = i == 4 ? 64 : 32; assert(sqlite3_column_bytes(st, (int)i + 2) == bytes); memcpy(keys[i], sqlite3_column_blob(st, (int)i + 2), (size_t)bytes); } assert(sqlite3_finalize(st) == SQLITE_OK); direct_refused(inst, &original); direct_refused(inst, NULL); request = original; request.owner_node_id ^= 1; direct_refused(inst, &request); request = original; request.x25519_privkey[8] ^= 1; direct_refused(inst, &request); request = original; request.ed25519_privkey[8] ^= 1; direct_refused(inst, &request); request = original; request.signature[0] ^= 1; direct_refused(inst, &request); request = original; memset(request.name, 'n', sizeof(request.name)); direct_refused(inst, &request); request = original; memset(request.channel_id, '1', sizeof(request.channel_id)); direct_refused(inst, &request); const char* invalid[] = {"0", "01", "9223372036854775808", "1x"}; for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { request = original; snprintf(request.channel_id, sizeof(request.channel_id), "%s", invalid[i]); direct_refused(inst, &request); } assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK); assert(chat_core_create_channel(inst, &original) < 0 && !sqlite3_get_autocommit(inst->topo_sqlite_db)); assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); char name[129]; memset(name, 'n', 128); name[128] = 0; refused(inst, name); request = original; snprintf(request.channel_id, sizeof(request.channel_id), "9223372036854775807"); memset(request.name, 'n', 127); request.name[127] = 0; uint8_t description[CHAT_CHANNEL_DESCRIPTION_MAX]; int bytes = chat_channel_description_data(request.channel_id, request.name, request.owner_node_id, request.x25519_pubkey, request.ed25519_pubkey, description, sizeof(description)); assert(bytes > 0 && sc_ed25519_sign(request.ed25519_privkey, description, (size_t)bytes, request.signature) == SC_OK); int count = channels(); unsigned events = channel_events; assert(!chat_core_create_channel(inst, &request) && channels() == count + 1 && channel_events == events + 1); direct_refused(inst, &request); OPENSSL_cleanse(&original, sizeof(original)); OPENSSL_cleanse(&request, sizeof(request)); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel API: malformed/occupied IDs,owner/key/signature/name checks,foreign transaction;127-byte name/max-ID commit"); } static void rpc_request(struct UASYNC* ua, socket_t fd, const char* request, int id, int success) { assert(send(fd, request, (int)strlen(request), 0) == (ssize_t)strlen(request)); char response[1024] = {0}; size_t length = 0; for (int attempt = 0; attempt < 2500 && !strchr(response, '\n'); attempt++) { uasync_poll(ua, 20); ssize_t received = recv(fd, response + length, sizeof(response) - length - 1, 0); if (received < 0) { int error = socket_get_error(); assert(error == ERR_AGAIN || error == ERR_WOULDBLOCK || error == ERR_INTR); continue; } assert(received > 0 && length + (size_t)received < sizeof(response) - 1); length += (size_t)received; response[length] = 0; } assert(strchr(response, '\n')); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT json_extract(?1,'$.id'),json_extract(?1,'$.ok')," "json_extract(?1,'$.data.created'),json_extract(?1,'$.data.channel_id'),json_extract(?1,'$.error')", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_bind_text(st, 1, response, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); assert(sqlite3_column_int(st, 0) == id && sqlite3_column_int(st, 1) == success); if (success) { assert(sqlite3_column_int(st, 2) == 1 && sqlite3_column_type(st, 3) == SQLITE_TEXT); assert(strtoull((const char*)sqlite3_column_text(st, 3), NULL, 10) > 0); } else assert(sqlite3_column_type(st, 4) == SQLITE_TEXT); assert(sqlite3_finalize(st) == SQLITE_OK); } static void headless_regressions(struct UTUN_INSTANCE* inst, struct UASYNC* ua) { assert(!chat_headless_control_init(ua, inst, "127.0.0.1", 0)); socket_t fd = socket(AF_INET, SOCK_STREAM, 0); assert(fd != SOCKET_INVALID); struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons(chat_headless_control_get_port(inst))}; assert(inet_pton(AF_INET, "127.0.0.1", &address.sin_addr) == 1 && !connect(fd, (struct sockaddr*)&address, sizeof(address))); assert(!socket_set_nonblocking(fd)); int count = channels(); sql_prefix = "BEGIN IMMEDIATE /* channel creation */"; sql_suffix = NULL; sql_fault = Q_EXEC; rpc_request(ua, fd, "{\"id\":17,\"cmd\":\"create_channel\",\"name\":\"RPC failed\"}\n", 17, 0); assert(sql_fault == Q_NONE && channels() == count); rpc_request(ua, fd, "{\"id\":18,\"cmd\":\"create_channel\",\"name\":\"RPC created\"}\n", 18, 1); assert(channels() == count + 1); socket_close_wrapper(fd); chat_headless_control_destroy(inst); chat_event_set_handler(inst, event); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel TCP API: failed commit path returns ok:false;retry returns committed channel_id"); } static void crypto_regressions(struct UTUN_INSTANCE* inst) { const int types[] = {EVP_PKEY_X25519, EVP_PKEY_ED25519}; unsigned cases = 0; for (unsigned type = 0; type < 2; type++) { for (enum crypto_fault point = K_CONTEXT; point <= K_PRIVATE_SIZE; point++) { crypto_type = types[type]; crypto_context = NULL; crypto_key = NULL; crypto_fault = point; refused(inst, "crypto refusal"); retry(inst); cases++; } } crypto_type = 0; crypto_context = NULL; crypto_key = NULL; for (enum crypto_fault point = K_RANDOM; point <= K_PROFILE; point++) { crypto_fault = point; refused(inst, "crypto refusal"); retry(inst); cases++; } fail_sign_number = 1; refused(inst, "crypto refusal"); retry(inst); cases++; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel crypto:%u faults/retries; key generation/extraction sizes,RNG,description,JOIN,invitation,profile", cases); } 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_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]; snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[ntp]\nenabled=no\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory); struct UASYNC* ua = uasync_create(); assert(ua); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && !utun_core_start(inst) && !chat_service_start(inst)); snprintf(database, sizeof(database), "%s/chats.db", directory); assert(sqlite3_open_v2(database, &reader, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); active = inst; chat_event_set_handler(inst, event); assert(!channels()); fail_sign_number = 1; chat_core_create_channel_auto(inst, "root signature failure"); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "channel test: after signature fault channels=%d events=%u", channels(), channel_events); assert(!fail_sign_number && !channels() && !channel_events); chat_core_create_channel_auto(inst, "successful retry"); assert(channels() == 1 && channel_events == 1); fail_profile_binding = 1; uint64_t created = chat_core_create_channel_auto(inst, "root profile SQL failure"); 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); descriptor_regressions(); direct_regressions(inst); crypto_regressions(inst); sql_regressions(inst); headless_regressions(inst, ua); int committed = channels(); uint8_t before[32], after[32]; snapshot(reader, before); unsigned events = channel_events; sql_prefix = "COMMIT /* channel creation */"; sql_suffix = NULL; sql_fault = Q_EXEC; failed_rollback = 1; inst->running = 1; assert(!chat_core_create_channel_auto(inst, "failed rollback") && sql_fault == Q_NONE && !failed_rollback); assert(!inst->running && !sqlite3_get_autocommit(inst->topo_sqlite_db) && channel_events == events); snapshot(reader, after); assert(!memcmp(before, after, 32)); 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; }