java-topology/defects/pgbouncer-0002/test/test_pgbouncer_0002.py
russell@unturf.com 49edd9e440 pgbouncer: 5-MOAD scan; pgbouncer-0002 CWE-407 find_database O(D) linear scan per connection 100x at D=100
pgbouncer-0001 (CWE-312 SCRAM verifier logged at slog_debug) was already documented.
pgbouncer-0002: find_database() uses statlist_for_each + strcmp O(D) on every new
client connection login path. find_global_user() uses aatree_search O(log U) but
DB lookup was never upgraded. Fix: AA-tree or hash index mirroring user_tree pattern.
100x op-count at D=100, 1000x at D=1000. Test PASS. MOADs 0002/0003/0005 CLEAN.
2026-04-03 13:14:43 -04:00

142 lines
4.3 KiB
Python

"""
pgbouncer-0002: CWE-407 — find_database() O(D) linear scan on per-connection login path
Source: src/objects.c, function find_database()
Defect: statlist_for_each + strcmp iterates all D databases on every new client
connection login. User lookup uses aatree_search() O(log U) but DB lookup
was never upgraded from linked-list O(D).
Fix: Add an AA-tree (or hash table) index for database lookup, matching the pattern
used by user_tree for global user lookup.
Benchmark: measures op-count ratio between linear scan (defective) and dict (fixed)
at N=100 and N=1000 databases. Assert speedup > 3x.
"""
import time
import sys
export_PYTHONUNBUFFERED = True # handled by caller via env
# ---- simulate the defective linked-list find_database ----
def find_database_linear(db_list, name):
"""O(D) linear scan — mirrors statlist_for_each + strcmp in objects.c"""
for db_name in db_list:
if db_name == name:
return db_name
return None
# ---- simulate the fixed hash/tree find_database ----
def find_database_hash(db_index, name):
"""O(1) hash lookup — mirrors what an AA-tree or hash index provides"""
return db_index.get(name)
def build_db_list(n):
return [f"tenant_{i:06d}" for i in range(n)]
def build_db_index(db_list):
return {name: name for name in db_list}
def measure_op_count_linear(db_list, queries):
"""Count total strcmp-equivalent ops for all queries against linear list."""
total_ops = 0
for name in queries:
for db_name in db_list:
total_ops += 1
if db_name == name:
break
return total_ops
def measure_op_count_hash(db_index, queries):
"""Count total ops for hash lookup — always 1 per query."""
return len(queries)
def run_benchmark(n, label):
db_list = build_db_list(n)
db_index = build_db_index(db_list)
# Queries: worst-case (last element in list) to stress the linear scan
queries = [db_list[-1]] * 1000 # 1000 connections all hitting last DB
op_count_linear = measure_op_count_linear(db_list, queries)
op_count_hash = measure_op_count_hash(db_index, queries)
# Also measure wall time
t0 = time.perf_counter()
for q in queries:
find_database_linear(db_list, q)
t_linear = time.perf_counter() - t0
t0 = time.perf_counter()
for q in queries:
find_database_hash(db_index, q)
t_hash = time.perf_counter() - t0
ratio_ops = op_count_linear / max(op_count_hash, 1)
ratio_time = t_linear / max(t_hash, 1e-9)
print(f" D={n}: linear={op_count_linear} ops, hash={op_count_hash} ops, "
f"op-ratio={ratio_ops:.1f}x, time-ratio={ratio_time:.1f}x")
return ratio_ops, ratio_time
def test_correctness():
"""Both implementations must return the same result."""
db_list = build_db_list(50)
db_index = build_db_index(db_list)
# Known hit
result_linear = find_database_linear(db_list, "tenant_000025")
result_hash = find_database_hash(db_index, "tenant_000025")
assert result_linear == result_hash == "tenant_000025", (
f"correctness fail: linear={result_linear}, hash={result_hash}"
)
# Known miss
result_linear = find_database_linear(db_list, "does_not_exist")
result_hash = find_database_hash(db_index, "does_not_exist")
assert result_linear is None and result_hash is None, (
f"miss correctness fail: linear={result_linear}, hash={result_hash}"
)
print("PASS correctness: linear and hash agree on hit and miss")
def test_speedup_n100():
ratio_ops, _ = run_benchmark(100, "N=100")
assert ratio_ops >= 3.0, f"FAIL N=100: op-ratio {ratio_ops:.1f}x < 3x threshold"
print(f"PASS N=100: op-ratio {ratio_ops:.1f}x >= 3x")
def test_speedup_n1000():
ratio_ops, _ = run_benchmark(1000, "N=1000")
assert ratio_ops >= 3.0, f"FAIL N=1000: op-ratio {ratio_ops:.1f}x < 3x threshold"
print(f"PASS N=1000: op-ratio {ratio_ops:.1f}x >= 3x")
def main():
print("=== pgbouncer-0002: CWE-407 find_database() O(D) linear scan ===")
print()
print("Correctness check:")
test_correctness()
print()
print("Benchmark — op-count ratio (linear / hash), worst-case queries:")
test_speedup_n100()
test_speedup_n1000()
print()
print("ALL PASS")
return 0
if __name__ == "__main__":
sys.exit(main())