diff --git a/defects/pgbouncer-0001/NOTES.md b/defects/pgbouncer-0001/NOTES.md new file mode 100644 index 000000000..c311e18ed --- /dev/null +++ b/defects/pgbouncer-0001/NOTES.md @@ -0,0 +1,43 @@ +# pgbouncer-0001: CWE-312 — SCRAM verifier logged verbatim at debug level + +## Target + +PgBouncer PostgreSQL connection pooler: `src/client.c`, function `scram_client_first()` + +## Defect + +Line 1124: +```c +slog_debug(client, "stored secret = \"%s\"", user->passwd); +``` + +During SCRAM-SHA-256 authentication, PgBouncer logs `user->passwd` at debug level. The +stored secret is either: +- A SCRAM-SHA-256 verifier string: `SCRAM-SHA-256$:$:` +- A plaintext password (when auth_type=plain is configured) + +Either form is sensitive. A SCRAM verifier can be used in an offline dictionary attack +to recover the original password. A plaintext password is immediately usable. + +Debug logging is commonly enabled during troubleshooting and the output is written to +persistent log files, creating indefinite credential exposure. + +## Fix + +Remove the `slog_debug` line. The adjacent line 1119 already logs the SCRAM event +(`SCRAM client-first-message`), preserving diagnostic context without exposing the secret. + +## Severity + +MEDIUM-HIGH (CWE-312). Requires debug log access, but operators routinely enable debug +logging during connection issues, leaving credentials in log files indefinitely. + +## All 5 MOAD Results for PgBouncer + +| MOAD | Status | Notes | +|------|--------|-------| +| 0001 (CWE-407) | CLEAN | find_database() is O(D) but D is config-bounded (<100 DBs typical); user lookup uses AA-tree O(log U) | +| 0002 (Intertangle) | CLEAN | Single-threaded libevent loop; no coupling via shared mutable runtime state | +| 0003 (Leaked Context) | CLEAN | Single-threaded; no thread_local usage; not applicable | +| 0004 (CWE-312) | DEFECT | pgbouncer-0001: client.c:1124 logs SCRAM verifier/password at slog_debug | +| 0005 (Thundering Herd) | CLEAN | Single-threaded; no concurrent cache access; not applicable | diff --git a/defects/pgbouncer-0001/patch/pgbouncer-0001.patch b/defects/pgbouncer-0001/patch/pgbouncer-0001.patch new file mode 100644 index 000000000..4b639c22e --- /dev/null +++ b/defects/pgbouncer-0001/patch/pgbouncer-0001.patch @@ -0,0 +1,10 @@ +--- a/src/client.c ++++ b/src/client.c +@@ -1121,7 +1121,7 @@ static bool scram_client_first(PgSocket *client, uint32_t datalen, const uint8_t + + if (!user->mock_auth) { +- slog_debug(client, "stored secret = \"%s\"", user->passwd); ++ /* Do not log user->passwd: it holds the SCRAM verifier or plaintext password (CWE-312). */ + switch (get_password_type(user->passwd)) { + case PASSWORD_TYPE_MD5: + slog_error(client, "SCRAM authentication failed: user has MD5 secret"); diff --git a/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest$PgCredentials.class b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest$PgCredentials.class new file mode 100644 index 000000000..b6d366c5f Binary files /dev/null and b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest$PgCredentials.class differ diff --git a/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.class b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.class new file mode 100644 index 000000000..377a69b50 Binary files /dev/null and b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.class differ diff --git a/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.java b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.java new file mode 100644 index 000000000..c3755a436 --- /dev/null +++ b/defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.java @@ -0,0 +1,132 @@ +import java.util.*; + +/** + * pgbouncer-0001: CWE-312 — SCRAM verifier logged verbatim at debug level + * + * Source: src/client.c, function scram_client_first() + * Line: slog_debug(client, "stored secret = \"%s\"", user->passwd); + * + * Defect: During SCRAM-SHA-256 authentication, PgBouncer logs the user's stored + * secret (user->passwd) at slog_debug level. Depending on the auth_type configured, + * user->passwd may contain: + * - A plaintext password (auth_type=plain) + * - A SCRAM-SHA-256 verifier: "SCRAM-SHA-256$:$:" + * + * Even the SCRAM verifier is sensitive: it can be used in an offline dictionary attack + * or, for plaintext passwords, directly as the credential. Logging it to a debug log + * file violates CWE-312 (Cleartext Storage of Sensitive Information). + * + * Fix: Remove the slog_debug line. The log event type ("SCRAM client-first received") + * is still present on line 1119; no diagnostic value is lost. + * + * CVE-applicable: Yes — debug log file exposure of authentication credential. + * Severity: MEDIUM-HIGH (requires debug log access, but many ops enable debug logging + * during troubleshooting, leaving credential in log files indefinitely). + */ +public class PgBouncerScramSecretLogTest { + + // Simulate the log capture system + static List capturedLogs = new ArrayList<>(); + + static void slog_debug(String context, String fmt, Object... args) { + capturedLogs.add(String.format("[DEBUG][" + context + "] " + fmt, args)); + } + + // Simulated PgCredentials + static class PgCredentials { + String name; + String passwd; + boolean mock_auth; + PgCredentials(String name, String passwd) { + this.name = name; + this.passwd = passwd; + this.mock_auth = false; + } + } + + // --- DEFECTIVE: logs stored secret --- + static void scram_client_first_defective(String clientContext, PgCredentials user, String clientFirstMessage) { + slog_debug(clientContext, "SCRAM client-first-message = \"%s\"", clientFirstMessage); + if (!user.mock_auth) { + // DEFECT: logs the SCRAM verifier or plaintext password + slog_debug(clientContext, "stored secret = \"%s\"", user.passwd); + } + // ... rest of SCRAM processing + } + + // --- FIXED: does not log stored secret --- + static void scram_client_first_fixed(String clientContext, PgCredentials user, String clientFirstMessage) { + slog_debug(clientContext, "SCRAM client-first-message = \"%s\"", clientFirstMessage); + if (!user.mock_auth) { + // Fixed: no credential logging. Comment in code: + // /* Do not log user->passwd: it holds the SCRAM verifier or plaintext password (CWE-312). */ + } + // ... rest of SCRAM processing + } + + static void testDefectiveLogsSecret() { + capturedLogs.clear(); + PgCredentials user = new PgCredentials("alice", + "SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$StoredKeyHere:ServerKeyHere"); + scram_client_first_defective("client:127.0.0.1:5432", user, "n,,n=alice,r=clientnonce"); + + boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); + assert secretFound : "Defective impl should log 'stored secret'"; + System.out.println("PASS defective: secret IS logged: " + + capturedLogs.stream().filter(l -> l.contains("stored secret")).findFirst().orElse("?")); + } + + static void testFixedDoesNotLogSecret() { + capturedLogs.clear(); + PgCredentials user = new PgCredentials("alice", + "SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$StoredKeyHere:ServerKeyHere"); + scram_client_first_fixed("client:127.0.0.1:5432", user, "n,,n=alice,r=clientnonce"); + + boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); + assert !secretFound : "Fixed impl must NOT log 'stored secret', got: " + capturedLogs; + System.out.println("PASS fixed: secret is NOT logged (logs: " + capturedLogs.size() + " line(s))"); + } + + static void testFixedStillLogsDiagnostic() { + capturedLogs.clear(); + PgCredentials user = new PgCredentials("bob", "SCRAM-SHA-256$4096:abc$defgh:ijklm"); + scram_client_first_fixed("client:127.0.0.1:5433", user, "n,,n=bob,r=bobnonce"); + + boolean clientFirstLogged = capturedLogs.stream().anyMatch(line -> line.contains("client-first-message")); + assert clientFirstLogged : "Fixed impl must still log SCRAM client-first-message event"; + System.out.println("PASS fixed: diagnostic 'client-first-message' still logged"); + } + + static void testPlaintextPasswordNotLogged() { + capturedLogs.clear(); + PgCredentials user = new PgCredentials("carol", "supersecretpassword123"); + scram_client_first_fixed("client:192.168.1.1:5432", user, "n,,n=carol,r=carolnonce"); + + boolean plainPassFound = capturedLogs.stream().anyMatch(line -> + line.contains("supersecretpassword123")); + assert !plainPassFound : "Fixed impl must NOT log plaintext passwords"; + System.out.println("PASS fixed: plaintext password NOT logged"); + } + + static void testMockAuthNotLogged() { + capturedLogs.clear(); + PgCredentials user = new PgCredentials("mockuser", "not-a-real-secret"); + user.mock_auth = true; + // Both defective and fixed skip the branch when mock_auth=true + scram_client_first_defective("client:10.0.0.1:5432", user, "n,,n=mockuser,r=mocknonce"); + + boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); + assert !secretFound : "mock_auth=true: secret branch should be skipped"; + System.out.println("PASS mock-auth: secret branch skipped for mock_auth=true"); + } + + public static void main(String[] args) { + System.out.println("=== pgbouncer-0001: CWE-312 SCRAM verifier logged at debug ==="); + testDefectiveLogsSecret(); + testFixedDoesNotLogSecret(); + testFixedStillLogsDiagnostic(); + testPlaintextPasswordNotLogged(); + testMockAuthNotLogged(); + System.out.println("ALL PASS"); + } +} diff --git a/defects/pgbouncer/CLEAN.md b/defects/pgbouncer/CLEAN.md index 65fb521e3..2b407d011 100644 --- a/defects/pgbouncer/CLEAN.md +++ b/defects/pgbouncer/CLEAN.md @@ -1,6 +1,8 @@ -# PgBouncer — CLEAN (CWE-407) +# PgBouncer — CLEAN (CWE-407) | DEFECT pgbouncer-0001 (CWE-312) -Scanned 2026-03-30. +Scanned 2026-03-30 (CWE-407 only). Re-scanned 2026-03-31 (all 5 MOADs). + +## MOAD-0001 (CWE-407) — CLEAN PgBouncer uses efficient data structures throughout: - **User lookup**: AA-tree (`aatree_search`) — O(log N) @@ -10,4 +12,28 @@ PgBouncer uses efficient data structures throughout: - **Database lookup**: `find_database()` is linear scan of `database_list`, but databases are configuration-bounded (typically <100), not attacker-controlled. -No CWE-407 defect found. +## MOAD-0002 (Intertangle) — CLEAN + +PgBouncer is single-threaded (libevent loop). Global lists (`database_list`, `pool_list`, +`user_tree`) are accessed from one thread only. No shared mutable god object coupling +independent subsystems. + +## MOAD-0003 (Leaked Context) — CLEAN + +Single-threaded event loop. No `thread_local` or `pthread_key` carrying request-scoped +identity. Not applicable. + +## MOAD-0004 (CWE-312) — DEFECT: see pgbouncer-0001 + +`src/client.c` function `scram_client_first()` line 1124: + +```c +slog_debug(client, "stored secret = \"%s\"", user->passwd); +``` + +Logs `user->passwd` at debug level. Depending on auth_type, this is a SCRAM-SHA-256 +verifier (offline-crackable) or a plaintext password. Fix: remove log line. + +## MOAD-0005 (Thundering Herd) — CLEAN + +Single-threaded. No concurrent cache access patterns. Not applicable. diff --git a/defects/snort3-0002/NOTES.md b/defects/snort3-0002/NOTES.md new file mode 100644 index 000000000..47c7d5eb5 --- /dev/null +++ b/defects/snort3-0002/NOTES.md @@ -0,0 +1,42 @@ +# snort3-0002: CWE-407 — CHP match_tally O(M*T) linear scan per HTTP packet + +## Target + +Snort3 network IDS: `src/network_inspectors/appid/detector_plugins/http_url_patterns.cc` + +## Defect + +`chp_add_candidate_to_tally()` uses `std::find_if` over `CHPMatchTally` (a +`std::vector`) to locate a CHPApp entry and decrement its +`key_pattern_countdown`. This function is called from `chp_key_pattern_match()`, which is +the Aho-Corasick match callback invoked for every key-pattern match in HTTP payload +inspection. + +With M pattern match callbacks per HTTP packet and T distinct CHPApp candidates in the +tally, the total work is O(M * T) per packet. + +## Fix + +Add `std::unordered_map match_tally_index` to `ChpMatchDescriptor`. +The index maps each CHPApp pointer to its position in the `match_tally` vector. Lookup is +O(1) amortized. Total work becomes O(M + T) per packet. + +## Complexity + +O(M*T) -> O(M+T) per HTTP packet. Measured: 48x op-count reduction at T=100, M=20. + +## Severity + +MEDIUM. HTTP inspection is a hot path in any network IDS deployment. Flows with many +HTTP rule patterns (e.g., enterprise deployments with hundreds of CHP rules) see +measurable per-packet overhead. + +## All 5 MOAD Results for Snort3 + +| MOAD | Status | Notes | +|------|--------|-------| +| 0001 (CWE-407) | DEFECT x2 | snort3-0001 (service_candidates, patched); snort3-0002 (CHP match_tally, this) | +| 0002 (Intertangle) | CLEAN | SnortConfig is immutable at runtime; per-thread flow state; no god object | +| 0003 (Leaked Context) | CLEAN | THREAD_LOCAL used correctly: per-thread packet stats, cleared after each packet | +| 0004 (CWE-312) | CLEAN | No sensitive headers logged verbatim; extractor does not log auth headers | +| 0005 (Thundering Herd) | CLEAN | Per-thread flow/session caches (THREAD_LOCAL); no shared mutable cache without sync | diff --git a/defects/snort3-0002/patch/snort3-0002.patch b/defects/snort3-0002/patch/snort3-0002.patch new file mode 100644 index 000000000..9c72d3ca7 --- /dev/null +++ b/defects/snort3-0002/patch/snort3-0002.patch @@ -0,0 +1,66 @@ +--- a/src/network_inspectors/appid/detector_plugins/http_url_patterns.h ++++ b/src/network_inspectors/appid/detector_plugins/http_url_patterns.h +@@ -217,9 +217,11 @@ struct CHPMatchCandidate + + typedef std::vector CHPMatchTally; + ++#include ++ + class ChpMatchDescriptor + { + public: + void sort_chp_matches() + { + for(unsigned i = 0; i < NUM_HTTP_FIELDS; i++) +@@ -235,6 +237,7 @@ public: + unsigned cur_ptype = 0; + uint8_t* buffer[NUM_HTTP_FIELDS] = {}; + unsigned length[NUM_HTTP_FIELDS] = {}; + CHPMatchTally match_tally; ++ std::unordered_map match_tally_index; + }; + +--- a/src/network_inspectors/appid/detector_plugins/http_url_patterns.cc ++++ b/src/network_inspectors/appid/detector_plugins/http_url_patterns.cc +@@ -589,14 +589,14 @@ static int chp_pattern_match(void* id, void*, int match_end_pos, void* data, vo + static inline void chp_add_candidate_to_tally(CHPMatchTally& match_tally, CHPApp* chpapp) + { +- auto it = std::find_if(match_tally.begin(), match_tally.end(), +- [&chpapp](const CHPMatchCandidate& item){ return chpapp == item.chpapp; }); +- if (it != match_tally.end()) +- { +- (*it).key_pattern_countdown--; +- return; +- } +- +- match_tally.emplace_back( CHPMatchCandidate{ chpapp, chpapp->key_pattern_length_sum, +- chpapp->key_pattern_count - 1 } ); + } + ++static inline void chp_add_candidate_to_tally(CHPMatchTally& match_tally, ++ std::unordered_map& match_tally_index, CHPApp* chpapp) ++{ ++ auto it = match_tally_index.find(chpapp); ++ if (it != match_tally_index.end()) ++ { ++ match_tally[it->second].key_pattern_countdown--; ++ return; ++ } ++ ++ std::size_t idx = match_tally.size(); ++ match_tally.emplace_back( CHPMatchCandidate{ chpapp, chpapp->key_pattern_length_sum, ++ chpapp->key_pattern_count - 1 } ); ++ match_tally_index[chpapp] = idx; ++} ++ + // In addition to creating the linked list of matching actions this function will + // create the CHPMatchTally needed to find the longest matching pattern. + static int chp_key_pattern_match(void* id, void*, int match_end_pos, void* data, void*) +@@ -611,7 +613,7 @@ static int chp_key_pattern_match(void* id, void*, int match_end_pos, void* data + if (target->key_pattern) + { +- chp_add_candidate_to_tally(cmd->match_tally, target->chpapp); ++ chp_add_candidate_to_tally(cmd->match_tally, cmd->match_tally_index, target->chpapp); + } + + return chp_pattern_match(id, nullptr, match_end_pos, cmd, nullptr); diff --git a/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPApp.class b/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPApp.class new file mode 100644 index 000000000..50abe2b34 Binary files /dev/null and b/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPApp.class differ diff --git a/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPMatchCandidate.class b/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPMatchCandidate.class new file mode 100644 index 000000000..8ac1a8bf1 Binary files /dev/null and b/defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPMatchCandidate.class differ diff --git a/defects/snort3-0002/test/Snort3ChpMatchTallyTest.class b/defects/snort3-0002/test/Snort3ChpMatchTallyTest.class new file mode 100644 index 000000000..6fa70d136 Binary files /dev/null and b/defects/snort3-0002/test/Snort3ChpMatchTallyTest.class differ diff --git a/defects/snort3-0002/test/Snort3ChpMatchTallyTest.java b/defects/snort3-0002/test/Snort3ChpMatchTallyTest.java new file mode 100644 index 000000000..c3554736e --- /dev/null +++ b/defects/snort3-0002/test/Snort3ChpMatchTallyTest.java @@ -0,0 +1,203 @@ +import java.util.*; + +/** + * snort3-0002: CWE-407 — chp_add_candidate_to_tally O(M*T) linear scan + * + * Source: src/network_inspectors/appid/detector_plugins/http_url_patterns.cc + * Function: chp_add_candidate_to_tally() + * + * Defect: CHPMatchTally is a std::vector. For each Aho-Corasick + * pattern match callback (chp_key_pattern_match), chp_add_candidate_to_tally performs + * std::find_if over the entire match_tally vector to locate the CHPApp entry and + * decrement its key_pattern_countdown. With M pattern matches and T unique CHPApp + * candidates in the tally, the total work is O(M * T) per HTTP packet. + * + * Fix: Add std::unordered_map match_tally_index alongside the vector. + * Lookup becomes O(1) amortized, reducing total work to O(M + T) per packet. + * + * Complexity: O(M*T) -> O(M+T) per HTTP packet + * Severity: MEDIUM (per HTTP flow, M~50-200 pattern matches, T~10-50 candidates) + * Speedup: up to T-fold (e.g., 50x at T=50 candidates) + */ +public class Snort3ChpMatchTallyTest { + + // Simulate CHPApp as an opaque identity (pointer in C++) + static class CHPApp { + final int id; + final int keyPatternCount; + final int keyPatternLengthSum; + CHPApp(int id, int kpc, int kpls) { + this.id = id; + this.keyPatternCount = kpc; + this.keyPatternLengthSum = kpls; + } + } + + static class CHPMatchCandidate { + CHPApp chpapp; + int keyPatternLengthSum; + int keyPatternCountdown; + CHPMatchCandidate(CHPApp app) { + this.chpapp = app; + this.keyPatternLengthSum = app.keyPatternLengthSum; + this.keyPatternCountdown = app.keyPatternCount - 1; + } + } + + // --- DEFECTIVE implementation: O(M*T) --- + static void chp_add_candidate_to_tally_defective(List match_tally, CHPApp chpapp) { + // Linear scan over tally - O(T) per call + for (CHPMatchCandidate item : match_tally) { + if (item.chpapp == chpapp) { + item.keyPatternCountdown--; + return; + } + } + match_tally.add(new CHPMatchCandidate(chpapp)); + } + + // --- FIXED implementation: O(1) amortized --- + static void chp_add_candidate_to_tally_fixed( + List match_tally, + Map match_tally_index, + CHPApp chpapp) { + Integer idx = match_tally_index.get(chpapp); + if (idx != null) { + match_tally.get(idx).keyPatternCountdown--; + return; + } + int newIdx = match_tally.size(); + match_tally.add(new CHPMatchCandidate(chpapp)); + match_tally_index.put(chpapp, newIdx); + } + + // Simulate M pattern match callbacks for a given set of CHPApp candidates + static long benchDefective(List apps, int matchesPerApp) { + List tally = new ArrayList<>(); + long ops = 0; + for (CHPApp app : apps) { + for (int m = 0; m < matchesPerApp; m++) { + // Count ops: linear scan + int scanned = 0; + boolean found = false; + for (CHPMatchCandidate item : tally) { + scanned++; + if (item.chpapp == app) { + item.keyPatternCountdown--; + found = true; + break; + } + } + ops += scanned; + if (!found) { + tally.add(new CHPMatchCandidate(app)); + ops++; + } + } + } + return ops; + } + + static long benchFixed(List apps, int matchesPerApp) { + List tally = new ArrayList<>(); + Map tallyIndex = new HashMap<>(); + long ops = 0; + for (CHPApp app : apps) { + for (int m = 0; m < matchesPerApp; m++) { + ops++; // O(1) hash lookup + Integer idx = tallyIndex.get(app); + if (idx != null) { + tally.get(idx).keyPatternCountdown--; + } else { + int newIdx = tally.size(); + tally.add(new CHPMatchCandidate(app)); + tallyIndex.put(app, newIdx); + ops++; // map insert + } + } + } + return ops; + } + + static void testCorrectness() { + int T = 5; // distinct CHPApp candidates + int M = 3; // matches per app + List apps = new ArrayList<>(); + for (int i = 0; i < T; i++) apps.add(new CHPApp(i, 3, i + 10)); + + // Build tally with defective impl + List tallyD = new ArrayList<>(); + for (int m = 0; m < M; m++) { + for (CHPApp app : apps) { + chp_add_candidate_to_tally_defective(tallyD, app); + } + } + + // Build tally with fixed impl + List tallyF = new ArrayList<>(); + Map indexF = new HashMap<>(); + for (int m = 0; m < M; m++) { + for (CHPApp app : apps) { + chp_add_candidate_to_tally_fixed(tallyF, indexF, app); + } + } + + // Both should have same number of candidates + assert tallyD.size() == T : "Defective: wrong tally size " + tallyD.size(); + assert tallyF.size() == T : "Fixed: wrong tally size " + tallyF.size(); + + // Both should have same countdown values (first match creates with count-1=2, then M-1=2 decrements -> 0) + for (int i = 0; i < T; i++) { + assert tallyD.get(i).keyPatternCountdown == tallyF.get(i).keyPatternCountdown + : "Countdown mismatch at index " + i + + ": defective=" + tallyD.get(i).keyPatternCountdown + + " fixed=" + tallyF.get(i).keyPatternCountdown; + } + System.out.println("PASS correctness: both produce identical tally (T=" + T + ", M=" + M + ")"); + } + + static void testOpCount() { + // T candidates, each with M=10 pattern matches + int T = 50; + int M = 10; + List apps = new ArrayList<>(); + for (int i = 0; i < T; i++) apps.add(new CHPApp(i, M, i + 10)); + + long opsD = benchDefective(apps, M); + long opsF = benchFixed(apps, M); + + double ratio = (double) opsD / opsF; + System.out.printf("PASS op-count: T=%d M=%d | defective=%d ops | fixed=%d ops | ratio=%.1fx%n", + T, M, opsD, opsF, ratio); + assert ratio > 5.0 : "Expected >5x ratio, got " + ratio; + } + + static void testLargeScale() { + // Simulate a heavy HTTP scan: T=100 candidates, M=20 matches each + int T = 100; + int M = 20; + List apps = new ArrayList<>(); + for (int i = 0; i < T; i++) apps.add(new CHPApp(i, M, i + 10)); + + long t0D = System.nanoTime(); + long opsD = benchDefective(apps, M); + long t1D = System.nanoTime(); + + long t0F = System.nanoTime(); + long opsF = benchFixed(apps, M); + long t1F = System.nanoTime(); + + double ratio = (double) opsD / opsF; + System.out.printf("PASS large-scale: T=%d M=%d | defective=%d ops (%.2fms) | fixed=%d ops (%.2fms) | ratio=%.1fx%n", + T, M, opsD, (t1D - t0D) / 1e6, opsF, (t1F - t0F) / 1e6, ratio); + assert ratio > 10.0 : "Expected >10x ratio, got " + ratio; + } + + public static void main(String[] args) { + System.out.println("=== snort3-0002: CHP match_tally O(M*T) -> O(M+T) ==="); + testCorrectness(); + testOpCount(); + testLargeScale(); + System.out.println("ALL PASS"); + } +}