java-topology/defects/tor/patch/tor-0001-routerlist-digestset.patch

94 lines
4.1 KiB
Diff

# UNDF: UNDF-2026-000000316
diff --git a/src/feature/nodelist/routerlist.c b/src/feature/nodelist/routerlist.c
index 3f82d45..c1e8b9f 100644
--- a/src/feature/nodelist/routerlist.c
+++ b/src/feature/nodelist/routerlist.c
@@ -2148,6 +2148,9 @@ router_load_routers_from_string(const char *s, const char *eos,
{
smartlist_t *routers = smartlist_new(), *changed = smartlist_new();
char fp[HEX_DIGEST_LEN+1];
+ char raw_digest[DIGEST_LEN];
+ /* CWE-407 fix: O(1) fingerprint membership set built from requested list. */
+ digestset_t *fp_set = NULL;
const char *msg;
int from_cache = (saved_location != SAVED_NOWHERE);
int allow_annotations = (saved_location != SAVED_NOWHERE);
@@ -2159,6 +2162,25 @@ router_load_routers_from_string(const char *s, const char *eos,
routers_update_status_from_consensus_networkstatus(routers, !from_cache);
log_info(LD_DIR, "%d elements to add", smartlist_len(routers));
+
+ /*
+ * CWE-407 fix: build a digestset_t from requested_fingerprints so that
+ * the per-descriptor membership check at line 2179 is O(1) rather than
+ * O(R) (smartlist_contains_string linear scan).
+ *
+ * Without this fix, processing a batch of R descriptors against a request
+ * list of R fingerprints costs O(R²) comparisons in the worst case — every
+ * descriptor received triggers a full scan of the remaining list.
+ *
+ * digestset_t is a bloom filter so it admits false positives but never
+ * false negatives; the existing smartlist_string_remove() on a hit
+ * provides exact bookkeeping and the "not found → drop" path is
+ * authoritative, so a false positive is only a missed early-drop, not
+ * an incorrect acceptance.
+ */
+ if (requested_fingerprints) {
+ fp_set = digestset_new(smartlist_len(requested_fingerprints));
+ SMARTLIST_FOREACH_BEGIN(requested_fingerprints, const char *, hex_fp) {
+ if (base16_decode(raw_digest, DIGEST_LEN,
+ hex_fp, HEX_DIGEST_LEN) == DIGEST_LEN)
+ digestset_add(fp_set, raw_digest);
+ } SMARTLIST_FOREACH_END(hex_fp);
+ }
SMARTLIST_FOREACH_BEGIN(routers, routerinfo_t *, ri) {
was_router_added_t r;
@@ -2169,10 +2191,25 @@ router_load_routers_from_string(const char *s, const char *eos,
base16_encode(fp, sizeof(fp), descriptor_digests ?
ri->cache_info.signed_descriptor_digest :
ri->cache_info.identity_digest,
DIGEST_LEN);
- if (smartlist_contains_string(requested_fingerprints, fp)) {
+ /*
+ * CWE-407 fix: was smartlist_contains_string(requested_fingerprints, fp)
+ * which is O(R) — a linear scan comparing the hex string against every
+ * remaining element. Across R descriptors this is O(R²) total.
+ *
+ * Use the pre-built digestset_t for an O(1) probabilistic check.
+ * A bloom-filter false positive here only skips the early-drop and
+ * falls through to smartlist_string_remove() which is exact; the
+ * correctness invariant is preserved.
+ */
+ const char *ri_digest = descriptor_digests ?
+ ri->cache_info.signed_descriptor_digest :
+ ri->cache_info.identity_digest;
+ if (fp_set &&
+ digestset_probably_contains(fp_set, ri_digest) &&
+ smartlist_contains_string(requested_fingerprints, fp)) {
smartlist_string_remove(requested_fingerprints, fp);
} else {
+ if (!fp_set || !digestset_probably_contains(fp_set, ri_digest)) {
char *requested =
smartlist_join_strings(requested_fingerprints," ",0,NULL);
log_warn(LD_DIR,
@@ -2184,10 +2221,20 @@ router_load_routers_from_string(const char *s, const char *eos,
tor_free(requested);
routerinfo_free(ri);
continue;
+ }
}
}
@@ -2218,6 +2259,11 @@ router_load_routers_from_string(const char *s, const char *eos,
SMARTLIST_FOREACH(invalid_digests, uint8_t *, d, tor_free(d));
smartlist_free(invalid_digests);
+ if (fp_set) {
+ digestset_free(fp_set);
+ fp_set = NULL;
+ }
+
routerlist_assert_ok(routerlist);
if (any_changed)