Skip to content

[RWF] polkadot-gossip-support: duplicate connect orphans authority mappings #12604

Description

@xlc

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions