java-topology/defects/tor/unit/TorNodelistFamilyTest.java
russell@unturf.com 9934133dcf whitepaper: 312 sites / 151 ecosystems — wave2+3 defect tables and PDF rebuild
Add 88 new defect entries to HIGH and MEDIUM tables:
  HIGH: mysql-0001/0002, mariadb-0001, redis-0001/0002, valkey-0001/0002, openvpn-0001,
        vlc-0001, prometheus-0001, otel-collector-0001, cockroachdb-0001..0004,
        tidb-0001..0008, kubernetes-0001/0002, go-0001, kotlin-0002, scala-0001,
        allegro5-0001, sdl2-0001, grafana-0001, clickhouse-0001, duckdb-0001,
        mongodb-0001, envoy-0001, istio-0001, cilium-0001, linkerd2-0001,
        linux-0001/0002/0003, tor-0002/0003, curl-0001, julia-0001, lua-0001,
        perl5-0001, nats-0001, spring-0003/0004, tomcat-0001, onos-0002, odl-0002

  MEDIUM: helm-0001, mariadb-0002, openssl-0001/0002, memcached-0001,
          cassandra-0001..0004, flink-0001, storm-0001/0002, zookeeper-0001..0003,
          pip-0001, gradle-0001, nginx-0001, haproxy-0001, caddy-0001, varnish-0001,
          ffmpeg-0001, gstreamer-0001, raylib-0001, love2d-0001, php-0001/0002,
          r-source-0001, cpython-0002, ruby-0001, rabbitmq-0003/0004, activemq-0001,
          ovs-0001, onos-0003, odl-0002, jetty-0001

PDF: 976K
2026-03-27 15:23:43 -04:00

193 lines
7.3 KiB
Java

