java-topology/defects/starrocks/patch/starrocks-0002-lockmanager-cycle-list-hashmap.md

3 KiB
Raw Permalink Blame History

UNDF: UNDF-2026-000000495

UNDF: (pending)

starrocks-0002: DeadLockChecker.hasCycleInternal — cycle ArrayList O(D²) onStack scan + diamond re-traversal

CWE-407 — Algorithmic Complexity: O(D²) onStack scan in deadlock detection DFS

Field Value
ID starrocks-0002
Severity LOW
Ecosystem starrocks
Package fe/fe-core
File fe/fe-core/src/main/java/com/starrocks/common/util/concurrent/lock/LockManager.java
Lines 560565, 538
Complexity O(D²) onStack scan; O(2^D) re-traversal on shared resource holders
Hot path Deadlock detection — rare but triggered under concurrent load

Defect

DeadLockChecker.hasCycleInternal is a recursive DFS deadlock detector. It maintains cycle, an ArrayList<CycleNode>, as the DFS path ("onStack" set). At each recursion step it scans the entire path to check whether the current locker is already on the path:

// Line 560-565: O(D) scan per recursion step → O(D²) total for chain depth D
for (int i = 0; i < cycle.size(); ++i) {
    if (cycle.get(i).getLocker().equals(locker)) {
        cycle.subList(0, i).clear();
        return true;
    }
}

For a deadlock chain of depth D, this scan runs D + (D-1) + ... + 1 = O(D²) times total. The fix is an O(1) lookup via HashMap<Locker, Integer> mapping locker → cycle index.

Additionally, when a resource is held by multiple lockers (each waiting for another resource), the DFS recurses into each holder without a global "fully-explored" set. A shared resource holder D reachable via two paths (B→D and C→D) is fully re-traversed on the second path after the first path returned false, adding O(2^D) re-traversal risk for diamond wait graphs.

Fix

Replace ArrayList onStack scan with O(1) HashMap lookup, and add fully-explored guard:

// AFTER — O(D) total for chain of depth D
private final Map<Locker, Integer> cycleIndex = new HashMap<>();
private final Set<Locker> explored = new HashSet<>();

for (int i = 0; i < cycle.size(); ++i) {
    if (cycle.get(i).getLocker().equals(locker)) {   // BEFORE: O(D) scan
        // AFTER: cycleIndex.get(locker) — O(1)
        cycle.subList(0, i).clear();
        return true;
    }
}

// After hasCycleInternal returns false for a subtree, mark as explored:
// if (!hasCycleInternal(locker, ...)) { explored.add(locker); }
// At top of hasCycleInternal: if (explored.contains(locker)) return false;

Speedup

Chain depth (D) Before (comparisons) After (comparisons) Speedup
10 55 10 5.5×
50 1,275 50 25.5×
100 5,050 100 50.5×
1,000 500,500 1,000 500×

Growth before: O(D²). Growth after: O(D).