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
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.
Reproduction Command
cargo test -p polkadot-node-subsystem-types test_risk_9zerfujx_partial_eq_symmetry_duplicates -- --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 the hand-written
PartialEqcomparingdeactivatedwith 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 becauseh2is missing.RWF Metadata
find_q79eteccmufreport_w9krxugtxipolkadot-node-subsystem-types20260628-212807-a53980lowCorrectness — PartialEq symmetry violationinv_w4biu6trrisk_9zerfujx