undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
244
tests/support/TarjanAlgorithm.java
Normal file
244
tests/support/TarjanAlgorithm.java
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
package support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Self-contained Tarjan SCC implementations — defective and fixed.
|
||||
*
|
||||
* These are verbatim ports of GraphUtils.Tarjan (the private inner class)
|
||||
* with one difference: the defective version uses list.contains(n) and the
|
||||
* fixed version uses n.onStack. An operation counter is exposed so tests can
|
||||
* assert exact comparison counts without relying on timing.
|
||||
*
|
||||
* This class has no dependency on jdk.compiler internals.
|
||||
*
|
||||
* KEY TOPOLOGY — starWithBackEdges(V):
|
||||
* N0 → N1, N0 → N2, ..., N0 → N(v-1) (tree edges from N0)
|
||||
* N_k → N0 for k=1..v-1 (back edges to already-visited N0)
|
||||
*
|
||||
* DFS visits N0 first, then each N_k in order. N0 is never popped until
|
||||
* all children finish (lowlink=0 != index=k for each child). So when N_k
|
||||
* fires its back edge to N0, the stack is [N_k, N_(k-1), ..., N1, N0]:
|
||||
* - defective: listContains scans k+1 elements to find N0 at the bottom
|
||||
* - fixed: n.onStack read in 1 operation
|
||||
*
|
||||
* Exact comparison counts:
|
||||
* defective: 2+3+...+V = V*(V+1)/2 - 1
|
||||
* fixed: 1*(V-1) = V-1
|
||||
*
|
||||
* Growth when V doubles: defective ≈4x (quadratic), fixed ≈2x (linear).
|
||||
*/
|
||||
public class TarjanAlgorithm {
|
||||
|
||||
// ─── Shared node base ───────────────────────────────────────────────────
|
||||
|
||||
public static class Node {
|
||||
public final String label;
|
||||
public final List<Node> deps = new ArrayList<>();
|
||||
|
||||
// Tarjan bookkeeping
|
||||
int index = -1;
|
||||
int lowlink = -1;
|
||||
boolean onStack = false; // used by FIXED version
|
||||
|
||||
public Node(String label) { this.label = label; }
|
||||
public void addDep(Node dep) { deps.add(dep); }
|
||||
@Override public String toString() { return label; }
|
||||
}
|
||||
|
||||
// ─── Result ─────────────────────────────────────────────────────────────
|
||||
|
||||
public static class Result {
|
||||
public final List<List<Node>> sccs;
|
||||
/**
|
||||
* Number of element-level comparisons made during stack membership checks.
|
||||
* Defective: each call to listContains() walks the list until it finds the target.
|
||||
* Fixed: each call to n.onStack counts as exactly 1 comparison.
|
||||
*/
|
||||
public final long comparisons;
|
||||
|
||||
Result(List<List<Node>> sccs, long comparisons) {
|
||||
this.sccs = sccs;
|
||||
this.comparisons = comparisons;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── DEFECTIVE implementation ───────────────────────────────────────────
|
||||
// Mirrors GraphUtils.java:186 — uses list.contains(n) for on-stack check.
|
||||
// We count each element-level comparison inside the linear scan.
|
||||
|
||||
public static Result tarjanDefective(List<Node> nodes) {
|
||||
return new DefectiveTarjan().run(nodes);
|
||||
}
|
||||
|
||||
private static class DefectiveTarjan {
|
||||
int index = 0;
|
||||
long comparisons = 0;
|
||||
final List<Node> stack = new ArrayList<>();
|
||||
final List<List<Node>> sccs = new ArrayList<>();
|
||||
|
||||
Result run(List<Node> nodes) {
|
||||
for (Node n : nodes) {
|
||||
if (n.index == -1) visit(n);
|
||||
}
|
||||
return new Result(sccs, comparisons);
|
||||
}
|
||||
|
||||
void visit(Node v) {
|
||||
v.index = index;
|
||||
v.lowlink = index;
|
||||
index++;
|
||||
stack.add(0, v); // prepend — matches ListBuffer.prepend()
|
||||
|
||||
for (Node n : v.deps) {
|
||||
if (n.index == -1) {
|
||||
visit(n);
|
||||
v.lowlink = Math.min(v.lowlink, n.lowlink);
|
||||
} else {
|
||||
// DEFECT (GraphUtils.java:186): linear scan through the stack
|
||||
if (listContains(stack, n)) {
|
||||
v.lowlink = Math.min(v.lowlink, n.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v.lowlink == v.index) {
|
||||
List<Node> scc = new ArrayList<>();
|
||||
Node n;
|
||||
do {
|
||||
n = stack.remove(0);
|
||||
n.onStack = false;
|
||||
scc.add(n);
|
||||
} while (n != v);
|
||||
sccs.add(scc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts every element comparison made during the linear scan.
|
||||
* This is what ListBuffer.contains() does internally.
|
||||
*/
|
||||
private boolean listContains(List<Node> list, Node target) {
|
||||
for (Node n : list) {
|
||||
comparisons++; // each element access = one comparison
|
||||
if (n == target) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FIXED implementation ────────────────────────────────────────────────
|
||||
// Uses n.onStack for O(1) membership check — the correct approach.
|
||||
// Exactly one comparison per non-tree edge: read n.onStack.
|
||||
|
||||
public static Result tarjanFixed(List<Node> nodes) {
|
||||
return new FixedTarjan().run(nodes);
|
||||
}
|
||||
|
||||
private static class FixedTarjan {
|
||||
int index = 0;
|
||||
long comparisons = 0;
|
||||
final List<Node> stack = new ArrayList<>();
|
||||
final List<List<Node>> sccs = new ArrayList<>();
|
||||
|
||||
Result run(List<Node> nodes) {
|
||||
for (Node n : nodes) {
|
||||
if (n.index == -1) visit(n);
|
||||
}
|
||||
return new Result(sccs, comparisons);
|
||||
}
|
||||
|
||||
void visit(Node v) {
|
||||
v.index = index;
|
||||
v.lowlink = index;
|
||||
index++;
|
||||
stack.add(0, v);
|
||||
v.onStack = true;
|
||||
|
||||
for (Node n : v.deps) {
|
||||
if (n.index == -1) {
|
||||
visit(n);
|
||||
v.lowlink = Math.min(v.lowlink, n.lowlink);
|
||||
} else {
|
||||
comparisons++; // exactly one operation: read n.onStack
|
||||
if (n.onStack) {
|
||||
v.lowlink = Math.min(v.lowlink, n.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v.lowlink == v.index) {
|
||||
List<Node> scc = new ArrayList<>();
|
||||
Node n;
|
||||
do {
|
||||
n = stack.remove(0);
|
||||
n.onStack = false;
|
||||
scc.add(n);
|
||||
} while (n != v);
|
||||
sccs.add(scc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Graph factory ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Star-with-back-edges graph on V nodes.
|
||||
*
|
||||
* Edges:
|
||||
* Tree: N0→N1, N0→N2, ..., N0→N(v-1)
|
||||
* Back: N_k→N0 for k=1..v-1
|
||||
*
|
||||
* This is ONE large SCC (all nodes reachable from N0 and back).
|
||||
*
|
||||
* Exact defective comparisons: V*(V+1)/2 - 1 (quadratic in V)
|
||||
* Exact fixed comparisons: V-1 (linear in V)
|
||||
*
|
||||
* Why defective is quadratic: N0 is pushed first and stays on the stack
|
||||
* until all children finish. When N_k fires its back edge N_k→N0, the
|
||||
* stack is [N_k, N_(k-1), ..., N1, N0]. listContains must scan all k+1
|
||||
* elements to find N0 at the bottom. Total: sum(k+1, k=1..V-1) = V*(V+1)/2 - 1.
|
||||
*/
|
||||
public static List<Node> starWithBackEdges(int v) {
|
||||
List<Node> nodes = new ArrayList<>();
|
||||
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
||||
// tree edges N0 → N1..N(v-1)
|
||||
for (int k = 1; k < v; k++) nodes.get(0).addDep(nodes.get(k));
|
||||
// back edges N_k → N0
|
||||
for (int k = 1; k < v; k++) nodes.get(k).addDep(nodes.get(0));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple cycle (path with one back edge): N0→N1→…→N(v-1)→N0
|
||||
*
|
||||
* ONE SCC of size V. Only ONE non-tree edge fires (the back edge N(v-1)→N0).
|
||||
*
|
||||
* Defective comparisons: V (scan entire stack to find N0 at bottom)
|
||||
* Fixed comparisons: 1 (read N0.onStack)
|
||||
*
|
||||
* Demonstrates O(V) vs O(1) per back-edge, not the full O(V²) pattern.
|
||||
* Use starWithBackEdges() for the O(V²) proof.
|
||||
*/
|
||||
public static List<Node> pathWithBackEdge(int v) {
|
||||
List<Node> nodes = new ArrayList<>();
|
||||
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
||||
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
|
||||
nodes.get(v - 1).addDep(nodes.get(0));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear path with no cycles: N0→N1→…→N(v-1)
|
||||
*
|
||||
* V singleton SCCs. No back edges → the defective `else` branch never fires.
|
||||
* comparisons = 0 for both versions. Used for correctness checks only.
|
||||
*/
|
||||
public static List<Node> linearPath(int v) {
|
||||
List<Node> nodes = new ArrayList<>();
|
||||
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
||||
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue