From 47293a4f116e04a88b0797bafad0c98b4393b744 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Thu, 5 Nov 2026 13:52:17 +0100 Subject: Clamp every elapsed time span at zero, using a set of helper functions --- tests/t_clean.c | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/t_clean.c (limited to 'tests/t_clean.c') diff --git a/tests/t_clean.c b/tests/t_clean.c new file mode 100644 index 0000000..7eca9aa --- /dev/null +++ b/tests/t_clean.c @@ -0,0 +1,140 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. */ + +#include +#include +#include +#include + +#include "ot_clean.h" +#include "ot_mutex.h" +#include "trackerlogic.h" +#include "test.h" + +/* A clock stepped backwards used to leave peer_list->base in the future, so + the span the cleaner works from came out negative. It was then added to a + peer's age and stored in the single byte that age lives in, where -1 reads + as 255 and the next pass threw the whole swarm away. */ + +static void announce(struct ot_workstruct *ws, ot_hash *hash, uint8_t last_octet, int seeding) { + static const uint8_t v4mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + + /* The peer list is written into the reply, which workstruct_init leaves + for its caller to point somewhere */ + ws->reply = ws->outbuf; + + memset(&ws->peer, 0, sizeof(ws->peer)); + memcpy(&ws->peer, v4mapped, sizeof(v4mapped)); + ((uint8_t *)&ws->peer)[15] = last_octet; + OT_SETPORT(&ws->peer, &(uint16_t){6881}); + if (seeding) + OT_PEERFLAG(ws->peer) |= PEER_FLAG_SEEDING; + ws->hash = hash; + add_peer_to_torrent_and_return_peers(FLAG_TCP, ws, 0); +} + +static size_t peers_in(ot_hash *hash) { + char reply[1024]; + size_t len = return_udp_scrape_for_torrent(*hash, reply); + uint32_t seeders, leechers; + + if (len < 12) + return 0; + memcpy(&seeders, reply, 4); + memcpy(&leechers, reply + 8, 4); + return (size_t)ntohl(seeders) + (size_t)ntohl(leechers); +} + +static ot_torrent *torrent_of(ot_hash *hash) { + ot_vector *bucket = mutex_bucket_lock_by_hash(*hash); + ot_torrent *t = (ot_torrent *)bucket->data; + ot_torrent *found = NULL; + size_t i; + + for (i = 0; i < bucket->size; ++i) + if (!memcmp(t[i].hash, hash, sizeof(ot_hash))) + found = t + i; + mutex_bucket_unlock_by_hash(*hash, 0); + return found; +} + +/* What a clock stepped back by `minutes` leaves behind */ +static void step_clock_back(ot_torrent *torrent, int minutes) { + torrent->peer_list6->base += minutes; + torrent->peer_list4->base += minutes; +} + +/* What the next minute of a well behaved clock looks like */ +static void age_by_a_minute(ot_torrent *torrent) { + torrent->peer_list6->base -= 1; + torrent->peer_list4->base -= 1; +} + +static void case_a_clock_that_went_back_keeps_the_swarm(void) { + struct ot_workstruct ws; + ot_hash hash; + ot_torrent *torrent; + + test_case("a swarm survives the clock being stepped backwards"); + memset(hash, 0x31, sizeof(hash)); + CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); + + announce(&ws, &hash, 1, 1); + announce(&ws, &hash, 2, 0); + CHECK_INT(peers_in(&hash), 2); + + torrent = torrent_of(&hash); + CHECK(torrent != NULL); + step_clock_back(torrent, 5); + + /* The pass that sees the step keeps everybody either way: unclamped it + writes a negative span into each peer's age byte, where it reads as 251 + rather than as a removal. */ + CHECK_INT(clean_single_torrent(torrent), 0); + CHECK_INT(peers_in(&hash), 2); + + /* A minute later, and this is where a poisoned age byte took the swarm: + 251 plus one minute is past any timeout. */ + age_by_a_minute(torrent); + CHECK_INT(clean_single_torrent(torrent), 0); + CHECK_INT(peers_in(&hash), 2); + + workstruct_deinit(&ws); +} + +static void case_an_idle_swarm_still_ages_out(void) { + struct ot_workstruct ws; + ot_hash hash; + ot_torrent *torrent; + + test_case("a swarm nobody touched is still cleaned away"); + memset(hash, 0x32, sizeof(hash)); + CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); + + announce(&ws, &hash, 3, 1); + CHECK_INT(peers_in(&hash), 1); + + torrent = torrent_of(&hash); + CHECK(torrent != NULL); + step_clock_back(torrent, -OT_PEER_TIMEOUT); + + CHECK_INT(clean_single_torrent(torrent), 0); + CHECK_INT(peers_in(&hash), 0); + + workstruct_deinit(&ws); +} + +void test_clean(void) { + sigset_t hup; + + /* accesslist_deinit sends SIGHUP to its reader, trackerlogic_init starts + the accesslist, and the test process must not take it */ + sigemptyset(&hup); + sigaddset(&hup, SIGHUP); + pthread_sigmask(SIG_BLOCK, &hup, NULL); + + trackerlogic_init(); + case_a_clock_that_went_back_keeps_the_swarm(); + case_an_idle_swarm_still_ages_out(); + trackerlogic_deinit(); +} -- cgit v1.3