From 4a05e52ba10da742cf195505daab3ed8212f75cc Mon Sep 17 00:00:00 2001 From: Daniel Norman Date: Fri, 19 Jun 2026 23:26:39 +0200 Subject: [PATCH] fetcher: Guard fetched against stale node results - Clear active[rid] only when the result's node matches the node that started the fetch; mismatched results now report NotFound - Prevent a late completion from a disconnected peer clearing a newer fetch for the same repo started by a different node - Add test covering the stale-from mismatch path --- crates/radicle-protocol/src/fetcher/state.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/radicle-protocol/src/fetcher/state.rs b/crates/radicle-protocol/src/fetcher/state.rs index cc9ad2b2..2379f275 100644 --- a/crates/radicle-protocol/src/fetcher/state.rs +++ b/crates/radicle-protocol/src/fetcher/state.rs @@ -203,16 +203,28 @@ impl FetcherState { } } - /// Process a [`Fetched`] command, which removes the given fetch from the set of active fetches. + /// Process a [`Fetched`] command, which removes the given fetch from the + /// set of active fetches, but only if the active fetch was from the given + /// node. + /// /// Note that this is agnostic of whether the fetch succeeded or failed. /// /// The caller will be notified if the completed fetch did not exist in the active set. /// /// [`Fetched`]: command::Fetched pub fn fetched(&mut self, command::Fetched { from, rid }: command::Fetched) -> event::Fetched { - match self.active.remove(&rid) { - None => event::Fetched::NotFound { from, rid }, - Some(ActiveFetch { from, refs }) => event::Fetched::Completed { from, rid, refs }, + // Only the node that started the active fetch may complete it. A result from any + // other node is stale — e.g. a late completion delivered after the peer disconnected + // and the repo was re-fetched from elsewhere. Clearing the entry on a mismatch would + // drop the wrong (newer) fetch, so we leave it and report `NotFound`. + match self.active.get(&rid) { + Some(ActiveFetch { + from: active_from, .. + }) if *active_from == from => match self.active.remove(&rid) { + Some(ActiveFetch { from, refs }) => event::Fetched::Completed { from, rid, refs }, + None => event::Fetched::NotFound { from, rid }, + }, + _ => event::Fetched::NotFound { from, rid }, } }