From 8003413d970c67e6542954f6ecc2d94bfe968aac Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 05:00:27 +0100 Subject: [PATCH] test(chat): cover channel input contracts and TCP creation result --- src/chat/chat_core.h | 20 ++++--- tests/test_chat_channel.c | 115 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 673139e2..3b0562f3 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -117,15 +117,15 @@ int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, u // ── Создание канала (GUI → uasync) ── struct chat_channel_create { - struct UTUN_INSTANCE* inst; - char channel_id[64]; - char name[128]; - uint64_t owner_node_id; - uint8_t x25519_pubkey[32]; - uint8_t x25519_privkey[32]; - uint8_t ed25519_pubkey[32]; - uint8_t ed25519_privkey[32]; - uint8_t signature[64]; + struct UTUN_INSTANCE* inst; /* Заимствован trampoline; прямой вызов использует свой аргумент inst. */ + char channel_id[64]; /* NUL-строка: каноническое десятичное число 1..INT64_MAX. */ + char name[128]; /* NUL-строка до127 байт, без молчаливого обрезания. */ + uint64_t owner_node_id; /* node_id создающего instance. */ + uint8_t x25519_pubkey[32]; /* Канальный ключ шифрования; должен соответствовать privkey. */ + uint8_t x25519_privkey[32];/* Независимый приватный X25519-ключ канала. */ + uint8_t ed25519_pubkey[32];/* Канальный ключ подписи; должен соответствовать raw seed. */ + uint8_t ed25519_privkey[32];/* Raw Ed25519 seed, без преобразования sc_derive_ed25519_pubkey. */ + uint8_t signature[64]; /* Подпись id\0 || name\0 || owner(native u64) || X25519 || Ed25519. */ }; struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; }; @@ -141,6 +141,7 @@ int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id * с открытой транзакцией запрашивает stop/закрытие БД владельцем вне callback. */ int chat_core_create_channel(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req); +// Забирает u_malloc-структуру chat_channel_create, очищает ключи и освобождает её после вызова. void chat_core_create_channel_trampoline(void* arg); /* Сгенерировать независимые пары X25519/Ed25519 и создать канал. ID>0 — сохранён,0 — отказ. @@ -148,6 +149,7 @@ void chat_core_create_channel_trampoline(void* arg); * временные приватные ключи очищаются. Внешняя транзакция отклоняется до генерации. */ uint64_t chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name); +// Забирает и освобождает u_malloc-структуру chat_create_auto_arg; instance должен дожить до выполнения. void chat_core_create_channel_auto_trampoline(void* arg); // ── Локальное удаление канала (GUI → uasync) ── diff --git a/tests/test_chat_channel.c b/tests/test_chat_channel.c index c68fc4b8..36b70ab3 100644 --- a/tests/test_chat_channel.c +++ b/tests/test_chat_channel.c @@ -8,10 +8,12 @@ #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" @@ -172,13 +174,13 @@ static void snapshot(sqlite3* db, uint8_t out[32]) { 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 (fail_sign_number && active && size == 112 && !memcmp(data + 72, active->my_keys.public_key, 32) && !--fail_sign_number) { + 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 (active && size == MEMBER_SIGN_MSG_SIZE && !memcmp(data, active->my_keys.public_key, 32) && + 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 (size >= 80 && !memcmp(data + 72, "{\"name\":", 8) && take_crypto(K_PROFILE)) return 0; - const uint8_t* zero = memchr(data, 0, size); + 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); @@ -236,6 +238,108 @@ static void retry(struct UTUN_INSTANCE* inst) { 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++) { @@ -320,8 +424,11 @@ 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); + 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;