From 83cd3bb631131a116f024cfacfd687e8c4855fd1 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Fri, 6 Nov 2026 08:28:16 +0100 Subject: Bound what load_state will accept for an age --- src/opentracker.c | 8 ++++---- src/trackerlogic.c | 2 +- tests/integration/test_statefile.py | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/opentracker.c b/src/opentracker.c index 65fe382..e467ce6 100644 --- a/src/opentracker.c +++ b/src/opentracker.c @@ -300,7 +300,7 @@ void load_state(const char *const state_filename) { FILE *state_filehandle; char inbuf[512]; ot_hash infohash; - uint64_t base, downcount; + uint64_t age, downcount; int consumed; state_filehandle = fopen(state_filename, "r"); @@ -318,12 +318,12 @@ void load_state(const char *const state_filename) { continue; i = 2 * sizeof(ot_hash); - if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &base, UINT64_MAX))) + if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &age, OT_TORRENT_TIMEOUT))) continue; i += consumed; - if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &downcount, UINT64_MAX))) + if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &downcount, SIZE_MAX))) continue; - add_torrent_from_saved_state(infohash, base, downcount); + add_torrent_from_saved_state(infohash, (ot_time)age, (size_t)downcount); } fclose(state_filehandle); diff --git a/src/trackerlogic.c b/src/trackerlogic.c index 2313379..9fd6114 100644 --- a/src/trackerlogic.c +++ b/src/trackerlogic.c @@ -45,7 +45,7 @@ void add_torrent_from_saved_state(ot_hash const hash, ot_time age, size_t down_c ot_torrent *torrent; ot_vector *torrents_list; - if (age > OT_TORRENT_TIMEOUT) + if (age < 0 || age > OT_TORRENT_TIMEOUT) return; torrents_list = mutex_bucket_lock_by_hash(hash); diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py index 9f6db01..21eefae 100644 --- a/tests/integration/test_statefile.py +++ b/tests/integration/test_statefile.py @@ -52,3 +52,43 @@ class StateFileRoundTrip(TrackerTestCase): restarted.stop() finally: restarted._cleanup() + + +class StateFileWithNonsenseAges(TrackerTestCase): + """The age column is read as unsigned and handed on as a signed ot_time. + A value at or above 2^63 arrived negative, cleared the timeout check, and + put the torrent's base in the future, where the cleaner never reaches it. + At exactly 2^63 the subtraction itself overflowed.""" + + AGES = { + "overflow": 2**63, + "wrapped": 2**63 + 60 * 24, + "minus-one": 2**64 - 1, + "int64-max": 2**63 - 1, + "past-timeout": 60 * 24 + 1, + } + + def test_no_age_outside_the_timeout_builds_a_torrent(self): + lines = ["%s:%d:7" % (self.hash_for(name).hex(), age) for name, age in self.AGES.items()] + lines.append("%s:5:7" % self.hash_for("honest").hex()) + + path = os.path.join(self.tracker.dir, "nonsense") + with open(path, "w") as handle: + handle.write("\n".join(lines) + "\n") + + restarted = Tracker(extra_args=["-l", path]) + try: + for name in self.AGES: + self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"], + "the line with age %d was loaded" % self.AGES[name]) + self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"], + "the state file was not read at all") + self.assertEqual(restarted.torrent_count(), 1) + complaints = restarted.stop() + self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) + finally: + restarted._cleanup() + + +if __name__ == "__main__": + unittest.main() -- cgit v1.3