New defects (all PASS): - exim-0001: same_hosts() MX-segment O(H²) → AVL set O(H log H), 10.5x at H=20 - minecraft-0001: DependencySorter.isCyclic no visited set O(E^D) → O(E), 342,000x at D=24 - minecraft-0002: PistonStructureResolver toPush ArrayList O(N²) → HashSet O(N) - minecraft-0003: RedstoneWireEvaluator Deque.contains O(N²) → HashSet O(N) - minecraft-0004: MoveThroughVillageGoal visited List O(N²) → HashSet O(N) - mpich-0001: group_lpid_to_rank O(N²) → HashMap O(N), 313x at N=1000 - ompi-0001: group_overlap process-name scan O(N×M) → HashMap O(N+M), 2048x - pcl-0001: RegionGrowing::getSegmentFromPoint O(C×S) → point_labels[] O(1), 50000x CLEAN confirmed: esbuild, express, koa, ktor, lucene, mpich-recvq, ompi-startup, prosody, roda, rust/rustc-wave2, signal-server, solana, wiredtiger, wireguard-tools, linux-kernel (pointer to linux/)
91 lines
3.6 KiB
Diff
91 lines
3.6 KiB
Diff
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 <stddef.h> /* 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. */
|