snort3: CWE-407 CHP match_tally O(M*T); pgbouncer: CWE-312 SCRAM secret logged
snort3-0002: chp_add_candidate_to_tally() in http_url_patterns.cc calls std::find_if over CHPMatchTally vector for each Aho-Corasick HTTP key-pattern match callback, O(M*T) per packet. Fix: add unordered_map index to ChpMatchDescriptor for O(1) lookup. 48x op-count reduction at T=100/M=20. 3/3 PASS. pgbouncer-0001: scram_client_first() logs user->passwd (SCRAM verifier or plaintext password) at slog_debug level, CWE-312. Fix: remove the log line. 5/5 PASS. pgbouncer MOAD-0002/0003/0005 CLEAN (single-threaded libevent loop). snort3 MOAD-0002/0003/0004/0005 CLEAN.
This commit is contained in:
parent
62305ef339
commit
6b80a87270
12 changed files with 525 additions and 3 deletions
43
defects/pgbouncer-0001/NOTES.md
Normal file
43
defects/pgbouncer-0001/NOTES.md
Normal file
|
|
@ -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$<iters>:<salt>$<StoredKey>:<ServerKey>`
|
||||
- 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 |
|
||||
10
defects/pgbouncer-0001/patch/pgbouncer-0001.patch
Normal file
10
defects/pgbouncer-0001/patch/pgbouncer-0001.patch
Normal file
|
|
@ -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");
|
||||
Binary file not shown.
BIN
defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.class
Normal file
BIN
defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.class
Normal file
Binary file not shown.
132
defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.java
Normal file
132
defects/pgbouncer-0001/test/PgBouncerScramSecretLogTest.java
Normal file
|
|
@ -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$<iterations>:<salt>$<StoredKey>:<ServerKey>"
|
||||
*
|
||||
* 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<String> 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
42
defects/snort3-0002/NOTES.md
Normal file
42
defects/snort3-0002/NOTES.md
Normal file
|
|
@ -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<CHPMatchCandidate>`) 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<CHPApp*, std::size_t> 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 |
|
||||
66
defects/snort3-0002/patch/snort3-0002.patch
Normal file
66
defects/snort3-0002/patch/snort3-0002.patch
Normal file
|
|
@ -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<CHPMatchCandidate> CHPMatchTally;
|
||||
|
||||
+#include <unordered_map>
|
||||
+
|
||||
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<CHPApp*, std::size_t> 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<CHPApp*, std::size_t>& 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);
|
||||
BIN
defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPApp.class
Normal file
BIN
defects/snort3-0002/test/Snort3ChpMatchTallyTest$CHPApp.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
defects/snort3-0002/test/Snort3ChpMatchTallyTest.class
Normal file
BIN
defects/snort3-0002/test/Snort3ChpMatchTallyTest.class
Normal file
Binary file not shown.
203
defects/snort3-0002/test/Snort3ChpMatchTallyTest.java
Normal file
203
defects/snort3-0002/test/Snort3ChpMatchTallyTest.java
Normal file
|
|
@ -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<CHPMatchCandidate>. 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<CHPApp*, size_t> 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<CHPMatchCandidate> 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<CHPMatchCandidate> match_tally,
|
||||
Map<CHPApp, Integer> 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<CHPApp> apps, int matchesPerApp) {
|
||||
List<CHPMatchCandidate> 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<CHPApp> apps, int matchesPerApp) {
|
||||
List<CHPMatchCandidate> tally = new ArrayList<>();
|
||||
Map<CHPApp, Integer> 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<CHPApp> apps = new ArrayList<>();
|
||||
for (int i = 0; i < T; i++) apps.add(new CHPApp(i, 3, i + 10));
|
||||
|
||||
// Build tally with defective impl
|
||||
List<CHPMatchCandidate> 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<CHPMatchCandidate> tallyF = new ArrayList<>();
|
||||
Map<CHPApp, Integer> 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<CHPApp> 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<CHPApp> 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue