java-topology/defects/moby/patch/moby-0002-networkdb-node-set.md

3.2 KiB
Raw Blame History

UNDF: UNDF-2026-000000688

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][]stringmap[string]map[string]struct{}

Pattern

// 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

// 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.