]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH] lib-imap: imap-match - Fix excessive CPU usage caused by backtracking
authorNoah Meyerhans <noahm@debian.org>
Thu, 10 Sep 2026 13:59:18 +0000 (09:59 -0400)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
Backport the NFA/non-backtracking algorithm from 2684624… while retaining the
byte-oriented matcher semantics of 2.4.1.

Original commit message:

Replace the recursive backtracking matcher with a Thompson-style NFA
simulation over grapheme clusters.

Each grapheme cluster of the compressed pattern becomes one state:
LITERAL, PERCENT (consume any number of non-separator clusters) or STAR
(consume any number of clusters including separators).  A virtual
ACCEPT position sits at index n_states.  Simulation tracks the set of
active positions in a bitmap.  Epsilon-closure (skipping a PERCENT or
STAR without consuming) is a single forward pass because the NFA is
linear: each state can only epsilon-skip to i+1.

The resulting match is O(n_data * n_pattern) regardless of pattern
shape, with no recursion and no backtracking, so there is no way for
a malicious pattern or mailbox name to trigger exponential CPU,
excessive stack depth, or unbounded memory.

IMAP_MATCH_YES / NO / CHILDREN / PARENT semantics are preserved:

- YES:      ACCEPT reachable after consuming all data.
- PARENT:   ACCEPT was active at some point while the next data
         grapheme cluster was the separator.
- CHILDREN: some active non-ACCEPT state remains after consuming all
         data, and either the data ends with a separator or an
         active state can still consume a separator (precomputed
         as sep_accept[]).

Inboxcase handling (case-insensitive comparison for the INBOX prefix
of data) and grapheme-cluster comparison are unchanged - the existing
match_gc logic is reused inline as literal_matches().
pattern_compress() and pattern_is_inboxcase() are unchanged.

Gbp-Pq: Name 0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch

src/lib-imap/imap-match.c
src/lib-imap/test-imap-match.c

index 8603e004fb488d85a0813d20baafdb3cddf75760..fec5a3c242c05ce134d31e4807afb0a5f5e3fcba 100644 (file)
@@ -9,9 +9,44 @@
 
 #include <ctype.h>
 
+/* Pattern matching is implemented as a Thompson-style NFA simulation.
+   Each byte in the compressed pattern becomes one NFA state:
+
+     LITERAL - must consume a matching byte, with inboxcase fallback
+     PERCENT - may consume any number of non-separator bytes
+     STAR    - may consume any number of bytes, including separators
+
+   A virtual ACCEPT position sits at index n_states.  Simulation tracks the
+   set of active positions in boolean arrays.  Epsilon transitions (skipping
+   a PERCENT or STAR without consuming) are applied as a single forward pass,
+   because the NFA is linear: each state can only epsilon-skip to i+1.
+
+   Complexity is O(n_data * n_pattern), regardless of wildcard count or
+   pattern shape, so there is no recursive backtracking and no way for a
+   malicious pattern or mailbox name to trigger exponential CPU usage.
+
+   This 2.4.1 backport keeps the historical byte-oriented matching model.
+   The upstream fix operates on grapheme clusters, but backporting that would
+   require additional Unicode matching changes outside the minimal ReDoS fix. */
+
+enum imap_match_nfa_type {
+       IMAP_MATCH_NFA_LITERAL = 0,
+       IMAP_MATCH_NFA_PERCENT,
+       IMAP_MATCH_NFA_STAR
+};
+
+struct imap_match_nfa_state {
+       enum imap_match_nfa_type type;
+       bool sep_accept;
+       unsigned char ch;
+};
+
 struct imap_match_pattern {
        const char *pattern;
        bool inboxcase;
+
+       unsigned int n_states;
+       struct imap_match_nfa_state *states;
 };
 
 struct imap_match_glob {
@@ -23,13 +58,6 @@ struct imap_match_glob {
        char patterns_data[FLEXIBLE_ARRAY_MEMBER];
 };
 
-struct imap_match_context {
-       const char *inboxcase_end;
-
-       char sep;
-       bool inboxcase;
-};
-
 /* name of "INBOX" - must not have repeated substrings */
 static const char inbox[] = "INBOX";
 #define INBOXLEN (sizeof(inbox) - 1)
@@ -108,6 +136,48 @@ static bool pattern_is_inboxcase(const char *pattern, char separator)
        return TRUE;
 }
 
