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
264
defects/solc/unit/SolcCallGraphCycleTest.java
Normal file
264
defects/solc/unit/SolcCallGraphCycleTest.java
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue