Summary
When handle_connect_disconnect receives a PeerConnected event for a peer_id already present in connected_peers with a different authority set, it inserts the new authorities into connected_authorities and overwrites connected_peers[peer_id], but fails to remove the previously-mapped authorities from connected_authorities. These orphaned entries are never cleaned up because PeerDisconnected only iterates the current connected_peers set. The update_authority_ids method at lib.rs:659-664 shows the intended cleanup pattern (prev.iter().for_each(|a| self.connected_authorities.remove(a))) which the PeerConnected handler lacks. The network bridge rx (bridge/src/rx/mod.rs:270-275) resolves and passes Some(authority_ids) in production, so this path is live.
Severity / Sensitivity
Medium severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.
Source Evidence
Full Relevant PoC Test
Paste the snippet below into an in-crate unit test module for the affected package. It is self-contained apart from helpers and types already available in that crate's existing test context.
use super::*;
fn assert_connected_maps_consistent<AD>(gs: &GossipSupport<AD>) {
for (authority, peer_id) in &gs.connected_authorities {
let consistent = gs
.connected_peers
.get(peer_id)
.map_or(false, |set| set.contains(authority));
assert!(
consistent,
"connected_authorities maps {:?} -> {:?} but connected_peers is missing or inconsistent",
authority,
peer_id,
);
}
for (peer_id, authorities) in &gs.connected_peers {
for authority in authorities {
assert_eq!(
gs.connected_authorities.get(authority),
Some(peer_id),
"connected_peers[{:?}] contains {:?} but connected_authorities does not map back correctly",
peer_id,
authority,
);
}
}
}
fn rwf_peer_connected_event(
peer_id: PeerId,
authorities: HashSet<AuthorityDiscoveryId>,
) -> NetworkBridgeEvent<GossipSupportNetworkMessage> {
NetworkBridgeEvent::PeerConnected(
peer_id,
ObservedRole::Authority,
ValidationVersion::V3.into(),
Some(authorities),
)
}
// RWF-RISKS: risk_dk9kymdc
// RWF-INVARIANTS: inv_n7ybsekt
#[test]
fn test_risk_dk9kymdc_vwn4acva_duplicate_connect_orphans_authorities() {
let alice = AUTHORITIES[0].clone();
let bob = AUTHORITIES[1].clone();
let charlie = AUTHORITIES[2].clone();
let mut gs =
make_subsystem_with_authority_discovery(MockAuthorityDiscovery::new(AUTHORITIES.clone()));
let p = PeerId::random();
gs.handle_connect_disconnect(rwf_peer_connected_event(
p,
HashSet::from([alice.clone(), bob.clone()]),
));
assert_connected_maps_consistent(&gs);
gs.handle_connect_disconnect(rwf_peer_connected_event(p, HashSet::from([charlie.clone()])));
assert_connected_maps_consistent(&gs);
}
Reproduction Command
cargo test -p polkadot-gossip-support test_risk_dk9kymdc_vwn4acva_duplicate_connect_orphans_authorities -- --nocapture
Observed Result
The PoC fails on the current implementation with:
connected_authorities maps a stale authority to a peer that connected_peers no longer lists
Expected Behavior
The target should preserve the invariant described above instead of producing the confirmed failure. In particular, the affected operation should reject malformed or inconsistent input, preserve durable state invariants, or return an error rather than silently corrupting state or panicking.
Validation Rationale
The public source shows handle_connect_disconnect overwriting connected_peers[peer_id] on PeerConnected without removing authorities previously mapped to that peer. The nearby update_authority_ids path demonstrates the expected cleanup pattern by removing entries from connected_authorities before inserting the replacement set. The PoC sends two PeerConnected events for the same peer with different authority sets and then checks that both maps are still inverse-consistent; stale authorities remain orphaned.
RWF Metadata
- Found by Runtime Whitebox Fuzzer
- Finding:
find_n2nmx58447
- Report:
freport_ib6p2ab4gq
- Target:
polkadot-gossip-support
- Run:
20260628-212807-a53980
- Severity:
medium
- Category:
Logic
- Invariants:
inv_n7ybsekt, inv_dai53sv8
- Risks:
risk_dk9kymdc, risk_vwn4acva
Summary
When handle_connect_disconnect receives a PeerConnected event for a peer_id already present in connected_peers with a different authority set, it inserts the new authorities into connected_authorities and overwrites connected_peers[peer_id], but fails to remove the previously-mapped authorities from connected_authorities. These orphaned entries are never cleaned up because PeerDisconnected only iterates the current connected_peers set. The update_authority_ids method at lib.rs:659-664 shows the intended cleanup pattern (prev.iter().for_each(|a| self.connected_authorities.remove(a))) which the PeerConnected handler lacks. The network bridge rx (bridge/src/rx/mod.rs:270-275) resolves and passes Some(authority_ids) in production, so this path is live.
Severity / Sensitivity
Medium severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.
Source Evidence
polkadot/node/network/gossip-support/src/lib.rs lines 673-682: The PeerConnected handler inserts new authorities into connected_authorities and overwrites connected_peers[peer_id] without removing the previously-mapped authorities, orphaning them in connected_authorities.polkadot/node/network/gossip-support/src/lib.rs lines 648-667: The update_authority_ids method correctly removes old authority entries from connected_authorities before inserting new ones and updating connected_peers, demonstrating the intended cleanup pattern that PeerConnected lacks.polkadot/node/network/gossip-support/src/lib.rs lines 683-688: The PeerDisconnected handler only removes connected_authorities entries present in the current connected_peers set, confirming orphaned entries from a duplicate PeerConnected can never be cleaned up.polkadot/node/network/bridge/src/rx/mod.rs lines 270-275: The network bridge resolves authority IDs via get_authority_ids_by_peer_id and passes them as Some in PeerConnected events in production, confirming the affected code path is live.Full Relevant PoC Test
Paste the snippet below into an in-crate unit test module for the affected package. It is self-contained apart from helpers and types already available in that crate's existing test context.
Reproduction Command
cargo test -p polkadot-gossip-support test_risk_dk9kymdc_vwn4acva_duplicate_connect_orphans_authorities -- --nocaptureObserved Result
Expected Behavior
The target should preserve the invariant described above instead of producing the confirmed failure. In particular, the affected operation should reject malformed or inconsistent input, preserve durable state invariants, or return an error rather than silently corrupting state or panicking.
Validation Rationale
The public source shows
handle_connect_disconnectoverwritingconnected_peers[peer_id]onPeerConnectedwithout removing authorities previously mapped to that peer. The nearbyupdate_authority_idspath demonstrates the expected cleanup pattern by removing entries fromconnected_authoritiesbefore inserting the replacement set. The PoC sends twoPeerConnectedevents for the same peer with different authority sets and then checks that both maps are still inverse-consistent; stale authorities remain orphaned.RWF Metadata
find_n2nmx58447freport_ib6p2ab4gqpolkadot-gossip-support20260628-212807-a53980mediumLogicinv_n7ybsekt,inv_dai53sv8risk_dk9kymdc,risk_vwn4acva