java-topology/tests/support/Moad0011Algorithm.java
russell@unturf.com 4888153e40 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.
2026-04-12 15:43:19 -04:00

216 lines
8.5 KiB
Java

package support;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* MOAD-0011: CWE-1333 — A Catastrophic Inheritance.
*
* Defect: a PCRE-compatible regex engine inherits Perl's backtracking NFA
* semantics. Applied to user-controlled input with nested quantifiers or
* ambiguous alternation, the engine explores O(2^N) parse trees before
* concluding no match. The defect propagates through intellectual lineage:
* Perl designed the backtracking NFA in 1987, PCRE codified it, and every
* language that adopted Perl's regex syntax inherited the worst-case complexity.
*
* Confirmed instance: bleach 3.x sanitize_css gauntlet pattern — 12-second hang
* on 71-char input. Java's java.util.regex uses backtracking NFA and exhibits
* the same catastrophic behavior on the same class of patterns.
*
* Fix: restructure the pattern to eliminate nested quantifiers and ambiguous
* alternation, or switch to a linear NFA engine (RE2, Rust regex) that
* guarantees O(N) matching with no catastrophic backtracking.
*
* This specimen proves the defect structurally (nested quantifier detection)
* and behaviorally at safe scale (step counting on a small custom NFA).
*
* Scanner detects: Pattern.compile / re.compile / RegExp constructor called with
* patterns containing nested quantifiers: (X+)+, (X*)+, (X+)*, (X|Y+)+, etc.
*/
public class Moad0011Algorithm {
// ── Structural detection ──────────────────────────────────────────────────
/**
* Returns true if the regex pattern string contains nested quantifiers —
* the structural signature of catastrophic backtracking risk.
*
* Detects patterns of the form (group-with-quantifier) followed by +, *, ?, {n,}.
* Examples that match: (a+)+ (a*)+ (a+b*)+ (a+)+? (a+){2,}
* Examples that do not: a+ (a+) (a|b)+ [a-z]+
*/
public static boolean hasCatastrophicStructure(String pattern) {
// Regex to detect nested quantifiers:
// A group whose contents contain a quantifier, followed by an outer quantifier.
// Inner: \([^)]*[+*?][^)]*\) — a paren group containing +, *, or ?
// Outer: [+*?] or {\d+,?\d*} following the closing paren
return pattern.matches(".*\\([^)]*[+*?][^)]*\\)[+*?{].*");
}
/**
* Returns true if the pattern is safe: no nested quantifiers detected.
*/
public static boolean isSafePattern(String pattern) {
return !hasCatastrophicStructure(pattern);
}
// ── Known defective and fixed pattern pairs ───────────────────────────────
/**
* Defective: (a+)+b — nested quantifier.
* The outer + allows grouping any prefix of a's; the inner + requires at
* least one. On input "aaa...c" (no 'b'), the engine explores all ways to
* partition the a-run into groups — O(2^N) attempts.
*/
public static final String DEFECTIVE_PATTERN_BLEACH = "(a+)+b";
/**
* Fixed: a+b — equivalent semantics, no nesting.
* Matches one or more a's followed by b. On "aaa...c" it fails in O(N).
*/
public static final String FIXED_PATTERN_BLEACH = "a+b";
/**
* Defective: CSS-style gauntlet (simplified from bleach 3.x).
* Ambiguous alternation with nested quantifiers across CSS property branches.
*/
public static final String DEFECTIVE_PATTERN_CSS =
"([a-z0-9]+[\\s]*:[\\s]*(([a-z0-9]+[\\s]*)+(,|;|$))+)";
/**
* Fixed: anchored, non-ambiguous CSS value pattern.
*/
public static final String FIXED_PATTERN_CSS =
"^[a-z0-9][a-z0-9\\s]*:[a-z0-9,;\\s]+$";
// ── Behavioral proof: step-counting NFA for (a+)+b ───────────────────────
/**
* Result of a regex match attempt: whether it matched and how many NFA
* steps were taken. Step count proves exponential vs linear growth.
*/
public static final class Result {
public final boolean matched;
public final long steps;
public Result(boolean matched, long steps) {
this.matched = matched;
this.steps = steps;
}
}
/**
* Defective NFA: matches (a+)+b with explicit backtracking step counting.
*
* Implements the nested quantifier pattern by trying all possible
* partitions of a leading run of 'a' characters. On non-matching input
* (no trailing 'b'), explores O(2^N) states.
*
* @param input the string to match
* @return Result with matched=true iff input matches, steps=NFA steps taken
*/
public static Result matchDefective(String input) {
long[] stepCounter = {0L};
boolean matched = defectiveMatch(input, 0, stepCounter);
return new Result(matched, stepCounter[0]);
}
/**
* Recursive backtracking NFA for (a+)+b.
*
* State: consumed 'pos' characters of input. Outer quantifier tries at
* least one inner (a+) group. Each inner group consumes 1..k a's. The
* outer loop repeats until no more a's can be consumed, then checks for 'b'.
*/
private static boolean defectiveMatch(String input, int pos, long[] steps) {
steps[0]++;
return outerPlus(input, pos, steps);
}
/** Outer (...)+ — must match at least one inner group. */
private static boolean outerPlus(String input, int pos, long[] steps) {
// Try consuming one inner (a+) group starting at 'pos', then recurse outer.
return innerPlus(input, pos, steps, true);
}
/**
* Inner a+ — consume 1..k a's, then try continuing the outer loop or matching 'b'.
*
* @param first true if this is the first attempt at the outer loop (must match at least once)
*/
private static boolean innerPlus(String input, int pos, long[] steps, boolean first) {
steps[0]++;
if (pos >= input.length() || input.charAt(pos) != 'a') {
// Cannot consume an 'a' — try ending outer loop and matching 'b'
if (!first) {
return matchB(input, pos, steps);
}
return false; // outer + needs at least one inner group
}
// Greedy: try consuming as many a's as possible, then backtrack
int start = pos;
// Count maximum a's from here
int maxPos = pos;
while (maxPos < input.length() && input.charAt(maxPos) == 'a') maxPos++;
// Try splitting: consume (maxPos - start) down to 1 a's in inner group,
// then continue outer loop or end
for (int endInner = maxPos; endInner > start; endInner--) {
steps[0]++;
// After consuming [start..endInner) a's, try continuing outer loop
if (innerPlus(input, endInner, steps, false)) return true;
}
return false;
}
/** Match literal 'b' at position pos. */
private static boolean matchB(String input, int pos, long[] steps) {
steps[0]++;
return pos < input.length() && input.charAt(pos) == 'b' && pos + 1 == input.length();
}
/**
* Fixed NFA: matches a+b in O(N) steps — no backtracking, no nested quantifiers.
*
* @param input the string to match
* @return Result with matched=true iff input matches a+b exactly, steps=O(N)
*/
public static Result matchFixed(String input) {
long steps = 0;
int pos = 0;
// Must have at least one 'a'
if (pos >= input.length() || input.charAt(pos) != 'a') {
return new Result(false, ++steps);
}
// Consume all leading a's — O(N) scan, single pass, no backtracking
while (pos < input.length() && input.charAt(pos) == 'a') {
pos++;
steps++;
}
// Must end with exactly one 'b' and nothing after
steps++;
boolean matched = (pos < input.length() && input.charAt(pos) == 'b'
&& pos + 1 == input.length());
return new Result(matched, steps);
}
/**
* Build an adversarial non-matching input: N a's followed by 'c'.
* Defective engine explores O(2^N) states; fixed engine scans O(N) and stops.
*/
public static String adversarialInput(int aCount) {
return "a".repeat(aCount) + "c";
}
/**
* Build a matching input: N a's followed by 'b'.
* Both engines return matched=true; step counts differ in constant factor.
*/
public static String matchingInput(int aCount) {
return "a".repeat(aCount) + "b";
}
}