diff --git a/defects/moby/patch/moby-0002-networkdb-node-set.md b/defects/moby/patch/moby-0002-networkdb-node-set.md new file mode 100644 index 000000000..ea18088a3 --- /dev/null +++ b/defects/moby/patch/moby-0002-networkdb-node-set.md @@ -0,0 +1,99 @@ +# Defect: moby-0002 — NetworkDB.networkNodes []string O(N²) node membership scan + +**Project:** moby (Docker Engine) +**File:** `daemon/libnetwork/networkdb/networkdb.go` +**Function:** `addNetworkNode`, `deleteNetworkNode`, `findCommonNetworks` +**Complexity:** O(N) per call where N = nodes per network; O(N²) total across cluster join storm +**Severity:** MEDIUM +**Fix:** Change `networkNodes map[string][]string` → `map[string]map[string]struct{}` + +## Pattern + +```go +// DEFECT: O(N) membership check on every gossip event +func (nDB *NetworkDB) addNetworkNode(nid string, nodeName string) { + if slices.Contains(nDB.networkNodes[nid], nodeName) { // O(N) scan + return + } + nDB.networkNodes[nid] = append(nDB.networkNodes[nid], nodeName) + ... +} + +// DEFECT: O(N) delete — rebuilds entire slice +func (nDB *NetworkDB) deleteNetworkNode(nid string, nodeName string) { + nodes, ok := nDB.networkNodes[nid] + ... + newNodes := make([]string, 0, len(nodes)-1) + for _, name := range nodes { // O(N) scan + if name == nodeName { continue } + newNodes = append(newNodes, name) + } + nDB.networkNodes[nid] = newNodes +} + +// DEFECT: O(N) per-network scan in findCommonNetworks +for nid, nodes := range nDB.networkNodes { + for _, name := range nodes { // O(N) inner loop + if name == nodeName { + networks = append(networks, nid) + break + } + } +} +``` + +## Fix + +```go +// Type change: map[string][]string → map[string]map[string]struct{} +networkNodes map[string]map[string]struct{} + +// addNetworkNode: O(1) +func (nDB *NetworkDB) addNetworkNode(nid string, nodeName string) { + if nDB.networkNodes[nid] == nil { + nDB.networkNodes[nid] = make(map[string]struct{}) + } + nDB.networkNodes[nid][nodeName] = struct{}{} + if n, ok := nDB.thisNodeNetworks[nid]; ok { + n.networkNodes.Store(int32(len(nDB.networkNodes[nid]))) + } +} + +// deleteNetworkNode: O(1) +func (nDB *NetworkDB) deleteNetworkNode(nid string, nodeName string) { + nodes, ok := nDB.networkNodes[nid] + if !ok || len(nodes) == 0 { return } + delete(nDB.networkNodes[nid], nodeName) + if n, ok := nDB.thisNodeNetworks[nid]; ok { + n.networkNodes.Store(int32(len(nDB.networkNodes[nid]))) + } +} + +// findCommonNetworks: O(M) instead of O(N×M) +for nid, nodes := range nDB.networkNodes { + if _, ok := nodes[nodeName]; ok { + networks = append(networks, nid) + } +} + +// Peers() iteration: use maps.Keys() or range map +for nodeName := range nDB.networkNodes[nid] { + if node, ok := nDB.nodes[nodeName]; ok { + peers = append(peers, PeerInfo{...}) + } +} + +// bulkSync call sites: convert set to slice +nodeSlice := make([]string, 0, len(nDB.networkNodes[nid])) +for n := range nDB.networkNodes[nid] { nodeSlice = append(nodeSlice, n) } +if _, err := nDB.bulkSync(nodeSlice, true); err != nil { ... } +``` + +## Impact + +In a Docker Swarm cluster with N nodes and M networks: +- Each node join/leave gossip event calls addNetworkNode/deleteNetworkNode +- Cluster join storm: N nodes × M networks × O(N) scan = O(N²×M) +- Fix: O(1) per event = O(N×M) total + +At N=1000 nodes, M=10 networks: 10,000,000 comparisons → 10,000. **1000× overhead.**