diff --git a/defects/go-libp2p-0001/patch/go-libp2p-0001.patch b/defects/go-libp2p-0001/patch/go-libp2p-0001.patch new file mode 100644 index 000000000..12bae53e2 --- /dev/null +++ b/defects/go-libp2p-0001/patch/go-libp2p-0001.patch @@ -0,0 +1,34 @@ +--- a/p2p/protocol/identify/id.go ++++ b/p2p/protocol/identify/id.go +@@ -698,22 +698,18 @@ func (ids *idService) getRecordBytes() []byte { + + // diff takes two slices of strings (a and b) and computes which elements were added and removed in b + func diff(a, b []protocol.ID) (added, removed []protocol.ID) { +- // This is O(n^2), but it's fine because the slices are small. +- for _, x := range b { +- var found bool +- if slices.Contains(a, x) { +- found = true +- } +- if !found { ++ aSet := make(map[protocol.ID]struct{}, len(a)) ++ for _, x := range a { ++ aSet[x] = struct{}{} ++ } ++ bSet := make(map[protocol.ID]struct{}, len(b)) ++ for _, x := range b { ++ bSet[x] = struct{}{} ++ if _, ok := aSet[x]; !ok { + added = append(added, x) + } + } + for _, x := range a { +- var found bool +- if slices.Contains(b, x) { +- found = true +- } +- if !found { ++ if _, ok := bSet[x]; !ok { + removed = append(removed, x) + } + } diff --git a/defects/go-libp2p-0001/test/go_libp2p_0001_test.py b/defects/go-libp2p-0001/test/go_libp2p_0001_test.py new file mode 100644 index 000000000..74fbd24e3 --- /dev/null +++ b/defects/go-libp2p-0001/test/go_libp2p_0001_test.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Unit test for go-libp2p-0001: identify protocol diff() uses +slices.Contains inside loop = O(N^2) protocol comparison. +Fix: use map[protocol.ID]struct{} for O(1) lookups = O(N) total. + +File: p2p/protocol/identify/id.go +Function: diff(a, b []protocol.ID) (added, removed []protocol.ID) +""" + +import time +import unittest + + +# --- DEFECTIVE VERSION: O(N^2) --- + +def diff_defective(a, b): + """Original: slices.Contains(a, x) inside for-range b loop.""" + added = [] + removed = [] + for x in b: + if x not in a: # linear scan = O(N) per element + added.append(x) + for x in a: + if x not in b: # linear scan = O(N) per element + removed.append(x) + return added, removed + + +# --- PATCHED VERSION: O(N) --- + +def diff_patched(a, b): + """Patched: build sets for O(1) lookups.""" + a_set = set(a) + b_set = set(b) + added = [x for x in b if x not in a_set] + removed = [x for x in a if x not in b_set] + return added, removed + + +class TestDiffCorrectness(unittest.TestCase): + """Verify patched version matches defective version output.""" + + def _check(self, a, b, want_add, want_rem): + add_d, rem_d = diff_defective(a, b) + add_p, rem_p = diff_patched(a, b) + self.assertEqual(add_d, add_p, "added mismatch between versions") + self.assertEqual(rem_d, rem_p, "removed mismatch between versions") + self.assertEqual(add_p, want_add) + self.assertEqual(rem_p, want_rem) + + def test_no_change(self): + self._check( + ["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"], + ["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"], + [], [] + ) + + def test_one_added(self): + self._check( + ["/ipfs/id/1.0.0"], + ["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"], + ["/ipfs/ping/1.0.0"], [] + ) + + def test_one_removed(self): + self._check( + ["/ipfs/id/1.0.0", "/ipfs/ping/1.0.0"], + ["/ipfs/id/1.0.0"], + [], ["/ipfs/ping/1.0.0"] + ) + + def test_both_added_and_removed(self): + self._check( + ["/proto/a", "/proto/b"], + ["/proto/b", "/proto/c"], + ["/proto/c"], ["/proto/a"] + ) + + def test_empty_to_many(self): + self._check( + [], + ["/p/1", "/p/2", "/p/3"], + ["/p/1", "/p/2", "/p/3"], [] + ) + + def test_many_to_empty(self): + self._check( + ["/p/1", "/p/2", "/p/3"], + [], + [], ["/p/1", "/p/2", "/p/3"] + ) + + +class TestDiffPerformance(unittest.TestCase): + """Benchmark: patched should be significantly faster at scale.""" + + def test_performance_scaling(self): + sizes = [10, 100, 500, 1000] + for n in sizes: + # Build two protocol lists with ~50% overlap + a = [f"/proto/{i}" for i in range(n)] + b = [f"/proto/{i}" for i in range(n // 2, n + n // 2)] + + iters = 200 + + # Benchmark defective (list-based O(N^2)) + start = time.perf_counter() + for _ in range(iters): + diff_defective(a, b) + defective_time = time.perf_counter() - start + + # Benchmark patched (set-based O(N)) + start = time.perf_counter() + for _ in range(iters): + diff_patched(a, b) + patched_time = time.perf_counter() - start + + ratio = defective_time / patched_time if patched_time > 0 else float('inf') + print(f" N={n:4d} defective={defective_time:.4f}s patched={patched_time:.4f}s ratio={ratio:.1f}x") + + if n >= 100: + self.assertGreater(ratio, 2.0, + f"N={n}: expected patched to be at least 2x faster, got {ratio:.1f}x") + + +if __name__ == "__main__": + unittest.main() diff --git a/defects/kubo/CLEAN b/defects/kubo/CLEAN new file mode 100644 index 000000000..d495ab1dc --- /dev/null +++ b/defects/kubo/CLEAN @@ -0,0 +1,29 @@ +kubo (Go IPFS implementation) — CLEAN across all 5 MOADs + +Scanned: 2026-03-31 +Source: ~/git/kubo (depth=1 clone from https://github.com/ipfs/kubo) +Files: 520 Go source files + +MOAD-0001 (CWE-407): CLEAN + - cid.Set (hash-based) used for GC mark set, pin dedup, refs visited + - map[string]struct{} used for bootstrap dedup, address filter dedup, announce dedup + - map[string]int used for refs seen tracking + - slices.Contains only on fixed-size config lists (5 method names, 4 container inits) + - Nested loop in filtersRemove (swarm.go) is one-shot CLI config command, trivially small N + +MOAD-0002 (Intertangle): No patchable defect + - IpfsNode is a large god object (30+ fields) but architectural, uses fx DI + +MOAD-0003 (Leaked Context): CLEAN + - No context.WithValue abuse found + - Uses fx dependency injection for component wiring + +MOAD-0004 (Logged Secret): CLEAN + - Swarm key fingerprint (hash) logged, not key itself + - API keys passed but never logged + - HTTPHeaders logged contain only CORS config + +MOAD-0005 (Thundering Herd): CLEAN + - sync.Once for autoconf client singleton + - fx dependency injection for initialization + - No unsynchronized cache patterns