package unit;
import java.util.*;
/**
* Unit test for tor-0002: nodelist_add_node_and_family() CWE-407.
*
* Defect: nodelist_add_node_and_family() (nodelist.c:2337) iterates all_nodes
* (size N) and for each node2 calls nodes_have_common_family_id(node, node2).
* nodes_have_common_family_id iterates ids_a and for each id calls
* smartlist_contains_string(ids_b, id) — a full linear scan of ids_b.
* Total cost when no match: O(N * |ids_a| * |ids_b|) = O(N * F^2).
*
* Fix: Before the outer loop, build a HashSet from node's own family IDs.
* For each node2, iterate node2's ids and do O(1) HashSet.contains().
* Total cost: O(N * F).
*
* Model:
* DefectiveMatcher — for each node2: nested loop scan of ids_b per id in ids_a (O(F^2))
* FixedMatcher — HashSet built once from ids_a; per node2: one pass over ids_b (O(F))
*
* Worst-case scenario: source node's IDs are disjoint from all candidate nodes' IDs.
* This is the common case (most relays are NOT in the same family).
* The full scan always runs in the defective path; the fixed path exits early if found.
*/
public class TorNodelistFamilyTest {
// ── Membership implementations ────────────────────────────────────────────
/**
* Defective: simulates nodes_have_common_family_id() —
* outer loop over ids_a, inner smartlist_contains_string scan of ids_b.
* Returns total comparison count. No short-circuit on IDs in different families.
*/
static long defectiveCheck(List<String> ids_a, List<String> ids_b) {
long ops = 0;
for (String id : ids_a) {
for (String candidate : ids_b) {
ops++;
if (id.equals(candidate)) {
return ops; // short-circuit on match (mirrors C code)
}
}
}
return ops;
}
/**
* Fixed: build a HashSet from ids_a once (caller does this before outer loop).
* Per call: iterate ids_b and call HashSet.contains() — O(1) per check.
*/
static long fixedCheck(Set<String> id_set, List<String> ids_b) {
long ops = 0;
for (String id : ids_b) {
ops++; // one O(1) hash lookup per id
if (id_set.contains(id)) {
return ops;
}
}
return ops;
}
// ── Test data generation ──────────────────────────────────────────────────
/**
* Generate F family IDs for a node. IDs are globally unique (no sharing),
* modelling the worst case: no family members, full scan required every time.
*/
static List<String> makeDisjointIds(int nodeIndex, int F) {
List<String> ids = new ArrayList<>();
for (int j = 0; j < F; j++) {
ids.add("fam:" + nodeIndex + ":" + j);
}
return ids;
}
/**
* Generate IDs where the last ID of ids_a matches the last ID of ids_b.
* Models the worst-case scan depth: match only found at end of both lists.
*/
static List<String> makeLastMatchIds(int nodeIndex, int F, String sharedId) {
List<String> ids = new ArrayList<>();
for (int j = 0; j < F - 1; j++) {
ids.add("fam:" + nodeIndex + ":" + j);
}
ids.add(sharedId); // shared at end — maximises scan depth
return ids;
}
// ── Benchmark ────────────────────────────────────────────────────────────
/**
* All-disjoint case: every pair has no match — maximum scan for slow path.
* Slow: O(N * F^2). Fast: O(N * F).
*/
static long[] runDisjoint(int N, int F) {
List<String> sourceIds = makeDisjointIds(0, F);
List<List<String>> allNodes = new ArrayList<>();
for (int i = 1; i <= N; i++) {
allNodes.add(makeDisjointIds(i, F));
}
long slowOps = 0;
for (List<String> otherIds : allNodes) {
slowOps += defectiveCheck(sourceIds, otherIds);
}
Set<String> id_set = new HashSet<>(sourceIds);
long fastOps = 0;
for (List<String> otherIds : allNodes) {
fastOps += fixedCheck(id_set, otherIds);
}
return new long[]{slowOps, fastOps};
}
/**
* Last-match case: match only at the end of both id lists.
* Slow: O(N * F^2) worst-case depth. Fast: O(N * F).
*/
static long[] runLastMatch(int N, int F) {
String shared = "shared:family:id";
List<String> sourceIds = makeLastMatchIds(0, F, shared);
List<List<String>> allNodes = new ArrayList<>();
for (int i = 1; i <= N; i++) {
allNodes.add(makeLastMatchIds(i, F, shared));
}
long slowOps = 0;
for (List<String> otherIds : allNodes) {
slowOps += defectiveCheck(sourceIds, otherIds);
}
Set<String> id_set = new HashSet<>(sourceIds);
long fastOps = 0;
for (List<String> otherIds : allNodes) {
fastOps += fixedCheck(id_set, otherIds);
}
return new long[]{slowOps, fastOps};
}
// ── Main ─────────────────────────────────────────────────────────────────
public static void main(String[] args) {
int passed = 0;
int total = 0;
// Disjoint scenario: no matches, full scan always.
// Ratio = F (slow scans F ids per pair; fast does 1 hash check per id in ids_b = F total).
// slow = N*F*F, fast = N*F → ratio = F.
int[][] disjointCfg = {
{50, 3, 2}, // ratio=3
{200, 5, 4}, // ratio=5
{500, 5, 4}, // ratio=5
{500, 10, 9}, // ratio=10
};
for (int[] cfg : disjointCfg) {
int N = cfg[0], F = cfg[1], minFactor = cfg[2];
total++;
long[] ops = runDisjoint(N, F);
long slowOps = ops[0], fastOps = ops[1];
boolean ok = slowOps > fastOps * minFactor;
System.out.printf("tor-0002 disjoint N=%4d F=%2d: slow=%7d fast=%5d ratio=%.1fx %s%n",
N, F, slowOps, fastOps, (double) slowOps / fastOps,
ok ? "PASS" : "FAIL");
if (ok) passed++;
}
// Last-match scenario: match at end of both lists
int[][] lastMatchCfg = {
{50, 3, 2},
{200, 5, 4},
{500, 8, 7},
};
for (int[] cfg : lastMatchCfg) {
int N = cfg[0], F = cfg[1], minFactor = cfg[2];
total++;
long[] ops = runLastMatch(N, F);
long slowOps = ops[0], fastOps = ops[1];
boolean ok = slowOps > fastOps * minFactor;
System.out.printf("tor-0002 lastmatch N=%4d F=%2d: slow=%7d fast=%5d ratio=%.1fx %s%n",
N, F, slowOps, fastOps, (double) slowOps / fastOps,
ok ? "PASS" : "FAIL");
if (ok) passed++;
}
System.out.printf("%d/%d PASS%n", passed, total);
if (passed != total) System.exit(1);
}
}