java-topology/defects/dgraph/patch/dgraph-0001-shortest-path-route-indexOf.md

2.9 KiB
Raw Blame History

UNDF: UNDF-2026-000000377

dgraph-0001: CWE-407 O(P) route cycle-check inside hot BFS/Dijkstra neighbour loop

Severity: HIGH File: query/shortest.go Function: runKShortestPaths (line 286) Lines: 5663 (indexOf), 380 (call site) Repo: https://github.com/dgraph-io/dgraph

Description

runKShortestPaths implements Yen's k-shortest-paths algorithm using a priority queue over queueItem nodes. Each queueItem carries a route — a slice of pathInfo structs representing the path taken to reach the current node.

Inside the main dequeue loop (line 325) there is an inner range over all neighbours (line 373). For each neighbour, the cycle-detection check at line 380 calls item.path.indexOf(toUid):

// query/shortest.go:380
if len(*item.path.route) > 0 && item.path.indexOf(toUid) != -1 {
    continue
}

indexOf is a linear scan of the path slice:

// query/shortest.go:5663
func (r *route) indexOf(uid uint64) int {
    for i, val := range *r.route {
        if val.uid == uid {
            return i
        }
    }
    return -1
}

Complexity

Symbol Meaning
V nodes (UIDs) in graph
E edges (neighbours) explored
P path length at dequeue time (up to V)
K number of shortest paths requested

Each dequeue processes up to E/V neighbours. Each neighbour call to indexOf is O(P). In the worst case P → V. The total cost is:

O(K * V * (E/V) * V) = O(K * E * V)

For a dense graph (E ≈ V²) and large K this degrades to O(K * V³). Even for sparse graphs (E ≈ V) it is O(K * V²), whereas the expected complexity of Yen's algorithm with O(1) cycle detection is O(K * V * log V).

Slow path

Every call to indexOf inside the neighbour loop is a full O(P) scan of the current path slice. The path grows with each hop, so later queue items pay a higher per-neighbour cost.

Fast path

Replace the *[]pathInfo path representation with a map[uint64]struct{} visited set carried alongside the route slice. indexOf (used only for cycle detection) becomes an O(1) map lookup; path reconstruction continues to use the slice for ordered output.

Fix sketch

type route struct {
    route       *[]pathInfo
    visited     map[uint64]struct{}   // NEW: uid → present
    totalWeight float64
}

// cycle check becomes:
if _, ok := item.path.visited[toUid]; ok {
    continue
}

When copying a route for a new branch, shallow-copy the visited map (one allocation + N inserts where N = current path length — paid once per branch, not per neighbour).

Impact

Any Dgraph query using shortest with numpaths > 1 on a graph with cycles and long paths degrades from O(K * E * log V) to O(K * E * V). At V = 10,000 nodes and K = 10 paths this is a 10,000× slower cycle check per edge expansion. Clients experience query timeouts on graphs that should complete in milliseconds.