java-topology/docs/tickets/0003-module-hasher-topo-deque-contains.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

91 lines
3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 0003 — ModuleHashesBuilder$TopoSorter: O(N) Deque.contains() for cycle detection
**Status:** open
**Severity:** performance — medium
**Component:** `java.base / jdk.internal.module.ModuleHashesBuilder$TopoSorter`
**Module:** `java.base` — ships in every JDK and JRE
---
## Root Cause
`ModuleHashesBuilder.java` (inner class `TopoSorter.visit()`), bytecode instruction 57:
```
invokeinterface java/util/Deque.contains:(Ljava/lang/Object;)Z
```
The `visit()` method uses an `ArrayDeque` as a DFS stack and calls `Deque.contains()` to detect back edges (cycles in the module dependency graph). `ArrayDeque.contains()` is a linear scan — O(N).
This is the same structural defect as ticket 0001 (`GraphUtils.Tarjan`) in a different module and a different layer of the stack.
Source pattern (reconstructed from bytecode):
```java
private void visit(T node, Set<T> visited, Deque<T> stack) {
if (visited.contains(node)) {
if (stack.contains(node)) { // O(N) — THE DEFECT
throw new IllegalArgumentException("Cycle detected: " + node + " -> " + children(node));
}
return;
}
visited.add(node);
stack.push(node);
children(node).forEach(child -> visit(child, visited, stack));
stack.pop();
result.addLast(node);
}
```
**Fix — one boolean field per node, or a `HashSet<T> onStack`:**
```java
Set<T> onStack = new HashSet<>(); // O(1) contains
private void visit(T node, Set<T> visited, Deque<T> stack, Set<T> onStack) {
if (visited.contains(node)) {
if (onStack.contains(node)) { // O(1)
throw new IllegalArgumentException("Cycle: " + node);
}
return;
}
visited.add(node);
stack.push(node);
onStack.add(node); // O(1)
children(node).forEach(...);
stack.pop();
onStack.remove(node); // O(1)
result.addLast(node);
}
```
---
## Where This Fires
`ModuleHashesBuilder` is called by `jlink` during custom runtime image creation:
```
jlink → ModuleHashesBuilder.computeHashes() → new TopoSorter(graph) → visit()
```
The module dependency graph fed to this sorter is the full transitive closure of the modules included in the image. For a typical server JDK image with 2050 modules, this is manageable. For large multi-module applications using `jlink` with 100+ modules, the O(N²) degradation is measurable.
---
## Significance
This defect lives in `java.base` — the lowest-level module present in every JDK/JRE. The same pattern (`Deque.contains()` for on-stack check) replicated here independently of `GraphUtils.Tarjan` shows this is a **systemic pattern** in the JDK codebase, not an isolated incident.
---
## Complexity
| V (modules) | Defective contains() calls | Fixed contains() calls |
|---|---|---|
| 20 | ≤ 190 | ≤ 20 |
| 50 | ≤ 1,225 | ≤ 50 |
| 100 | ≤ 4,950 | ≤ 100 |
For small module graphs, impact is negligible. For large `jlink` builds or module graphs
with deep dependency chains, this compounds with other O(N²) patterns.