Skip to content

[RWF] polkadot-node-subsystem-types: ActiveLeavesUpdate PartialEq is asymmetric #12605

Description

@xlc

Summary

The hand-written PartialEq for ActiveLeavesUpdate uses self.deactivated.len() == other.deactivated.len() && self.deactivated.iter().all(|a| other.deactivated.contains(a)). This is a one-directional subset check that only works when no duplicates exist. When a duplicate hash inflates the length (e.g., [h1, h1] has the same length as [h1, h2]), the length guard passes and the unidirectional containment check reports true in one direction only. This violates the PartialEq symmetry requirement (a == b must imply b == a), which is a documented contract of the Rust PartialEq trait.

Severity / Sensitivity

Low severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.

Source Evidence

  • polkadot/node/subsystem-types/src/lib.rs lines 90-99: The PartialEq::eq implementation for ActiveLeavesUpdate uses self.deactivated.len() == other.deactivated.len() combined with a single-direction containment check (self.deactivated.iter().all(|a| other.deactivated.contains(a))). When duplicate hashes exist, a==b can be true while b==a is false, violating PartialEq symmetry. The doc comment at line 93 claims 'equality when considered as sets,' but the implementation does not correctly handle duplicates.

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_partial_eq_symmetry(a: &ActiveLeavesUpdate, b: &ActiveLeavesUpdate) {
	if a == b {
		assert!(b == a, "PartialEq symmetry violated: a == b but b != a");
	}
	if b == a {
		assert!(a == b, "PartialEq symmetry violated: b == a but a != b");
	}
}

// RWF-RISKS: risk_9zerfujx
// RWF-INVARIANTS: inv_w4biu6tr
#[test]
fn test_risk_9zerfujx_partial_eq_symmetry_duplicates() {
	let h1 = Hash::from_low_u64_be(1);
	let h2 = Hash::from_low_u64_be(2);

	let dup = ActiveLeavesUpdate {
		activated: None,
		deactivated: [h1, h1][..].into(),
	};
	let distinct = ActiveLeavesUpdate {
		activated: None,
		deactivated: [h1, h2][..].into(),
	};

	assert_partial_eq_symmetry(&dup, &distinct);
	assert_partial_eq_symmetry(&distinct, &dup);
}

Reproduction Command

cargo test -p polkadot-node-subsystem-types test_risk_9zerfujx_partial_eq_symmetry_duplicates -- --nocapture

Observed Result

The PoC fails on the current implementation with:

PartialEq symmetry violated: a == b but b != a

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 the hand-written PartialEq comparing deactivated with equal lengths plus one-directional containment. That is set-like only when duplicates are absent. The PoC compares [h1, h1] with [h1, h2]; one direction passes because every element of the duplicate side is present in the distinct side, while the reverse direction fails because h2 is missing.

RWF Metadata

  • Found by Runtime Whitebox Fuzzer
  • Finding: find_q79eteccmu
  • Report: freport_w9krxugtxi
  • Target: polkadot-node-subsystem-types
  • Run: 20260628-212807-a53980
  • Severity: low
  • Category: Correctness — PartialEq symmetry violation
  • Invariants: inv_w4biu6tr
  • Risks: risk_9zerfujx

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