fix: iteration race, NID bounds check, and DRY delay parsing

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 <noreply@anthropic.com>
This commit is contained in:
Maciek "mab122" Bator 2026-04-23 13:23:45 +02:00
parent 2ab81b525f
commit 05dc078f31
2 changed files with 17 additions and 27 deletions

View File

@ -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,

View File

@ -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