# 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)