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.
This commit is contained in:
russell@unturf.com 2026-03-26 19:48:18 -04:00
parent 0a580b313d
commit db29a08762
1311 changed files with 371202 additions and 1188 deletions

View file

@ -0,0 +1,373 @@
package unit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* MavenGraphBuilderTest
*
* Models two CWE-407 defects in Apache Maven:
*
* maven-0004: DefaultGraphBuilder (trimProjectsToRequest / trimSelectedProjects /
* includeAlsoMakeTransitively)
* Defective: result.sort(comparing(sortedProjects::indexOf)) — each indexOf call
* is O(N) → sort comparator is O(N) → total sort is O(N² log N).
* Fixed: build an index Map once in O(N); each comparator call is O(1)
* → sort is O(N log N).
*
* maven-0005: BuildPlanLogger
* Defective: stream sorted with Comparator.comparingInt(plan.sortedNodes()::indexOf)
* — sortedNodes() called once per comparison; indexOf is O(N) per call
* → O(N²) total for sorting N steps.
* Fixed: build an IdentityHashMap<BuildStep, Integer> index once in O(N);
* comparator uses indexMap.getOrDefault(step, MAX_VALUE) → O(1) per call.
*
* Operation counts are instrumented explicitly — no wall-clock timing — to isolate
* the algorithmic difference.
*/
public class MavenGraphBuilderTest {
// =========================================================================
// Models for maven-0004: sortedProjects.indexOf() inside sort comparator
// =========================================================================
/**
* Simulates a single sort using indexOf-based comparator.
*
* During Java's TimSort, a list of size N is sorted using O(N log N) comparisons.
* Each comparison calls indexOf on a list of size N → O(N) per comparison.
* Total: O(N² log N) indexOf probes.
*
* We instrument by counting the number of indexOf-equivalent scans that would
* be needed. We run an actual sort and count how many comparator invocations
* occur, then multiply by the average scan length (N/2 on average for a random
* hit, N for a miss — we use N to represent worst-case indexOf cost).
*
* @param n size of the list to sort
* @return total element comparisons (simulated indexOf cost)
*/
static long defectiveSortIndexOfCost(int n) {
// Build sortedProjects list (reference ordering)
List<Integer> sortedProjects = new ArrayList<>(n);
for (int i = 0; i < n; i++) sortedProjects.add(i);
// Build result list in reverse order (worst case for sort)
List<Integer> result = new ArrayList<>(n);
for (int i = n - 1; i >= 0; i--) result.add(i);
// Defective comparator: O(N) indexOf per invocation
long[] comparisons = {0L};
result.sort((a, b) -> {
// Each indexOf call scans up to N elements
int ia = 0;
for (int i = 0; i < sortedProjects.size(); i++) {
comparisons[0]++;
if (sortedProjects.get(i).equals(a)) { ia = i; break; }
}
int ib = 0;
for (int i = 0; i < sortedProjects.size(); i++) {
comparisons[0]++;
if (sortedProjects.get(i).equals(b)) { ib = i; break; }
}
return Integer.compare(ia, ib);
});
return comparisons[0];
}
/**
* Simulates the fixed sort using a pre-built index map.
*
* Build an orderMap in O(N) once; comparator does O(1) map.get().
* We count map lookups (each is O(1)).
*
* @param n size of the list to sort
* @return total map lookups (one pair per comparator invocation)
*/
static long fixedSortIndexMapCost(int n) {
// Build sortedProjects list
List<Integer> sortedProjects = new ArrayList<>(n);
for (int i = 0; i < n; i++) sortedProjects.add(i);
// CWE-407 fix: build orderMap in O(N)
Map<Integer, Integer> orderMap = new HashMap<>(n * 2);
for (int i = 0; i < n; i++) {
orderMap.put(sortedProjects.get(i), i);
}
// Build result list in reverse order
List<Integer> result = new ArrayList<>(n);
for (int i = n - 1; i >= 0; i--) result.add(i);
long[] lookups = {0L};
result.sort((a, b) -> {
lookups[0] += 2; // two O(1) map.get() calls per comparison
return Integer.compare(orderMap.get(a), orderMap.get(b));
});
return lookups[0];
}
// =========================================================================
// Models for maven-0005: BuildPlanLogger sortedNodes().indexOf() in stream sort
// =========================================================================
/**
* Simulates the defective BuildPlanLogger sort:
* .sorted(Comparator.comparingInt(plan.sortedNodes()::indexOf))
*
* sortedNodes() returns a list of N BuildStep objects.
* indexOf is O(N) per comparison.
* Sorting M steps using this comparator costs O(M log M) comparisons × O(N) each.
* When M ≈ N this is O(N² log N).
*
* We model steps as Integers and sortedNodes as a List<Integer>.
* We count individual element comparisons inside the indexOf simulation.
*
* @param nodeCount total nodes in the plan (N)
* @param stepCount steps being sorted for one project (M, typically ≤ N)
* @return total element comparisons (simulated indexOf cost)
*/
static long defectiveBuildPlanLoggerCost(int nodeCount, int stepCount) {
// sortedNodes: list of node ids 0..nodeCount-1
List<Integer> sortedNodes = new ArrayList<>(nodeCount);
for (int i = 0; i < nodeCount; i++) sortedNodes.add(i);
// steps for this project: every other node, in reverse order (stress sort)
List<Integer> steps = new ArrayList<>(stepCount);
for (int i = stepCount - 1; i >= 0; i--) steps.add(i * (nodeCount / stepCount));
long[] comparisons = {0L};
steps.sort((a, b) -> {
// Defect: O(N) indexOf per call — simulated here with explicit scan
int ia = 0;
for (int i = 0; i < sortedNodes.size(); i++) {
comparisons[0]++;
if (sortedNodes.get(i).equals(a)) { ia = i; break; }
}
int ib = 0;
for (int i = 0; i < sortedNodes.size(); i++) {
comparisons[0]++;
if (sortedNodes.get(i).equals(b)) { ib = i; break; }
}
return Integer.compare(ia, ib);
});
return comparisons[0];
}
/**
* Simulates the fixed BuildPlanLogger sort:
* build indexMap once, sort with indexMap.getOrDefault(step, MAX_VALUE).
*
* @param nodeCount total nodes in the plan (N)
* @param stepCount steps being sorted (M)
* @return total map lookups (two per comparator invocation)
*/
static long fixedBuildPlanLoggerCost(int nodeCount, int stepCount) {
// sortedNodes
List<Integer> sortedNodes = new ArrayList<>(nodeCount);
for (int i = 0; i < nodeCount; i++) sortedNodes.add(i);
// CWE-407 fix: build indexMap using IdentityHashMap equivalent (HashMap here
// since Integer objects are pooled for small values; semantics are identical)
Map<Integer, Integer> indexMap = new IdentityHashMap<>(nodeCount * 2);
for (int i = 0; i < nodeCount; i++) {
indexMap.put(sortedNodes.get(i), i);
}
// steps (same as defective version)
List<Integer> steps = new ArrayList<>(stepCount);
for (int i = stepCount - 1; i >= 0; i--) steps.add(i * (nodeCount / stepCount));
long[] lookups = {0L};
steps.sort((a, b) -> {
lookups[0] += 2; // two O(1) getOrDefault() calls per comparison
int ia = indexMap.getOrDefault(a, Integer.MAX_VALUE);
int ib = indexMap.getOrDefault(b, Integer.MAX_VALUE);
return Integer.compare(ia, ib);
});
return lookups[0];
}
// =========================================================================
// Test 1 — maven-0004: defect vs fixed operation count at N=100
// =========================================================================
static void test1_sortIndexOfVsIndexMap() {
int n = 100;
long defectOps = defectiveSortIndexOfCost(n);
long fixedOps = fixedSortIndexMapCost(n);
System.out.printf(
"test1: n=%d defect_comparisons=%d fixed_lookups=%d%n",
n, defectOps, fixedOps);
// Defect must do significantly more work: O(N log N) × O(N) vs O(N log N) × O(1)
assert defectOps > fixedOps
: "defect must do more work than fix at n=" + n;
// Conservative lower bound: at least N comparator calls, each doing 2 indexOf
// scans of at least 1 element each → total ≥ N*(N-1) (triangular sum lower bound)
long lowerBound = (long) n * (n - 1);
assert defectOps >= lowerBound
: "defect comparisons=" + defectOps + " expected >= N*(N-1)=" + lowerBound;
}
// =========================================================================
// Test 2 — maven-0004: doubling N grows defect super-quadratically,
// fixed sub-quadratically
// =========================================================================
static void test2_sortScalingDefectVsFixed() {
int n1 = 60;
int n2 = 120;
long d1 = defectiveSortIndexOfCost(n1);
long d2 = defectiveSortIndexOfCost(n2);
long f1 = fixedSortIndexMapCost(n1);
long f2 = fixedSortIndexMapCost(n2);
double defectGrowth = (double) d2 / Math.max(1, d1);
double fixedGrowth = (double) f2 / Math.max(1, f1);
System.out.printf(
"test2: defect_growth=%.2fx (n 2x) fixed_growth=%.2fx%n",
defectGrowth, fixedGrowth);
// Defect is O(N² log N): doubling N → ~4× growth (the log N factor is minor)
assert defectGrowth > 3.0
: "defect should grow at least 3x with 2x N (O(N² log N)), got " + defectGrowth;
// Fixed is O(N log N): doubling N → ~2× growth
assert fixedGrowth <= 3.0
: "fixed should grow at most 3x with 2x N (O(N log N)), got " + fixedGrowth;
assert defectGrowth > fixedGrowth
: "defect growth must exceed fixed growth";
}
// =========================================================================
// Test 3 — maven-0004: sorted result is correct for both implementations
// =========================================================================
static void test3_sortCorrectnessCheck() {
int n = 50;
// Build sortedProjects (reference order 0..n-1)
List<Integer> sortedProjects = new ArrayList<>(n);
for (int i = 0; i < n; i++) sortedProjects.add(i);
// result list in arbitrary order (reverse)
List<Integer> defResult = new ArrayList<>(n);
List<Integer> fixResult = new ArrayList<>(n);
for (int i = n - 1; i >= 0; i--) {
defResult.add(i);
fixResult.add(i);
}
// Defective sort
defResult.sort((a, b) -> Integer.compare(sortedProjects.indexOf(a), sortedProjects.indexOf(b)));
// Fixed sort
Map<Integer, Integer> orderMap = new HashMap<>(n * 2);
for (int i = 0; i < n; i++) orderMap.put(sortedProjects.get(i), i);
fixResult.sort((a, b) -> Integer.compare(orderMap.get(a), orderMap.get(b)));
System.out.printf("test3: n=%d correctness check defResult[0]=%d fixResult[0]=%d last=%d%n",
n, defResult.get(0), fixResult.get(0), defResult.get(n - 1));
// Both should produce the same ordering
assert defResult.equals(fixResult)
: "defective and fixed sorts produced different orderings";
// First element should be 0 (lowest index in sortedProjects)
assert defResult.get(0).equals(0)
: "first sorted element should be 0, got " + defResult.get(0);
assert defResult.get(n - 1).equals(n - 1)
: "last sorted element should be " + (n-1) + ", got " + defResult.get(n-1);
}
// =========================================================================
// Test 4 — maven-0005: BuildPlanLogger defect vs fixed operation count
// =========================================================================
static void test4_buildPlanLoggerDefectVsFixed() {
int nodeCount = 200;
int stepCount = 100;
long defectOps = defectiveBuildPlanLoggerCost(nodeCount, stepCount);
long fixedOps = fixedBuildPlanLoggerCost(nodeCount, stepCount);
System.out.printf(
"test4: nodeCount=%d stepCount=%d defect_comparisons=%d fixed_lookups=%d%n",
nodeCount, stepCount, defectOps, fixedOps);
// Defect: O(M log M) comparator calls × O(N) indexOf each
// Fixed: O(M log M) × O(1) map lookup
assert defectOps > fixedOps
: "defect must do more work than fix";
// Lower bound: at least M comparator invocations × 2 indexOf scans of avg length N/2
long lowerBound = (long) stepCount * nodeCount / 2;
assert defectOps >= lowerBound
: "defect=" + defectOps + " expected >= " + lowerBound;
}
// =========================================================================
// Test 5 — maven-0005: scaling node count grows defect super-linearly,
// fixed grows at most linearly
// =========================================================================
static void test5_buildPlanLoggerScaling() {
int stepCount = 80;
int nodes1 = 100;
int nodes2 = 200; // 2x nodes
long d1 = defectiveBuildPlanLoggerCost(nodes1, stepCount);
long d2 = defectiveBuildPlanLoggerCost(nodes2, stepCount);
long f1 = fixedBuildPlanLoggerCost(nodes1, stepCount);
long f2 = fixedBuildPlanLoggerCost(nodes2, stepCount);
double defectGrowth = (double) d2 / Math.max(1, d1);
double fixedGrowth = (double) f2 / Math.max(1, f1);
System.out.printf(
"test5: stepCount=%d defect_growth=%.2fx (nodes 2x) fixed_growth=%.2fx%n",
stepCount, defectGrowth, fixedGrowth);
// Defect: O(N) per indexOf → doubling N doubles cost per comparator call → 2x overall
assert defectGrowth > 1.5
: "defect should grow with node count (O(N) indexOf), got " + defectGrowth;
// Fixed: O(1) per lookup → doubling N has no effect on sort cost (only on build-map cost)
// With N doubling, build-map is O(N) but sort is O(M log M) × O(1) — fixed lookups unchanged
assert fixedGrowth <= 2.0
: "fixed lookups should not grow with node count, got " + fixedGrowth;
assert defectGrowth > fixedGrowth
: "defect growth must exceed fixed growth when N doubles";
}
// =========================================================================
// Main
// =========================================================================
public static void main(String[] args) {
System.out.println("=== MavenGraphBuilderTest ===");
System.out.println("Modelling CWE-407 defects:");
System.out.println(" maven-0004: DefaultGraphBuilder sortedProjects.indexOf() → O(N² log N)");
System.out.println(" maven-0005: BuildPlanLogger sortedNodes().indexOf() → O(N²) per project");
System.out.println();
test1_sortIndexOfVsIndexMap();
System.out.println(" PASS test1_sortIndexOfVsIndexMap");
test2_sortScalingDefectVsFixed();
System.out.println(" PASS test2_sortScalingDefectVsFixed");
test3_sortCorrectnessCheck();
System.out.println(" PASS test3_sortCorrectnessCheck");
test4_buildPlanLoggerDefectVsFixed();
System.out.println(" PASS test4_buildPlanLoggerDefectVsFixed");
test5_buildPlanLoggerScaling();
System.out.println(" PASS test5_buildPlanLoggerScaling");
System.out.println();
System.out.println("All 5 tests PASSED.");
}
}