java-topology/defects/exim/patch/exim-0001-same-hosts-mx-segment-hashset.md

119 lines
4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000358
## Classification
| Field | Value |
|-------------|-------|
| CWE | CWE-407 Inefficient Algorithmic Complexity |
| Severity | MEDIUM |
| Component | `src/src/deliver.c:482-490` |
| Function | `same_hosts()` — MX-equal-priority segment membership check |
| Hot path | Called O(N) times per message during remote delivery batching |
| Status | PATCHED (unit test PASS) |
## Defect
`same_hosts()` is called by `deliver_message()` to determine whether two
remote addresses can be batched into the same SMTP delivery transaction.
It compares two host lists for equivalence, allowing reordering within
equal-MX-priority groups.
When two host lists share a group of H hosts at the same MX priority, the
function verifies membership using a nested linear scan:
```c
/* deliver.c:479-490 */
/* 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) /* O(H) inner scan */
if (Ustrcmp(one->name, hi->name) == 0) break;
if (hi == end_two->next) return FALSE;
if (one == end_one) break;
one = one->next; /* O(H) outer iterations */
}
```
For a segment of H equal-priority hosts this costs O(H²) string comparisons.
`same_hosts()` is called from the address-grouping loop at `deliver.c:4527`:
```c
while ((next = *anchor) && address_count < address_count_max)
{
if ( ...
&& same_hosts(next->host_list, addr->host_list) /* O(H²) per call */
...
```
The outer loop runs over all N remote addresses not yet batched. For a
mailing-list message with N recipients all routed to the same domain, total
cost is O(N × H²).
## Complexity proof
| Scenario | N recipients | H equal-MX hosts | `same_hosts` ops | Comparison |
|----------|-------------|-----------------|-----------------|------------|
| Small | 50 | 5 | 50 × 25 = 1,250 | — |
| Typical | 500 | 10 | 500 × 100 = 50,000 | baseline |
| High-MX | 500 | 20 | 500 × 400 = 200,000 | 4× worse |
| Extreme | 1,000 | 40 | 1,000 × 1,600 = 1,600,000 | 32× worse |
After fix (O(H log H) per call using AVL tree set):
| Scenario | Cost after fix | Speedup |
|----------|---------------|---------|
| High-MX | 500 × 20×5 = 50,000 | ~4× |
| Extreme | 1,000 × 40×6 = 240,000 | ~6× |
With a proper O(1) hash set the speedup at H=40 would be ~1,600×.
## Real-world trigger
Any domain that advertises H ≥ 2 MX records with equal priority and uses DNS
randomisation to load-balance triggers the MX-segment path. Large providers
(Google Workspace, Outlook, large self-hosted setups with HA MX pairs) commonly
use equal-priority MX pairs. H=2 is the common case; H=5-10 is not unusual.
## Fix
Before the nested scan, build an AVL tree set (using exim's existing
`tree_insertnode` / `tree_search` from `tree.c`) from the 'two' segment host
names. Membership checks then cost O(log H) each instead of O(H), reducing
total segment work from O(H²) to O(H log H).
```c
/* CWE-407 fix: build AVL set of 'two' host names; check each 'one' in O(log H) */
{
tree_node * set = NULL;
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;
(void) tree_insertnode(&set, tn);
}
for (;;)
{
if (!tree_search(set, one->name)) return FALSE;
if (one == end_one) break;
one = one->next;
}
}
```
See `exim-0001-same-hosts-mx-segment-hashset.patch` for the unified diff.
## Op-count verification
Unit test `EximSameHosts0001Test.java` measures `Ustrcmp`-equivalent string
comparison counts for H=20 equal-priority hosts, N=100 address pairs.
| Implementation | Op count (H=20, N=100) | Ratio |
|----------------|------------------------|-------|
| Before (linear) | 40,000 | baseline |
| After (AVL) | ≤ 9,000 | ≥ 4.4× |