Cover invite response bounds isolation and failure propagation

This commit is contained in:
evgeny committed 2026-10-24 15:30:04 +02:00
1 parent 483e89cd79
commit 9ce0abe5a9
2 files changed
+138

No files matched your search

+1
View File
@@ -690,6 +690,7 @@ test_sock_match_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_invite_group_create_SOURCES = test_invite_group_create.c
test_invite_group_create_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_invite_group_create_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_invite_group_create_LDFLAGS = -Wl,--wrap=etcp_send -Wl,--wrap=uasync_set_timeout -Wl,--wrap=u_calloc_impl
test_etcp_connect_SOURCES = test_etcp_connect.c
test_etcp_connect_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
+137
View File
@@ -5,6 +5,7 @@
#include "../lib/platform_compat.h"
#include "../lib/socket_compat.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../src/utun_instance.h"
#include "../src/chat/chat_core.h"
#include "../src/chat/invite_link.h"
@@ -12,6 +13,8 @@
#include "../src/routing_layer/topo_node_sqlite.h"
#include "../src/routing_layer/topo_group_invite.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/transport_layer/etcp_api.h"
#include "../src/transport_layer/node_conn_direct.h"
#define REQUIRE(expr) do { if (!(expr)) { \
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "test_invite_group_create: line=%d failed: %s", __LINE__, #expr); \
@@ -23,6 +26,55 @@ struct invite_result {
uint64_t node_id, group_id;
};
static uint8_t response[512];
static size_t response_size;
static int hold_requests, requests, fail_request_send, fail_timer, fail_alloc;
static void invite_done(uint64_t node_id, uint64_t group_id, int event, void* arg);
int __real_etcp_send(struct ETCP_CONN*, struct ll_entry*);
int __wrap_etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry->len >= 2 && entry->dgram[0] == ETCP_ID_GROUP_INVITE) {
if (entry->dgram[1] == TGI_SUBCMD_INFO_RESP && entry->len <= sizeof(response)) {
response_size = entry->len; memcpy(response, entry->dgram, response_size);
}
if (entry->dgram[1] == TGI_SUBCMD_INFO_REQ && hold_requests) {
requests++;
if (fail_request_send) return -1;
queue_dgram_free(entry); queue_entry_free(entry); return 0;
}
}
return __real_etcp_send(conn, entry);
}
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t cb, const char* name) {
if (fail_timer && !strcmp(name, "invite_info")) return NULL;
return __real_uasync_set_timeout(ua, delay, arg, cb, name);
}
void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* location) {
if (fail_alloc && strstr(location, "topo_group_invite.c")) return NULL;
return __real_u_calloc_impl(count, size, location);
}
static void deliver(struct ETCP_CONN* conn, const uint8_t* bytes, size_t size) {
struct ll_entry* entry = ll_alloc_lldgram(size ? size : 1);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "test invite: cannot allocate injected packet"); return; }
if (size) memcpy(entry->dgram, bytes, size);
entry->len = (uint16_t)size;
topo_group_invite_recv_handler(conn, entry);
}
/* Начальный UP от NCD откладывается: довести запрос до отправки INFO_REQ без ответа. */
static int begin_wait(struct UTUN_INSTANCE* inst, uint64_t gid, struct TOPO_NODE* node, struct invite_result* reply) {
memset(reply, 0, sizeof(*reply));
int before = requests;
if (topo_group_invite_join(inst, gid, node, node->node_id, invite_done, reply) < 0) return -1;
for (int i = 0; i < 20 && requests == before; i++) uasync_poll(inst->ua, 0);
return requests == before + 1 ? 0 : -1;
}
/* Сохранить результат; жизненный цикл экземпляров меняется после выхода из callback. */
static void invite_done(uint64_t node_id, uint64_t group_id, int event, void* arg) {
struct invite_result* result = arg;
@@ -35,6 +87,7 @@ int main(void) {
int result = 1;
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE *a = NULL, *b = NULL;
struct NODE_CONN_DIRECT* owner_handle = NULL;
struct invite_result reply = {0};
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO);
@@ -117,8 +170,92 @@ int main(void) {
/* Получение описания не заменяет протокол добавления: B ещё не дал членство A. */
REQUIRE(!topo_node_sqlite_member_in_channel(b->topo_sqlite_db, group_b->channel_id, a->node_id));
REQUIRE(!topo_group_peer_ready(group_b, a->node_id));
/* Сохраняем рабочий транспорт у независимого владельца на всё время fault-проверок. */
REQUIRE(node_conn_direct_open_node(a, b->node_id, NULL, NULL, &owner_handle, &node, NULL) != NCD_ERR);
struct ETCP_CONN* conn = node_conn_direct_get_conn(owner_handle);
REQUIRE(conn && conn->links_up && response_size == 12 + strlen("TestCh") + 136);
uint8_t valid[512]; size_t valid_size = response_size;
memcpy(valid, response, valid_size);
hold_requests = 1;
struct invite_result second = {0};
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &second) == -1);
REQUIRE(!second.calls);
REQUIRE(begin_wait(a, gid ^ 1, &node, &second) == 0);
/* Чужой instance и peer не должны завершать ни один ожидающий запрос. */
struct ETCP_CONN foreign = { .instance = b, .peer_node_id = b->node_id };
deliver(&foreign, valid, valid_size);
foreign.instance = a; foreign.peer_node_id = a->node_id;
deliver(&foreign, valid, valid_size);
REQUIRE(!reply.calls && !second.calls);
deliver(conn, valid, valid_size);
REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_JOIN && !second.calls);
/* Другой group_id не выбирается по одному лишь совпадению peer. */
uint8_t bad[513]; memcpy(bad, valid, valid_size);
uint64_t other_gid = gid ^ 1; memcpy(bad + 2, &other_gid, 8);
deliver(conn, bad, valid_size);
REQUIRE(second.calls == 1 && second.event == TGI_EVENT_TIMEOUT && second.group_id == other_gid);
REQUIRE(!topo_groups_find(a->topo_groups, other_gid));
/* Все усечения настоящего подписанного ответа: нет OOB, нет принятого неполного канала. */
for (size_t cut = 0; cut < valid_size; cut++) {
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
deliver(conn, valid, cut);
REQUIRE(cut < 10 ? !reply.calls : reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT);
topo_group_invite_cancel_all(a);
REQUIRE(!a->group_invites && node_conn_direct_get_conn(owner_handle) == conn && !conn->close_requested);
}
/* Лишний хвост, неизвестный status, слишком длинное/содержащее NUL имя, неверная подпись. */
for (int fault = 0; fault < 6; fault++) {
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
memcpy(bad, valid, valid_size); bad[valid_size] = 0;
if (fault == 1) bad[10] = 2;
if (fault == 2) bad[11] = 128;
if (fault == 3) bad[11] = 255;
if (fault == 4) bad[12] = 0;
if (fault == 5) bad[valid_size - 1] ^= 1;
deliver(conn, bad, valid_size + (fault == 0));
REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites);
}
/* available=0 — отсутствие описания, не отрицательный результат проверки членства. */
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
memcpy(bad, valid, 12); bad[10] = bad[11] = 0;
deliver(conn, bad, 12);
REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT);
/* Ошибки памяти/таймера до успешного старта не вызывают callback. */
memset(&reply, 0, sizeof(reply)); fail_alloc = 1;
REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1);
fail_alloc = 0; fail_timer = 1;
REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1);
fail_timer = 0;
REQUIRE(!reply.calls && !a->group_invites);
fail_request_send = 1;
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites);
fail_request_send = 0;
/* Ошибка записи проверенного описания обязана запрещать успешный callback. */
REQUIRE(sqlite3_exec(a->topo_sqlite_db, "CREATE TEMP TRIGGER invite_write_fail BEFORE INSERT ON channels "
"BEGIN SELECT RAISE(ABORT,'invite audit failure'); END", NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(begin_wait(a, gid, &node, &reply) == 0);
deliver(conn, valid, valid_size);
REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT);
REQUIRE(sqlite3_exec(a->topo_sqlite_db, "DROP TRIGGER invite_write_fail", NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(topo_node_sqlite_channel_get(a->topo_sqlite_db, group_a->channel_id, name, sizeof(name),
&owner, x25519, ed25519, signature) == 0);
REQUIRE(!strcmp(name, "TestCh") && owner == b->node_id && !memcmp(signature, expected_signature, 64));
REQUIRE(!memcmp(x25519, expected_x, 32) && !memcmp(ed25519, expected_ed, 32));
REQUIRE(!topo_node_find_by_id(group_a, b->node_id));
REQUIRE(node_conn_direct_get_conn(owner_handle) == conn && !conn->close_requested);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite fault checks: %zu truncations, isolation, framing, signature, OOM, timer, send, SQL PASS",
valid_size);
result = 0;
cleanup:
fail_alloc = fail_timer = fail_request_send = 0;
if (owner_handle) node_conn_direct_close(owner_handle);
if (a) utun_instance_destroy(a);
if (b) utun_instance_destroy(b);
/* Дать event loop освободить отменённые таймеры перед проверкой ресурсов. */