+static void
+imap_match_compile(pool_t pool, struct imap_match_pattern *pat, char sep)
+{
+       unsigned int i;
+
+       pat->n_states = strlen(pat->pattern);
+       pat->states = pat->n_states == 0 ? NULL :
+               p_new(pool, struct imap_match_nfa_state, pat->n_states);
+
+       for (i = 0; i < pat->n_states; i++) {
+               struct imap_match_nfa_state *state = &pat->states[i];
+               unsigned char ch = (unsigned char)pat->pattern[i];
+
+               if (ch == '%')
+                       state->type = IMAP_MATCH_NFA_PERCENT;
+               else if (ch == '*')
+                       state->type = IMAP_MATCH_NFA_STAR;
+               else {
+                       state->type = IMAP_MATCH_NFA_LITERAL;
+                       state->ch = ch;
+               }
+       }
+
+       for (i = pat->n_states; i > 0; i--) {
+               unsigned int idx = i - 1;
+               const struct imap_match_nfa_state *state = &pat->states[idx];
+               bool consume_sep =
+                       state->type == IMAP_MATCH_NFA_STAR ||
+                       (state->type == IMAP_MATCH_NFA_LITERAL &&
+                        state->ch == (unsigned char)sep);
+               bool eps_skippable =
+                       state->type == IMAP_MATCH_NFA_PERCENT ||
+                       state->type == IMAP_MATCH_NFA_STAR;
+               bool next_sep_accept =
+                       idx + 1 < pat->n_states ?
+                       pat->states[idx + 1].sep_accept : FALSE;
+
+               pat->states[idx].sep_accept =
+                       consume_sep || (eps_skippable && next_sep_accept);
+       }
+}
+
 static struct imap_match_glob *
 imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
                              bool inboxcase, char separator)
@@ -138,7 +208,7 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
        glob->pool = pool;
        glob->sep = separator;
 
-       /* copy pattern strings to our allocated memory */
+       /* copy pattern strings to our allocated memory and compile NFAs */
        for (i = 0, pos = 0; i < patterns_count; i++) {
                len = strlen(match_patterns[i].pattern) + 1;
                i_assert(pos + len <= patterns_data_len);
@@ -148,6 +218,8 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
                       match_patterns[i].pattern, len);
                match_patterns[i].pattern = glob->patterns_data + pos;
                pos += len;
+
+               imap_match_compile(pool, &match_patterns[i], separator);
        }
        glob->patterns = match_patterns;
        return glob;
