#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 {
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)
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)
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);
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;
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;
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;
#include "lib.h"
#include "imap-match.h"
+#include "str.h"
#include "test-common.h"
struct test_imap_match {
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;
{
static void (*const test_functions[])(void) = {
test_imap_match,
+ test_imap_match_no_redos,
test_imap_match_globs_equal,
NULL
};