From 05dc078f311c825430d9e5f760acdea7c76f9d7b Mon Sep 17 00:00:00 2001 From: "Maciek \"mab122\" Bator" Date: Thu, 23 Apr 2026 13:23:45 +0200 Subject: [PATCH] fix: iteration race, NID bounds check, and DRY delay parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gossip: _poll_loop_once iterated self.rids directly while _discover_rids could append to it from the same call — replaced with list(self.rids) snapshot to avoid RuntimeError on concurrent modification. gossip: _on_announce was missing the nid_len bounds clamp that bridge.py has, allowing a malformed truncated packet to slice beyond available bytes. Now consistent with bridge.py: nid_len = min(nid_len, available). cli: announce_retry_delays string→tuple parsing was copy-pasted three times across cmd_gossip, cmd_seed, and cmd_bridge. Extracted to _parse_delays(). cmd_seed now validates delays before starting any processes so it never needs to stop a running seed just to report a bad argument. Co-Authored-By: Claude Sonnet 4.6 --- src/radicle_reticulum/cli.py | 37 ++++++++++++--------------------- src/radicle_reticulum/gossip.py | 7 ++++--- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/radicle_reticulum/cli.py b/src/radicle_reticulum/cli.py index 9e819c4..2781085 100644 --- a/src/radicle_reticulum/cli.py +++ b/src/radicle_reticulum/cli.py @@ -49,6 +49,14 @@ def detect_radicle_nid() -> Optional[str]: return None +def _parse_delays(s: str) -> Tuple[int, ...]: + try: + return tuple(int(x.strip()) for x in s.split(",") if x.strip()) + except ValueError: + print("Error: --announce-retry-delays must be comma-separated integers.", file=sys.stderr) + sys.exit(1) + + def on_peer_discovered(peer: PeerInfo): """Callback when a new peer is discovered.""" print(f"[+] Discovered peer: {peer.identity.did}") @@ -257,13 +265,7 @@ def cmd_gossip(args): if nid: print(f"Local NID: {nid}") - try: - announce_retry_delays = tuple( - int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip() - ) - except ValueError: - print("Error: --announce-retry-delays must be comma-separated integers.", file=sys.stderr) - sys.exit(1) + announce_retry_delays = _parse_delays(args.announce_retry_delays) relay = GossipRelay( identity=identity, @@ -318,6 +320,9 @@ def cmd_seed(args): """Start a dedicated seed radicle-node, bridge, and gossip relay.""" seed_home = Path(args.seed_home) + # Validate args before starting any processes + announce_retry_delays = _parse_delays(args.announce_retry_delays) + seed = SeedNode(seed_home=seed_home, port=args.seed_port) # First-time setup: guide the user @@ -345,19 +350,9 @@ def cmd_seed(args): seed.stop() sys.exit(1) - # Start bridge pointing at the seed identity = RadicleIdentity.load_or_generate(args.identity) _print_identity_info(args.identity) - try: - announce_retry_delays = tuple( - int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip() - ) - except ValueError: - print("Error: --announce-retry-delays must be comma-separated integers.", file=sys.stderr) - seed.stop() - sys.exit(1) - bridge = RadicleBridge( identity=identity, listen_port=args.bridge_port, @@ -452,13 +447,7 @@ def cmd_bridge(args): auto_connect = not args.no_auto_connect and not args.connect auto_seed = not args.no_auto_seed - try: - announce_retry_delays = tuple( - int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip() - ) - except ValueError: - print("Error: --announce-retry-delays must be comma-separated integers, e.g. 5,15,30", file=sys.stderr) - sys.exit(1) + announce_retry_delays = _parse_delays(args.announce_retry_delays) bridge = RadicleBridge( identity=identity, diff --git a/src/radicle_reticulum/gossip.py b/src/radicle_reticulum/gossip.py index b028f38..d15d0bc 100644 --- a/src/radicle_reticulum/gossip.py +++ b/src/radicle_reticulum/gossip.py @@ -261,7 +261,7 @@ class GossipRelay: if self.auto_discover: self._discover_rids() - for rid in self.rids: + for rid in list(self.rids): # snapshot: _discover_rids may append mid-iteration try: refs = _read_refs(self.storage, rid) with self._refs_lock: @@ -414,8 +414,9 @@ class GossipRelay: try: offset = len(GOSSIP_MAGIC) nid_len = struct.unpack("!H", app_data[offset:offset + 2])[0] - raw = app_data[offset + 2: offset + 2 + nid_len] - radicle_nid = raw.decode() or None + offset += 2 + nid_len = min(nid_len, len(app_data) - offset) + radicle_nid = app_data[offset:offset + nid_len].decode() or None except Exception: pass