@@ -172,8 +244,15 @@ imap_match_init_multiple(pool_t pool, const char *const *patterns,
 
 void imap_match_deinit(struct imap_match_glob **glob)
 {
+       struct imap_match_pattern *p;
+
        if (glob == NULL || *glob == NULL)
                return;
+
+       for (p = (*glob)->patterns; p->pattern != NULL; p++) {
+               if (p->states != NULL)
+                       p_free((*glob)->pool, p->states);
+       }
        p_free((*glob)->pool, (*glob)->patterns);
        p_free((*glob)->pool, *glob);
        *glob = NULL;
@@ -230,148 +309,136 @@ bool imap_match_globs_equal(const struct imap_match_glob *glob1,
        return p1->pattern == p2->pattern;
 }
 
-#define CMP_CUR_CHR(ctx, data, pattern) \
-       (*(data) == *(pattern) || \
-        (i_toupper(*(data)) == i_toupper(*(pattern)) && \
-        (data) < (ctx)->inboxcase_end))
+static bool
+literal_matches(const struct imap_match_nfa_state *state,
+               unsigned char data_ch, bool inboxcase_pos)
+{
+       if (state->ch == data_ch)
+               return TRUE;
+       return inboxcase_pos &&
+               i_toupper(data_ch) == i_toupper(state->ch);
+}
 
-static enum imap_match_result
-match_sub(struct imap_match_context *ctx, const char **data_p,
-         const char **pattern_p)
+static void
+nfa_eps_close(const struct imap_match_pattern *pat, bool *bits)
 {
-       enum imap_match_result ret, match;
        unsigned int i;
-       const char *data = *data_p, *pattern = *pattern_p;
-
-       /* match all non-wildcards */
-       i = 0;
-       while (pattern[i] != '\0' && pattern[i] != '*' && pattern[i] != '%') {
-               if (!CMP_CUR_CHR(ctx, data+i, pattern+i)) {
-                       if (data[i] != '\0')
-                               return IMAP_MATCH_NO;
-                       if (pattern[i] == ctx->sep)
-                               return IMAP_MATCH_CHILDREN;
-                       if (i > 0 && pattern[i-1] == ctx->sep) {
-                               /* data="foo/" pattern = "foo/bar/%" */
-                               return IMAP_MATCH_CHILDREN;
-                       }
-                       return IMAP_MATCH_NO;
-               }
-               i++;
-       }
-       data += i;
-       pattern += i;
 
-       if (*data == '\0' && *data_p != data && data[-1] == ctx->sep &&
-           *pattern != '\0') {
-               /* data="/" pattern="/%..." */
-               match = IMAP_MATCH_CHILDREN;
-       } else {
-               match = IMAP_MATCH_NO;
+       for (i = 0; i < pat->n_states; i++) {
+               if (!bits[i])
+                       continue;
+               if (pat->states[i].type == IMAP_MATCH_NFA_PERCENT ||
+                   pat->states[i].type == IMAP_MATCH_NFA_STAR)
+                       bits[i + 1] = TRUE;
        }
-       while (*pattern == '%') {
-               pattern++;
-
-               if (*pattern == '\0') {
-                       /* match, if this is the last hierarchy */
-                       while (*data != '\0' && *data != ctx->sep)
-                               data++;
-                       break;
-               }
-
-               /* skip over this hierarchy */
-               while (*data != '\0') {
-                       if (CMP_CUR_CHR(ctx, data, pattern)) {
-                               ret = match_sub(ctx, &data, &pattern);
-                               if (ret == IMAP_MATCH_YES)
-                                       break;
-
-                               match |= ret;
-                       }
-
-                       if (*data == ctx->sep)
-                               break;
+}
 
-                       data++;
-               }
+static enum imap_match_result
+imap_match_pattern_run(const struct imap_match_pattern *pat,
+                      const char *data, char sep, bool inboxcase_pattern)
+{
+       const char *inboxcase_end = data;
+       unsigned int n_bits = pat->n_states + 1;
+       enum imap_match_result result = IMAP_MATCH_NO;
+       bool parent_flag = FALSE;
+       bool data_ends_with_sep = FALSE;
+       bool *cur, *next;
+       const unsigned char *p;
+
+       if (inboxcase_pattern &&
+           strncasecmp(data, inbox, INBOXLEN) == 0 &&
+           (data[INBOXLEN] == '\0' || data[INBOXLEN] == sep)) {
+               inboxcase_end += INBOXLEN;
        }
 
-       if (*pattern != '*') {
-               if (*data == '\0' && *pattern != '\0') {
-                       if (*pattern == ctx->sep)
-                               match |= IMAP_MATCH_CHILDREN;
-                       return match;
-               }
+       cur = t_new(bool, n_bits);
+       next = t_new(bool, n_bits);
+       memset(cur, 0, n_bits * sizeof(*cur));
+       memset(next, 0, n_bits * sizeof(*next));
 
-               if (*data != '\0') {
-                       if (*pattern == '\0' && *data == ctx->sep)
-                               match |= IMAP_MATCH_PARENT;
-                       return match;
-               }
-       }
+       cur[0] = TRUE;
+       nfa_eps_close(pat, cur);
 
-       *data_p = data;
-       *pattern_p = pattern;
-       return IMAP_MATCH_YES;
-}
+       for (p = (const unsigned char *)data; *p != '\0'; p++) {
+               unsigned int i;
+               unsigned char ch = *p;
+               bool ch_is_sep = ch == (unsigned char)sep;
+               bool inboxcase_pos = (const char *)p < inboxcase_end;
 
-static enum imap_match_result
-imap_match_pattern(struct imap_match_context *ctx,
-                  const char *data, const char *pattern)
-{
-       enum imap_match_result ret, match;
+               if (ch_is_sep && cur[pat->n_states])
+                       parent_flag = TRUE;
 
-       ctx->inboxcase_end = data;
-       if (ctx->inboxcase && strncasecmp(data, inbox, INBOXLEN) == 0 &&
-           (data[INBOXLEN] == '\0' || data[INBOXLEN] == ctx->sep)) {
-               /* data begins with INBOX/, use case-insensitive comparison
-                  for it */
-               ctx->inboxcase_end += INBOXLEN;
-       }
+               memset(next, 0, n_bits * sizeof(*next));
+               for (i = 0; i < pat->n_states; i++) {
+                       const struct imap_match_nfa_state *state;
 
-       if (*pattern != '*') {
-               /* handle the pattern up to the first '*' */
-               ret = match_sub(ctx, &data, &pattern);
-               if (ret != IMAP_MATCH_YES || *pattern == '\0')
-                       return ret;
-       }
+                       if (!cur[i])
+                               continue;
 
-       match = IMAP_MATCH_CHILDREN;
-       while (*pattern == '*') {
-               pattern++;
+                       state = &pat->states[i];
+                       switch (state->type) {
+                       case IMAP_MATCH_NFA_LITERAL:
+                               if (literal_matches(state, ch, inboxcase_pos))
+                                       next[i + 1] = TRUE;
+                               break;
+                       case IMAP_MATCH_NFA_PERCENT:
+                               if (!ch_is_sep)
+                                       next[i] = TRUE;
+                               break;
+                       case IMAP_MATCH_NFA_STAR:
+                               next[i] = TRUE;
+                               break;
+                       }
+               }
 
-               if (*pattern == '\0')
-                       return IMAP_MATCH_YES;
+               {
+                       bool *tmp = cur;
+                       cur = next;
+                       next = tmp;
+               }
+               nfa_eps_close(pat, cur);
+               data_ends_with_sep = ch_is_sep;
+       }
 
-               while (*data != '\0') {
-                       if (CMP_CUR_CHR(ctx, data, pattern)) {
-                               ret = match_sub(ctx, &data, &pattern);
-                               if (ret == IMAP_MATCH_YES)
-                                       break;
-                               match |= ret;
+       if (cur[pat->n_states])
+               result = IMAP_MATCH_YES;
+       else {
+               unsigned int i;
+               bool has_nonaccept = FALSE;
+               bool has_sep_accept = FALSE;
+
+               for (i = 0; i < pat->n_states; i++) {
+                       if (!cur[i])
+                               continue;
+                       has_nonaccept = TRUE;
+                       if (pat->states[i].sep_accept) {
+                               has_sep_accept = TRUE;
+                               break;
                        }
-
-                       data++;
                }
+
+               if (has_nonaccept && (data_ends_with_sep || has_sep_accept))
+                       result |= IMAP_MATCH_CHILDREN;
+               if (parent_flag)
+                       result |= IMAP_MATCH_PARENT;
        }
 
-       return *data == '\0' && *pattern == '\0' ?
-               IMAP_MATCH_YES : match;
+       return result;
 }
 
 enum imap_match_result
 imap_match(struct imap_match_glob *glob, const char *data)
 {
-       struct imap_match_context ctx;
        unsigned int i;
        enum imap_match_result ret, match;
 
        match = IMAP_MATCH_NO;
-       ctx.sep = glob->sep;
        for (i = 0; glob->patterns[i].pattern != NULL; i++) {
-               ctx.inboxcase = glob->patterns[i].inboxcase;
-
-               ret = imap_match_pattern(&ctx, data, glob->patterns[i].pattern);
+               T_BEGIN {
+                       ret = imap_match_pattern_run(&glob->patterns[i], data,
+                                                    glob->sep,
+                                                    glob->patterns[i].inboxcase);
+               } T_END;
                if (ret == IMAP_MATCH_YES)
                        return IMAP_MATCH_YES;
 
index df911dae50ec191c366a551ea2d5f40e041eee03..88459ea9c3d14c812bb70a7bb0a6b009a8a20f32 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "lib.h"
 #include "imap-match.h"
+#include "str.h"
 #include "test-common.h"
 
 struct test_imap_match {
@@ -88,6 +89,56 @@ static void test_imap_match(void)
        test_end();
 }
 
+static void test_imap_match_no_redos(void)
+{
+       struct imap_match_glob *glob;
+       pool_t pool;
+       const unsigned int wildcard_count = 1000;
+       string_t *mailbox_name, *pattern;
+       unsigned int i;
+
+       pool = pool_alloconly_create("imap match redos", 1024);
+       test_begin("imap match no redos");
+
+       mailbox_name = str_new(pool, 256);
+       for (i = 0; i < 255; i++)
+               str_append_c(mailbox_name, 'a');
+
+       pattern = str_new(pool, wildcard_count * 2 + 10);
+       for (i = 0; i < 255; i++)
+               str_append(pattern, "%a");
+       glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
+       test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_YES);
+
+       str_truncate(pattern, 0);
+       for (i = 0; i < wildcard_count; i++)
+               str_append(pattern, "%a");
+       glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
+       test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO);
+
+       str_append_c(pattern, 'b');
+       glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
+       test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO);
+
+       str_truncate(pattern, 0);
+       for (i = 0; i < wildcard_count; i++)
+               str_append(pattern, "*a");
+       str_append_c(pattern, 'b');
+       glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
+       test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_CHILDREN);
+       p_clear(pool);
+
+       glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/');
+       test_assert(imap_match(glob, "aaaaab") == IMAP_MATCH_YES);
+       p_clear(pool);
+
+       glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/');
+       test_assert(imap_match(glob, "aaaaa") == IMAP_MATCH_NO);
+
+       pool_unref(&pool);
+       test_end();
+}
+
 static void test_imap_match_globs_equal(void)
 {
        struct imap_match_glob *glob;
@@ -120,6 +171,7 @@ int main(void)
 {
        static void (*const test_functions[])(void) = {
                test_imap_match,
+               test_imap_match_no_redos,
                test_imap_match_globs_equal,
                NULL
        };