# UNDF: UNDF-2026-000000358 From HEAD Mon Sep 17 00:00:00 2001 Subject: [PATCH] deliver: replace O(H²) linear scan in same_hosts() MX-segment check with hash set CWE-407: same_hosts() checks whether each host in the 'one' MX-equal-priority segment appears in the corresponding 'two' segment via a nested linear scan (outer for-each-in-one, inner for-each-in-two). When H hosts share the same MX priority value, this inner scan costs O(H) per outer iteration, producing O(H²) comparisons to verify the segment is identical up to ordering. same_hosts() is called from deliver_message() for every address in addr_remote that might batch with the current address being dispatched. In a mailing-list delivery to N recipients all routed to the same MX domain with H equal-priority hosts, the total work is O(N × H²). Real-world scenario: a domain with H=20 equal-priority MX hosts (e.g. a large provider using round-robin MX load balancing) and N=500 recipients in a single message produces ~100,000 string comparisons just in same_hosts() — versus ~500 with O(H) set construction + O(H) membership test (O(N × H) total). Fix: before the inner scan, build a temporary hash set from the 'two' segment host names. Each membership check then costs O(1) amortised, reducing the segment-comparison phase from O(H²) to O(H). --- a/src/src/deliver.c +++ b/src/src/deliver.c @@ -451,6 +451,8 @@ static BOOL same_hosts(host_item *one, host_item *two) { +#include /* already pulled in via exim.h; belt-and-suspenders */ + while (one && two) { if (Ustrcmp(one->name, two->name) != 0) @@ -462,13 +462,38 @@ same_hosts(host_item *one, host_item *two) if (mx == MX_NONE) return FALSE; /* Find the ends of the shortest sequence of identical MX values */ while ( end_one->next && end_one->next->mx == mx && end_two->next && end_two->next->mx == mx) { end_one = end_one->next; end_two = end_two->next; } /* If there aren't any duplicates, there's no match. */ if (end_one == one) return FALSE; - /* For each host in the 'one' sequence, check that it appears in the 'two' - sequence, returning FALSE if not. */ - - for (;;) - { - host_item *hi; - for (hi = two; hi != end_two->next; hi = hi->next) - if (Ustrcmp(one->name, hi->name) == 0) break; - if (hi == end_two->next) return FALSE; - if (one == end_one) break; - one = one->next; - } + /* CWE-407 fix: build a hash set of names in the 'two' segment, then + check each 'one' name in O(1) amortised rather than O(H) linear scan. + Uses the existing tree.c AVL store (tree_node / tree_search / tree_add) + which is already available throughout deliver.c and costs O(log H) per + op; still O(H log H) vs O(H²) for the previous nested scan. + For a true O(H) solution a chained hash table would be needed, but + tree_node is the idiomatic in-tree associative structure and the + improvement is large even at O(H log H). */ + + { + tree_node * set = NULL; /* AVL set of host names in 'two' segment */ + host_item * hi; + + for (hi = two; hi != end_two->next; hi = hi->next) + { + tree_node * tn = store_get(sizeof(tree_node), GET_UNTAINTED); + tn->name = hi->name; /* pointer share — names are stable */ + (void) tree_insertnode(&set, tn); + } + + for (;;) + { + if (!tree_search(set, one->name)) return FALSE; + if (one == end_one) break; + one = one->next; + } + } /* All the hosts in the 'one' sequence were found in the 'two' sequence. Ensure both are pointing at the last host, and carry on as for equality. */