From dfdd89b5f05eaccb686fe4fcca8f0995c00cbff0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 21:40:24 +0100 Subject: [PATCH] Reproduce cross-group file transfer effects and destination truncation --- tests/test_chat_group_isolation.c | 102 +++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/test_chat_group_isolation.c b/tests/test_chat_group_isolation.c index 7c15cf45b..d863505fb 100644 --- a/tests/test_chat_group_isolation.c +++ b/tests/test_chat_group_isolation.c @@ -35,6 +35,8 @@ #include "../src/routing_layer/nat_detection.c" /* Управляем границей отложенной выдачи, не заменяя обработчики MULTICAST. */ #include "../src/routing_layer/multicast.c" +/* Сверяем активные операции/offset после сетевой инъекции и до файловых callbacks. */ +#include "../src/media_delivery/file_transfer.c" #define REQUIRE(expr) do { if (!(expr)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "isolation setup: %s", #expr); return -1; } } while (0) @@ -790,6 +792,103 @@ static int attachment_isolation(void) { return 0; } +static struct { + char source[640]; + unsigned lookups, blocked, done[2]; + int error[2]; + struct file_transfer_op* pulls[2]; +} transfers; + +/* Предметный владелец разрешает объект: отказ чужой группы обязан произойти раньше lookup. */ +static int transfer_lookup(void* arg, uint64_t group, uint64_t peer, const uint8_t object[16], + uint64_t size, char* path, size_t cap) { + (void)arg; (void)peer; (void)object; (void)size; + transfers.lookups++; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation file lookup group=%llu count=%u", (unsigned long long)group, transfers.lookups); + snprintf(path, cap, "%s", transfers.source); return 0; +} + +static void transfer_done(void* arg, int error) { + unsigned i = (unsigned)(uintptr_t)arg; + transfers.pulls[i] = NULL; transfers.done[i]++; transfers.error[i] = error; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation file done group=%u error=%d count=%u", 111 + i, error, transfers.done[i]); +} + +/* Удерживаем настоящие DATA в reorder очереди B; A принимает их обычным обработчиком. */ +static int transfer_ready(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, const uint8_t* data, size_t size) { + (void)inst; (void)peer; (void)data; (void)size; + if (group != 112) return 1; + transfers.blocked++; return 0; +} + +static int transfer_isolation(void) { + struct UTUN_INSTANCE* receiver = fixture.nodes[0]; struct UTUN_INSTANCE* sender = fixture.nodes[1]; + uint8_t object[16] = {0x53}, bytes[65536]; memset(bytes, 0x6a, sizeof(bytes)); + char paths[2][640], denied[640]; + snprintf(transfers.source, sizeof(transfers.source), "%s/transfer.source", fixture.directory); + snprintf(denied, sizeof(denied), "%s/transfer.denied", fixture.directory); + FILE* file = fopen(transfers.source, "wb"); REQUIRE(file); + REQUIRE(fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes) && !fclose(file)); + file = fopen(denied, "wb"); REQUIRE(file); REQUIRE(fwrite("keep", 1, 4, file) == 4 && !fclose(file)); + for (unsigned i = 0; i < 2; i++) REQUIRE(file_transfer_create(fixture.nodes[i], transfer_lookup, NULL)); + struct file_transfer_op* refused = file_transfer_pull(receiver->file_transfer, 112, sender->node_id, object, + sizeof(bytes), denied, transfer_done, (void*)1); + if (refused) file_transfer_cancel(refused); + file = fopen(denied, "rb"); REQUIRE(file); char marker[4]; size_t length = fread(marker, 1, 4, file); REQUIRE(!fclose(file)); + check("FILE_TRANSFER denied local pull preserves destination before fopen", !refused && length == 4 && !memcmp(marker, "keep", 4)); + REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK); + for (unsigned i = 0; i < 2; i++) + REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id))); + uint64_t deadline = get_time_tb() + 50000; + while ((!topo_group_peer_ready(fixture.groups[0][1], sender->node_id) || + !topo_group_peer_ready(fixture.groups[1][1], receiver->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1); + REQUIRE(topo_group_peer_ready(fixture.groups[0][1], sender->node_id) && topo_group_peer_ready(fixture.groups[1][1], receiver->node_id)); + etcp_router_set_receive_ready(receiver, ETCP_RT_ID_FILE_TRANSFER, transfer_ready); + for (unsigned i = 0; i < 2; i++) { + snprintf(paths[i], sizeof(paths[i]), "%s/transfer.%u", fixture.directory, i); + transfers.pulls[i] = file_transfer_pull(receiver->file_transfer, 111 + i, sender->node_id, object, + sizeof(bytes), paths[i], transfer_done, (void*)(uintptr_t)i); + REQUIRE(transfers.pulls[i]); + } + deadline = get_time_tb() + 30000; + while ((!transfers.done[0] || !transfers.blocked) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1); + REQUIRE(transfers.done[0] == 1 && transfers.error[0] == 0 && transfers.blocked && transfers.pulls[1]); + check("FILE_TRANSFER A completes while B waits in router", transfers.pulls[1]->offset == 0 && transfers.lookups == 2); + struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(receiver, 112, sender->node_id, ETCP_RT_ID_FILE_TRANSFER); + REQUIRE(route && route->peer_reset_id && route->recv_q->head); + struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = sender->node_id, + .dst_node_id = receiver->node_id, .svc_id = ETCP_RT_ID_FILE_TRANSFER, .reset_id = route->peer_reset_id, + .peer_reset_id = route->reset_id, .seq = route->rx_seq}; + uint8_t data[26] = {FT_DATA}; memcpy(data + 1, transfers.pulls[1]->tid, 16); data[25] = 0x6a; + char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)sender->node_id); + REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); + topo_group_revalidate_members(fixture.groups[0][1]); + check("FILE_TRANSFER revoke cancels B operation before callback", !transfers.pulls[1] && transfers.done[1] == 1 && + transfers.error[1] == -2 && !receiver->file_transfer->ops); + etcp_router_set_receive_ready(receiver, ETCP_RT_ID_FILE_TRANSFER, NULL); + REQUIRE(!send_routed(&saved, data, sizeof(data), ROUTE_CRYPTO_SIGN)); + uint8_t request[49] = {FT_REQUEST}; request[1] = 0x77; memcpy(request + 17, object, 16); + uint64_t size = sizeof(bytes); memcpy(request + 33, &size, 8); saved.seq++; + unsigned lookups = transfers.lookups; + REQUIRE(!send_routed(&saved, request, sizeof(request), ROUTE_CRYPTO_SIGN)); + check("revoked signed FILE_TRANSFER REQUEST never reaches lookup", transfers.lookups == lookups); + struct stat state; REQUIRE(!stat(paths[1], &state)); + check("revoked FILE_TRANSFER queue and late DATA cannot write destination", state.st_size == 0 && transfers.done[1] == 1); + /* Та же личная копия повторяется через разрешённую A с новым transfer ID. */ + if (transfers.pulls[1]) file_transfer_cancel(transfers.pulls[1]); + transfers.done[1] = 0; + transfers.pulls[1] = file_transfer_pull(receiver->file_transfer, 111, sender->node_id, object, + sizeof(bytes), paths[1], transfer_done, (void*)1); REQUIRE(transfers.pulls[1]); + deadline = get_time_tb() + 30000; + while (!transfers.done[1] && get_time_tb() < deadline) uasync_poll(fixture.ua, 1); + file = fopen(paths[1], "rb"); REQUIRE(file); uint8_t received[sizeof(bytes)]; + length = fread(received, 1, sizeof(received), file); REQUIRE(!fclose(file)); + check("FILE_TRANSFER retries same object via A on shared ETCP", transfers.done[1] == 1 && !transfers.error[1] && + length == sizeof(bytes) && !memcmp(received, bytes, sizeof(bytes)) && fixture.transport->links_up); + for (unsigned i = 0; i < 2; i++) file_transfer_destroy(fixture.nodes[i]->file_transfer); + return 0; +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); @@ -815,13 +914,14 @@ int main(void) { #ifdef __linux__ if (!fixture.failed && file_isolation(1)) fixture.failed++; #endif - if (!fixture.failed && (attachment_isolation() || stop_isolation())) fixture.failed++; + if (!fixture.failed && (attachment_isolation() || transfer_isolation() || stop_isolation())) fixture.failed++; } } pthread_mutex_lock(&fixture.disk_mutex); fixture.disk_released = 1; pthread_cond_signal(&fixture.disk_condition); pthread_mutex_unlock(&fixture.disk_mutex); for (unsigned i = 0; i < 2; i++) if (fixture.handles[i]) conn_mgr_close(fixture.handles[i]); for (unsigned i = 0; i < 2; i++) if (fixture.nodes[i]) { + file_transfer_destroy(fixture.nodes[i]->file_transfer); chat_join_destroy(fixture.nodes[i]); radio_call_destroy(fixture.nodes[i]); member_files_destroy(fixture.nodes[i]); member_sync_destroy(fixture.nodes[i]); utun_instance_destroy(fixture.nodes[i]); }