feat: add unit, integration, and functional test coverage for all 9 MOADs
Each MOAD now has a synthetic defective specimen and fixed specimen proven from first principles across three test tiers: Unit (tests/unit/Moad000X*.java): - Correctness: defective and fixed produce identical functional output - Defect behavior: defective specimen exhibits the defect (measurable) - Fix behavior: fixed specimen eliminates the defect Integration (tests/integration/AllMoadsIntegrationTest.java): - All 9 MOADs proven at medium scale (N=500-2000) - MOAD-0001: O(N^2) vs O(N) list scan at N=1000 - MOAD-0002: 500 sessions trample each other (defective) vs coexist (fixed) - MOAD-0003: 250 anonymous requests leak auth identity (defective) vs zero (fixed) - MOAD-0004: 3000 credential exposures across 1000 requests (defective) vs zero (fixed) - MOAD-0005: 500 computes for 500 concurrent misses vs exactly 1 - MOAD-0006: all 500 passwords extractable from DB (defective) vs unextractable (fixed) - MOAD-0007: N=2000 spatial objects, defective visits all 2000 vs O(log N + k) - MOAD-0009: 990 wasted firings for 1000 ticks / 10 events vs zero waste - MOAD-0011: 10240 NFA steps vs 13 steps on N=12 adversarial input (788x) Functional (tests/functional/AllMoadsFunctionalTest.java): - MOAD-0005: real-thread contention proves herd (defective >1 compute, fixed exactly 1) - MOAD-0007: N=50000 spatial objects, 50M defective probes vs 516K fixed (97x speedup) - MOAD-0009: 10000 ticks / 10 events, 9990 wasted firings vs zero (1000x ratio) - MOAD-0011: N=16 adversarial, 163840 defective steps vs 17 fixed (9638x ratio) Support algorithms (tests/support/Moad000X*.java): - Moad0002Algorithm: shared mutable global state (DefectiveAudioSystem / FixedAudioSystem + Context) - Moad0003Algorithm: ThreadLocal not cleared (handleDefective / handleFixed with finally) - Moad0004Algorithm: HTTP headers logged verbatim (logDefective / logFixed with CREDENTIAL_HEADERS denylist) - Moad0005Algorithm: get+null+compute+put (DefectiveCache HashMap / FixedCache ConcurrentHashMap.computeIfAbsent) - Moad0006Algorithm: Base64 password storage (DefectiveCredentialStore / FixedCredentialStore SHA-256+salt) - Moad0007Algorithm: linear spatial scan (queryDefective list / queryFixed sorted array + binary search) - Moad0009Algorithm: timer-driven polling (runDefectiveScheduler / runFixedEventDriven) - Moad0011Algorithm: PCRE nested quantifiers (matchDefective backtracking NFA / matchFixed linear NFA) Makefile: added unit-moad-0002 through unit-moad-0011 targets, integration-all-moads, functional-all-moads. integration and functional targets now depend on all-MOADs variants.
This commit is contained in:
parent
1f88aa0374
commit
4888153e40
19 changed files with 3359 additions and 5 deletions
125
tests/support/Moad0005Algorithm.java
Normal file
125
tests/support/Moad0005Algorithm.java
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package support;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* MOAD-0005: CWE-362 — A Thundering Herd.
|
||||
*
|
||||
* Defect: a cache lookup uses the pattern get() + null-check + compute + put()
|
||||
* with no lock or synchronization around the compute path. When N threads all
|
||||
* miss the cache simultaneously, each one sees null, each one runs the expensive
|
||||
* computation, and all N results race to be stored. N-1 computes are wasted.
|
||||
* Under high concurrency, this saturates the compute resource (CPU, DB, network)
|
||||
* with redundant work.
|
||||
*
|
||||
* Fix: ConcurrentHashMap.computeIfAbsent() guarantees exactly one compute per
|
||||
* key under concurrent access. All other callers block until the first compute
|
||||
* completes, then receive the cached value.
|
||||
*
|
||||
* Scanner detects: HashMap.get() + null check + compute + HashMap.put() on the
|
||||
* same key, without a surrounding synchronized block or lock acquisition.
|
||||
*
|
||||
* Note: unit tests simulate the concurrent race with a single-threaded model
|
||||
* (all callers check before any put). Integration tests use real threads.
|
||||
*/
|
||||
public class Moad0005Algorithm {
|
||||
|
||||
/** Counts how many times the "expensive" compute function was called. */
|
||||
public static final AtomicInteger COMPUTE_CALLS = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* Simulates an expensive computation (DB query, RPC, etc.).
|
||||
* Increments COMPUTE_CALLS so tests can verify how many times it ran.
|
||||
*/
|
||||
public static String expensiveCompute(String key) {
|
||||
COMPUTE_CALLS.incrementAndGet();
|
||||
return "value-for-" + key;
|
||||
}
|
||||
|
||||
// ── Defective: get + null-check + compute + put ───────────────────────────
|
||||
|
||||
/**
|
||||
* Defective cache backed by unsynchronized HashMap.
|
||||
*
|
||||
* The defect: get() and put() are not atomic. If N callers all invoke
|
||||
* getOrCompute() before any of them completes, they all see null and all
|
||||
* call expensiveCompute().
|
||||
*/
|
||||
public static final class DefectiveCache {
|
||||
private final HashMap<String, String> cache = new HashMap<>();
|
||||
|
||||
public String getOrCompute(String key) {
|
||||
String value = cache.get(key); // 1. lookup — may be null for N callers simultaneously
|
||||
if (value == null) { // 2. all N see null
|
||||
value = expensiveCompute(key); // 3. all N compute — HERD
|
||||
cache.put(key, value); // 4. last writer wins, N-1 computes wasted
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates N callers all hitting a cold DefectiveCache simultaneously.
|
||||
*
|
||||
* All callers read before any put — the race window. Returns the number
|
||||
* of times expensiveCompute was called (should be 1, but returns N for defective).
|
||||
*/
|
||||
public static int simulateDefectiveConcurrentMiss(int callerCount, String key) {
|
||||
COMPUTE_CALLS.set(0);
|
||||
DefectiveCache cache = new DefectiveCache();
|
||||
|
||||
// Phase 1: all callers read — none sees a cached value yet.
|
||||
String[] seen = new String[callerCount];
|
||||
for (int i = 0; i < callerCount; i++) {
|
||||
seen[i] = cache.cache.get(key); // all see null
|
||||
}
|
||||
|
||||
// Phase 2: all callers that saw null compute and put.
|
||||
for (int i = 0; i < callerCount; i++) {
|
||||
if (seen[i] == null) {
|
||||
String v = expensiveCompute(key);
|
||||
cache.cache.put(key, v);
|
||||
}
|
||||
}
|
||||
|
||||
return COMPUTE_CALLS.get(); // DEFECT: returns callerCount
|
||||
}
|
||||
|
||||
// ── Fixed: computeIfAbsent ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fixed cache backed by ConcurrentHashMap.computeIfAbsent().
|
||||
*
|
||||
* The fix: computeIfAbsent() is atomic per key. The first caller computes;
|
||||
* all subsequent callers block until the value is ready, then receive it.
|
||||
* expensiveCompute() runs exactly once per key regardless of concurrency.
|
||||
*/
|
||||
public static final class FixedCache {
|
||||
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public String getOrCompute(String key) {
|
||||
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates N callers all hitting a cold FixedCache.
|
||||
* Returns the number of times expensiveCompute was called (always 1).
|
||||
*/
|
||||
public static int simulateFixedConcurrentMiss(int callerCount, String key) {
|
||||
COMPUTE_CALLS.set(0);
|
||||
FixedCache cache = new FixedCache();
|
||||
for (int i = 0; i < callerCount; i++) {
|
||||
cache.getOrCompute(key);
|
||||
}
|
||||
return COMPUTE_CALLS.get(); // FIX: always 1
|
||||
}
|
||||
|
||||
/** Reset compute counter between test cases. */
|
||||
public static void resetCounter() {
|
||||
COMPUTE_CALLS.set(0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue