package unit; import java.util.*; /** * HttpdProxyBalancerTest — CWE-407 model test for Apache httpd HTTPD-001. * * Models the defect in mod_proxy_balancer.c where every sticky-session request * triggers a linear O(W) strcmp scan over all worker route strings. * * Defective: List scanned with String.equals() per request → O(W·R) * Fixed: HashMap lookup per request → O(R) * * Parameters: * W = 100 workers, R = 1 000 requests * * Five test methods: * 1. testDefectiveCorrectness — defective path returns the right worker * 2. testFixedCorrectness — fixed path returns the right worker * 3. testDefectiveCompareCount — defective path performs O(W·R) comparisons * 4. testFixedCompareCount — fixed path performs O(R) comparisons * 5. testSpeedupRatio — ratio > 50x */ public class HttpdProxyBalancerTest { static final int W = 100; // worker count static final int R = 1_000; // request count // ----------------------------------------------------------------------- // Model classes // ----------------------------------------------------------------------- /** Instrumented string comparison counter (shared, reset between tests). */ static long compareCount = 0; static boolean instrumentedEquals(String a, String b) { compareCount++; return a.equals(b); } static class Worker { final String route; Worker(String route) { this.route = route; } } /** Defective balancer: scans all workers linearly per request. */ static class DefectiveBalancer { final List workers = new ArrayList<>(); Worker findByRoute(String route) { for (Worker w : workers) { if (instrumentedEquals(w.route, route)) { return w; } } return null; } } /** Fixed balancer: O(1) hash lookup per request. */ static class FixedBalancer { final List workers = new ArrayList<>(); // CWE-407 fix: route_index populated at init time final Map routeIndex = new HashMap<>(); void addWorker(Worker w) { workers.add(w); if (!w.route.isEmpty()) { routeIndex.put(w.route, w); // O(1) insert } } Worker findByRoute(String route) { compareCount++; // count each hash probe as 1 op return routeIndex.get(route); } } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /** Build a list of W workers with routes "worker-0" .. "worker-(W-1)". */ static List buildWorkers() { List list = new ArrayList<>(W); for (int i = 0; i < W; i++) { list.add(new Worker("worker-" + i)); } return list; } /** * Return an array of R route strings drawn uniformly from the pool of W * worker routes. Every route is valid so both implementations always find * a match (worst case for both, fair comparison). */ static String[] buildRequests() { String[] reqs = new String[R]; // Spread requests evenly across all workers to exercise the full // scan depth of the defective implementation. for (int i = 0; i < R; i++) { reqs[i] = "worker-" + (i % W); } return reqs; } // ----------------------------------------------------------------------- // Assert helper // ----------------------------------------------------------------------- static void assertTrue(String msg, boolean condition) { if (!condition) throw new AssertionError("FAIL: " + msg); } static void assertEquals(String msg, Object expected, Object actual) { if (!Objects.equals(expected, actual)) throw new AssertionError("FAIL: " + msg + " expected=" + expected + " actual=" + actual); } // ----------------------------------------------------------------------- // Test 1: defective path returns the correct worker // ----------------------------------------------------------------------- static void testDefectiveCorrectness() { DefectiveBalancer balancer = new DefectiveBalancer(); buildWorkers().forEach(w -> balancer.workers.add(w)); compareCount = 0; // Look up each worker by its exact route for (int i = 0; i < W; i++) { String route = "worker-" + i; Worker found = balancer.findByRoute(route); assertTrue("defective found non-null for route " + route, found != null); assertEquals("defective correct worker for route " + route, route, found.route); } System.out.println("[PASS] testDefectiveCorrectness"); } // ----------------------------------------------------------------------- // Test 2: fixed path returns the correct worker // ----------------------------------------------------------------------- static void testFixedCorrectness() { FixedBalancer balancer = new FixedBalancer(); buildWorkers().forEach(balancer::addWorker); compareCount = 0; for (int i = 0; i < W; i++) { String route = "worker-" + i; Worker found = balancer.findByRoute(route); assertTrue("fixed found non-null for route " + route, found != null); assertEquals("fixed correct worker for route " + route, route, found.route); } System.out.println("[PASS] testFixedCorrectness"); } // ----------------------------------------------------------------------- // Test 3: defective comparison count is O(W·R) // ----------------------------------------------------------------------- static void testDefectiveCompareCount() { DefectiveBalancer balancer = new DefectiveBalancer(); buildWorkers().forEach(w -> balancer.workers.add(w)); String[] reqs = buildRequests(); compareCount = 0; for (String route : reqs) { balancer.findByRoute(route); } long defectiveCount = compareCount; // Each request scans until it finds the worker. Requests are spread // across all workers so on average the scan length is W/2; the minimum // bound we assert is R (every request matches at position 1 or later). assertTrue( "defective compare count (" + defectiveCount + ") >= R (" + R + ")", defectiveCount >= R ); // And we expect roughly W/2 * R comparisons on average long expected = (long) W / 2 * R; assertTrue( "defective compare count (" + defectiveCount + ") is close to W/2*R (" + expected + ")", defectiveCount >= expected / 2 && defectiveCount <= expected * 3 ); System.out.println("[PASS] testDefectiveCompareCount comparisons=" + defectiveCount); } // ----------------------------------------------------------------------- // Test 4: fixed comparison count is O(R) // ----------------------------------------------------------------------- static void testFixedCompareCount() { FixedBalancer balancer = new FixedBalancer(); buildWorkers().forEach(balancer::addWorker); String[] reqs = buildRequests(); compareCount = 0; for (String route : reqs) { balancer.findByRoute(route); } long fixedCount = compareCount; // Each request costs exactly 1 hash probe (we count that as 1 in // findByRoute), so fixedCount should equal R exactly. assertEquals("fixed compare count equals R", (long) R, fixedCount); System.out.println("[PASS] testFixedCompareCount comparisons=" + fixedCount); } // ----------------------------------------------------------------------- // Test 5: speedup ratio > 50x // ----------------------------------------------------------------------- static void testSpeedupRatio() { // Measure defective DefectiveBalancer defBalancer = new DefectiveBalancer(); buildWorkers().forEach(w -> defBalancer.workers.add(w)); String[] reqs = buildRequests(); compareCount = 0; for (String route : reqs) { defBalancer.findByRoute(route); } long defectiveCount = compareCount; // Measure fixed FixedBalancer fixBalancer = new FixedBalancer(); buildWorkers().forEach(fixBalancer::addWorker); compareCount = 0; for (String route : reqs) { fixBalancer.findByRoute(route); } long fixedCount = compareCount; double ratio = (double) defectiveCount / fixedCount; System.out.printf("[INFO] speedup ratio = %.1fx (defective=%d fixed=%d)%n", ratio, defectiveCount, fixedCount); assertTrue( "speedup ratio " + ratio + " > 50x (W=" + W + ", R=" + R + ")", ratio > 50.0 ); System.out.printf("[PASS] testSpeedupRatio ratio=%.1fx%n", ratio); } // ----------------------------------------------------------------------- // Main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== HttpdProxyBalancerTest W=" + W + " R=" + R + " ==="); testDefectiveCorrectness(); testFixedCorrectness(); testDefectiveCompareCount(); testFixedCompareCount(); testSpeedupRatio(); System.out.println("=== ALL TESTS PASSED ==="); } }