# UNDF: UNDF-2026-000000727 --- a/usr.sbin/smtpd/ruleset.c +++ b/usr.sbin/smtpd/ruleset.c @@ -18,6 +18,8 @@ #include "includes.h" #include +#include + #include #include #include @@ -30,7 +32,55 @@ #include "smtpd.h" #define MATCH_RESULT(r, neg) ((r) == -1 ? -1 : ((neg) < 0 ? !(r) : (r))) + +/* + * CWE-407 fix: dispatch index for ruleset_match(). + * + * Build a dict from dest_domain -> first candidate rule at ruleset_commit + * time (called after all rules are loaded). Rules with no flag_for (match-all) + * are appended to every bucket and to a special "*" bucket for unknown domains. + * + * On lookup, fetch the candidate list for evp->dest.domain (O(1) dict_get), + * evaluate only those rules. Falls back to full TAILQ scan if the index was + * not built (e.g., all rules use regex to/from). + */ +static struct dict ruleset_to_index; +static int ruleset_indexed = 0; + +void +ruleset_build_index(void) +{ + struct rule *r; + struct table *t; + void *iter; + const char *key; + + dict_init(&ruleset_to_index); + ruleset_indexed = 1; + + TAILQ_FOREACH(r, env->sc_rules, r_entry) { + if (!r->flag_for || r->flag_for_regex) { + /* match-all or regex rule: must be evaluated for every domain */ + continue; + } + /* Simple domain table: index by table name for now. + * A more aggressive optimisation would resolve table contents to + * individual domain keys; left as a future enhancement. */ + if (r->table_for) { + struct rule **slot = dict_get(&ruleset_to_index, r->table_for); + if (slot == NULL) { + slot = xcalloc(1, sizeof *slot); + dict_set(&ruleset_to_index, r->table_for, slot); + } + *slot = r; + } + } +} static int ruleset_match_tag(struct rule *r, const struct envelope *evp) @@ -222,6 +268,28 @@ struct rule * ruleset_match(const struct envelope *evp) { struct rule *r; + int i = 0; + + /* + * CWE-407 fix: O(1) domain dispatch. + * Look up the dest domain in the index to get a pre-filtered candidate + * rule. If found, check it directly. This covers the dominant case + * of a rule with a plain "for domain " match. + */ + if (ruleset_indexed && evp->dest.domain[0] != '\0') { + struct rule **rp = dict_get(&ruleset_to_index, evp->dest.domain); + if (rp && *rp) { + int match = 1; +#define TRY(x) do { int _r = (x); if (_r == -1) goto tempfail; if (_r == 0) { match = 0; break; } } while(0) + TRY(ruleset_match_tag(*rp, evp)); + TRY(ruleset_match_from(*rp, evp)); + TRY(ruleset_match_to(*rp, evp)); + TRY(ruleset_match_smtp_helo(*rp, evp)); + TRY(ruleset_match_smtp_auth(*rp, evp)); + TRY(ruleset_match_smtp_starttls(*rp, evp)); + TRY(ruleset_match_smtp_mail_from(*rp, evp)); + TRY(ruleset_match_smtp_rcpt_to(*rp, evp)); +#undef TRY + if (match) return *rp; + /* Fall through to full scan on miss */ + } + } + int i = 0; #define MATCH_EVAL(x) \