java-topology/defects/solc/unit/SolcCallGraphCycleTest.java
russell@unturf.com 0a580b313d 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.
2026-03-26 17:11:57 -04:00

264 lines
11 KiB
Java
Raw 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.

package unit;
import java.util.*;
/**
* Unit test for solc-0001: CallGraphCycleFinder CWE-407.
*
* Defect: CallGraphCycleFinder.visit() uses std::find on currentPath (a vector)
* to detect whether a function is already on the DFS stack. That is an O(D)
* membership test performed once per edge visited. With F functions each calling
* D others, total comparisons are O(F × D²).
*
* File: libyul/optimiser/CallGraphGenerator.cpp:49
* Symbol: CallGraphCycleFinder::visit — `std::find(currentPath.begin(), currentPath.end(), _function)`
*
* Fix: Carry a parallel std::set<FunctionHandle> (currentPathSet) alongside
* the currentPath vector. The set gives O(log D) membership, effectively
* O(1) compared to O(D) at practical depths.
*
* Modeled here in Java:
* - YulString ≡ String (function name)
* - currentPath ≡ List<String>
* - currentPathSet ≡ Set<String>
* - comparisons counted at the membership-test site
*
* Expected at depth=32, functions=32:
* defective ≈ F × D×(D+1)/2 = 32 × 528 = 16896
* fixed ≈ F × D = 32 × 32 = 1024
* ratio ≈ 16.5×
*/
public class SolcCallGraphCycleTest {
// ── Defective: linear scan of currentPath for cycle detection ─────────────
static class DefectiveCycleFinder {
final Map<String, List<String>> callGraph;
final Set<String> containedInCycle = new HashSet<>();
final Set<String> visited = new HashSet<>();
final List<String> currentPath = new ArrayList<>();
long comparisons = 0;
DefectiveCycleFinder(Map<String, List<String>> callGraph) {
this.callGraph = callGraph;
}
void visit(String function) {
if (visited.contains(function)) return;
// O(D) linear scan — the defect
int cycleStart = -1;
for (int i = 0; i < currentPath.size(); i++) {
comparisons++;
if (currentPath.get(i).equals(function)) {
cycleStart = i;
break;
}
}
if (cycleStart >= 0) {
for (int i = cycleStart; i < currentPath.size(); i++)
containedInCycle.add(currentPath.get(i));
} else {
currentPath.add(function);
List<String> callees = callGraph.getOrDefault(function, Collections.emptyList());
for (String child : callees)
visit(child);
currentPath.remove(currentPath.size() - 1);
visited.add(function);
}
}
}
// ── Fixed: parallel HashSet for O(1) membership test ─────────────────────
static class FixedCycleFinder {
final Map<String, List<String>> callGraph;
final Set<String> containedInCycle = new HashSet<>();
final Set<String> visited = new HashSet<>();
final List<String> currentPath = new ArrayList<>();
final Set<String> currentPathSet = new HashSet<>();
long comparisons = 0;
FixedCycleFinder(Map<String, List<String>> callGraph) {
this.callGraph = callGraph;
}
void visit(String function) {
if (visited.contains(function)) return;
// O(1) hash lookup — the fix
comparisons++;
if (currentPathSet.contains(function)) {
// still need to walk currentPath to collect cycle members
boolean inCycle = false;
for (String f : currentPath) {
if (f.equals(function)) inCycle = true;
if (inCycle) containedInCycle.add(f);
}
} else {
currentPath.add(function);
currentPathSet.add(function);
List<String> callees = callGraph.getOrDefault(function, Collections.emptyList());
for (String child : callees)
visit(child);
currentPath.remove(currentPath.size() - 1);
currentPathSet.remove(function);
visited.add(function);
}
}
}
// ── Graph builders ────────────────────────────────────────────────────────
/**
* Build a linear chain: f0→f1→f2→…→f(D-1), no cycles.
* Each function is visited once; the path grows to depth D.
* Defective finder scans 0+1+2+…+(D-1) = D*(D-1)/2 times per function.
* With F independent chains total defective comparisons ≈ F * D*(D-1)/2.
*/
public static Map<String, List<String>> chainGraph(int F, int D) {
Map<String, List<String>> g = new LinkedHashMap<>();
for (int f = 0; f < F; f++) {
String prefix = "fn" + f + "_";
for (int d = 0; d < D - 1; d++) {
g.computeIfAbsent(prefix + d, k -> new ArrayList<>())
.add(prefix + (d + 1));
}
g.computeIfAbsent(prefix + (D - 1), k -> new ArrayList<>());
}
return g;
}
/**
* Build a star graph: root calls all F leaf functions.
* Path depth is 2 for each leaf. Membership test on each leaf sees
* currentPath = [root] → 1 comparison (defective) vs O(1) (fixed).
* This isolates the per-visit cost pattern.
*/
static Map<String, List<String>> starGraph(int F) {
Map<String, List<String>> g = new LinkedHashMap<>();
List<String> children = new ArrayList<>();
for (int i = 0; i < F; i++) {
String leaf = "leaf_" + i;
children.add(leaf);
g.put(leaf, Collections.emptyList());
}
g.put("root", children);
return g;
}
// ── Simulation helpers ────────────────────────────────────────────────────
public static long simulateDefective(Map<String, List<String>> g) {
DefectiveCycleFinder finder = new DefectiveCycleFinder(g);
for (String fn : g.keySet()) finder.visit(fn);
return finder.comparisons;
}
public static long simulateFixed(Map<String, List<String>> g) {
FixedCycleFinder finder = new FixedCycleFinder(g);
for (String fn : g.keySet()) finder.visit(fn);
return finder.comparisons;
}
// ── Tests ─────────────────────────────────────────────────────────────────
static void testCorrectnessNoCycle() {
Map<String, List<String>> g = chainGraph(4, 5);
DefectiveCycleFinder def = new DefectiveCycleFinder(g);
FixedCycleFinder fix = new FixedCycleFinder(g);
for (String fn : g.keySet()) { def.visit(fn); fix.visit(fn); }
assert def.containedInCycle.equals(fix.containedInCycle)
: "cycle sets differ (no-cycle graph)";
assert def.containedInCycle.isEmpty()
: "expected no cycles in chain graph";
System.out.println("PASS testCorrectnessNoCycle");
}
static void testCorrectnessCycleDetection() {
// f0 → f1 → f2 → f0 (cycle), f3 standalone
Map<String, List<String>> g = new LinkedHashMap<>();
g.put("f0", Arrays.asList("f1"));
g.put("f1", Arrays.asList("f2"));
g.put("f2", Arrays.asList("f0"));
g.put("f3", Collections.emptyList());
DefectiveCycleFinder def = new DefectiveCycleFinder(g);
FixedCycleFinder fix = new FixedCycleFinder(g);
for (String fn : g.keySet()) { def.visit(fn); fix.visit(fn); }
assert def.containedInCycle.containsAll(Arrays.asList("f0", "f1", "f2"))
: "defective missed cycle members: " + def.containedInCycle;
assert fix.containedInCycle.containsAll(Arrays.asList("f0", "f1", "f2"))
: "fixed missed cycle members: " + fix.containedInCycle;
assert !def.containedInCycle.contains("f3")
: "defective falsely included f3";
assert !fix.containedInCycle.contains("f3")
: "fixed falsely included f3";
assert def.containedInCycle.equals(fix.containedInCycle)
: "cycle sets differ between defective and fixed";
System.out.println("PASS testCorrectnessCycleDetection");
}
static void testDefectiveGrowsQuadratically() {
// Comparison count in chain graph grows quadratically with depth D
long prev = -1;
for (int D : new int[]{8, 16, 32}) {
long c = simulateDefective(chainGraph(1, D));
if (prev > 0) {
double ratio = (double) c / prev;
assert ratio > 2.5
: "defective should grow >2.5x when D doubles; got " + ratio + " at D=" + D;
}
prev = c;
}
System.out.println("PASS testDefectiveGrowsQuadratically");
}
static void testFixedGrowsLinearly() {
long prev = -1;
for (int D : new int[]{8, 16, 32}) {
long c = simulateFixed(chainGraph(1, D));
if (prev > 0) {
double ratio = (double) c / prev;
assert ratio < 2.3
: "fixed should grow ~2x when D doubles; got " + ratio + " at D=" + D;
}
prev = c;
}
System.out.println("PASS testFixedGrowsLinearly");
}
static void testRatioAtScale() {
// F=32 independent chains of depth D=32
// defective: each of the 32 chains accumulates 0+1+…+31 = 496 comparisons → 32*496 = 15872
// fixed: each chain accumulates 32 comparisons → 32*32 = 1024
// The spec says ≈16896 (uses D*(D+1)/2) and 1024; ratio ≈16.5×
int F = 32, D = 32;
Map<String, List<String>> g = chainGraph(F, D);
long defComp = simulateDefective(g);
long fixComp = simulateFixed(g);
double ratio = (double) defComp / fixComp;
assert defComp >= 15000 && defComp <= 18000
: "defective comparisons out of expected range: " + defComp;
assert fixComp == (long) F * D
: "fixed comparisons should be F*D=" + (F * D) + "; got " + fixComp;
assert ratio > 10.0
: "ratio should be >10x at F=32,D=32; got " + ratio;
System.out.printf(
"PASS testRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n",
defComp, fixComp, ratio);
}
public static void main(String[] args) {
testCorrectnessNoCycle();
testCorrectnessCycleDetection();
testDefectiveGrowsQuadratically();
testFixedGrowsLinearly();
testRatioAtScale();
System.out.println("All solc-0001 tests passed.");
}
}