From 6d74aa6138895b3662bade9bd578338b0c4f8a15 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Wed, 17 Dec 2025 18:48:34 +0100 Subject: [PATCH] CVE-2026-0967 match: Avoid recursive matching (ReDoS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The specially crafted patterns (from configuration files) could cause exhaustive search or timeouts. Previous attempts to fix this by limiting recursion to depth 16 avoided stack overflow, but not timeouts. This is due to the backtracking, which caused the exponential time complexity O(N^16) of existing algorithm. This is code comes from the same function from OpenSSH, where this code originates from, which is not having this issue (due to not limiting the number of recursion), but will also easily exhaust stack due to unbound recursion: https://github.com/openssh/openssh-portable/commit/05bcd0cadf160fd4826a2284afa7cba6ec432633 This is an attempt to simplify the algorithm by preventing the backtracking to previous wildcard, which should keep the same behavior for existing inputs while reducing the complexity to linear O(N*M). This fixes the long-term issue we had with fuzzing as well as recently reported security issue by Kang Yang. Signed-off-by: Jakub Jelen Reviewed-by: Pavol Žáčik (cherry picked from commit a411de5ce806e3ea24d088774b2f7584d6590b5f) --- src/match.c | 111 +++++++++++++---------------- tests/unittests/torture_config.c | 116 +++++++++++++++++++++++-------- 2 files changed, 135 insertions(+), 92 deletions(-) diff --git a/src/match.c b/src/match.c index 2c004c98..771ee63c 100644 --- a/src/match.c +++ b/src/match.c @@ -53,85 +53,70 @@ #include "libssh/priv.h" -#define MAX_MATCH_RECURSION 16 - -/* - * Returns true if the given string matches the pattern (which may contain ? - * and * as wildcards), and zero if it does not match. +/** + * @brief Compare a string with a pattern containing wildcards `*` and `?` + * + * This function is an iterative replacement for the previously recursive + * implementation to avoid exponential complexity (DoS) with specific patterns. + * + * @param[in] s The string to match. + * @param[in] pattern The pattern to match against. + * + * @return 1 if the pattern matches, 0 otherwise. */ -static int match_pattern(const char *s, const char *pattern, size_t limit) +static int match_pattern(const char *s, const char *pattern) { - bool had_asterisk = false; + const char *s_star = NULL; /* Position in s when last `*` was met */ + const char *p_star = NULL; /* Position in pattern after last `*` */ - if (s == NULL || pattern == NULL || limit <= 0) { + if (s == NULL || pattern == NULL) { return 0; } - for (;;) { - /* If at end of pattern, accept if also at end of string. */ - if (*pattern == '\0') { - return (*s == '\0'); - } - - /* Skip all the asterisks and adjacent question marks */ - while (*pattern == '*' || (had_asterisk && *pattern == '?')) { - if (*pattern == '*') { - had_asterisk = true; - } + while (*s) { + /* Case 1: Exact match or '?' wildcard */ + if (*pattern == *s || *pattern == '?') { + s++; pattern++; + continue; } - if (had_asterisk) { - /* If at end of pattern, accept immediately. */ - if (!*pattern) - return 1; - - /* If next character in pattern is known, optimize. */ - if (*pattern != '?') { - /* - * Look instances of the next character in - * pattern, and try to match starting from - * those. - */ - for (; *s; s++) - if (*s == *pattern && match_pattern(s + 1, pattern + 1, limit - 1)) { - return 1; - } - /* Failed. */ - return 0; - } - /* - * Move ahead one character at a time and try to - * match at each position. + /* Case 2: '*' wildcard */ + if (*pattern == '*') { + /* Record the position of the star and the current string position. + * We optimistically assume * matches 0 characters first. */ - for (; *s; s++) { - if (match_pattern(s, pattern, limit - 1)) { - return 1; - } - } - /* Failed. */ - return 0; - } - /* - * There must be at least one more character in the string. - * If we are at the end, fail. - */ - if (!*s) { - return 0; + p_star = ++pattern; + s_star = s; + continue; } - /* Check if the next character of the string is acceptable. */ - if (*pattern != '?' && *pattern != *s) { - return 0; + /* Case 3: Mismatch */ + if (p_star) { + /* If we have seen a star previously, backtrack. + * We restore the pattern to just after the star, + * but advance the string position (consume one more char for the + * star). + * No need to backtrack to previous stars as any match of the last + * star could be eaten the same way by the previous star. + */ + pattern = p_star; + s = ++s_star; + continue; } - /* Move to the next character, both in string and in pattern. */ - s++; + /* Case 4: Mismatch and no star to backtrack to */ + return 0; + } + + /* Handle trailing stars in the pattern + * (e.g., pattern "abc*" matching "abc") */ + while (*pattern == '*') { pattern++; } - /* NOTREACHED */ - return 0; + /* If we reached the end of the pattern, it's a match */ + return (*pattern == '\0'); } /* @@ -182,7 +167,7 @@ int match_pattern_list(const char *string, const char *pattern, sub[subi] = '\0'; /* Try to match the subpattern against the string. */ - if (match_pattern(string, sub, MAX_MATCH_RECURSION)) { + if (match_pattern(string, sub)) { if (negated) { return -1; /* Negative */ } else { diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 973758eb..5c908aa5 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -2372,80 +2372,138 @@ static void torture_config_match_pattern(void **state) (void) state; /* Simple test "a" matches "a" */ - rv = match_pattern("a", "a", MAX_MATCH_RECURSION); + rv = match_pattern("a", "a"); assert_int_equal(rv, 1); /* Simple test "a" does not match "b" */ - rv = match_pattern("a", "b", MAX_MATCH_RECURSION); + rv = match_pattern("a", "b"); assert_int_equal(rv, 0); /* NULL arguments are correctly handled */ - rv = match_pattern("a", NULL, MAX_MATCH_RECURSION); + rv = match_pattern("a", NULL); assert_int_equal(rv, 0); - rv = match_pattern(NULL, "a", MAX_MATCH_RECURSION); + rv = match_pattern(NULL, "a"); assert_int_equal(rv, 0); /* Simple wildcard ? is handled in pattern */ - rv = match_pattern("a", "?", MAX_MATCH_RECURSION); + rv = match_pattern("a", "?"); assert_int_equal(rv, 1); - rv = match_pattern("aa", "?", MAX_MATCH_RECURSION); + rv = match_pattern("aa", "?"); assert_int_equal(rv, 0); /* Wildcard in search string */ - rv = match_pattern("?", "a", MAX_MATCH_RECURSION); + rv = match_pattern("?", "a"); assert_int_equal(rv, 0); - rv = match_pattern("?", "?", MAX_MATCH_RECURSION); + rv = match_pattern("?", "?"); assert_int_equal(rv, 1); /* Simple wildcard * is handled in pattern */ - rv = match_pattern("a", "*", MAX_MATCH_RECURSION); + rv = match_pattern("a", "*"); assert_int_equal(rv, 1); - rv = match_pattern("aa", "*", MAX_MATCH_RECURSION); + rv = match_pattern("aa", "*"); assert_int_equal(rv, 1); /* Wildcard in search string */ - rv = match_pattern("*", "a", MAX_MATCH_RECURSION); + rv = match_pattern("*", "a"); assert_int_equal(rv, 0); - rv = match_pattern("*", "*", MAX_MATCH_RECURSION); + rv = match_pattern("*", "*"); assert_int_equal(rv, 1); /* More complicated patterns */ - rv = match_pattern("a", "*a", MAX_MATCH_RECURSION); + rv = match_pattern("a", "*a"); assert_int_equal(rv, 1); - rv = match_pattern("a", "a*", MAX_MATCH_RECURSION); + rv = match_pattern("a", "a*"); assert_int_equal(rv, 1); - rv = match_pattern("abababc", "*abc", MAX_MATCH_RECURSION); + rv = match_pattern("abababc", "*abc"); assert_int_equal(rv, 1); - rv = match_pattern("ababababca", "*abc", MAX_MATCH_RECURSION); + rv = match_pattern("ababababca", "*abc"); assert_int_equal(rv, 0); - rv = match_pattern("ababababca", "*abc*", MAX_MATCH_RECURSION); + rv = match_pattern("ababababca", "*abc*"); assert_int_equal(rv, 1); /* Multiple wildcards in row */ - rv = match_pattern("aa", "??", MAX_MATCH_RECURSION); + rv = match_pattern("aa", "??"); assert_int_equal(rv, 1); - rv = match_pattern("bba", "??a", MAX_MATCH_RECURSION); + rv = match_pattern("bba", "??a"); assert_int_equal(rv, 1); - rv = match_pattern("aaa", "**a", MAX_MATCH_RECURSION); + rv = match_pattern("aaa", "**a"); assert_int_equal(rv, 1); - rv = match_pattern("bbb", "**a", MAX_MATCH_RECURSION); + rv = match_pattern("bbb", "**a"); assert_int_equal(rv, 0); /* Consecutive asterisks do not make sense and do not need to recurse */ - rv = match_pattern("hostname", "**********pattern", 5); + rv = match_pattern("hostname", "**********pattern"); assert_int_equal(rv, 0); - rv = match_pattern("hostname", "pattern**********", 5); + rv = match_pattern("hostname", "pattern**********"); assert_int_equal(rv, 0); - rv = match_pattern("pattern", "***********pattern", 5); + rv = match_pattern("pattern", "***********pattern"); assert_int_equal(rv, 1); - rv = match_pattern("pattern", "pattern***********", 5); + rv = match_pattern("pattern", "pattern***********"); assert_int_equal(rv, 1); - /* Limit the maximum recursion */ - rv = match_pattern("hostname", "*p*a*t*t*e*r*n*", 5); + rv = match_pattern("hostname", "*p*a*t*t*e*r*n*"); assert_int_equal(rv, 0); - /* Too much recursion */ - rv = match_pattern("pattern", "*p*a*t*t*e*r*n*", 5); + rv = match_pattern("pattern", "*p*a*t*t*e*r*n*"); + assert_int_equal(rv, 1); + + /* Regular Expression Denial of Service */ + rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a"); + assert_int_equal(rv, 1); + rv = match_pattern("ababababababababababababababababababababab", + "*a*b*a*b*a*b*a*b*a*b*a*b*a*b*a*b"); + assert_int_equal(rv, 1); + + /* A lot of backtracking */ + rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax", + "a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*ax"); + assert_int_equal(rv, 1); + + /* Test backtracking: *a matches first 'a', fails on 'b', must backtrack */ + rv = match_pattern("axaxaxb", "*a*b"); + assert_int_equal(rv, 1); + + /* Test greedy consumption with suffix */ + rv = match_pattern("foo_bar_baz_bar", "*bar"); + assert_int_equal(rv, 1); + + /* Test exact suffix requirement (ensure no partial match acceptance) */ + rv = match_pattern("foobar_extra", "*bar"); assert_int_equal(rv, 0); + /* Test multiple distinct wildcards */ + rv = match_pattern("a_very_long_string_with_a_pattern", "*long*pattern"); + assert_int_equal(rv, 1); + + /* ? inside a * sequence */ + rv = match_pattern("abcdefg", "a*c?e*g"); + assert_int_equal(rv, 1); + + /* Consecutive mixed wildcards */ + rv = match_pattern("abc", "*?c"); + assert_int_equal(rv, 1); + + /* ? at the very end after * */ + rv = match_pattern("abc", "ab?"); + assert_int_equal(rv, 1); + rv = match_pattern("abc", "ab*?"); + assert_int_equal(rv, 1); + + /* Consecutive stars should be collapsed or handled gracefully */ + rv = match_pattern("abc", "a**c"); + assert_int_equal(rv, 1); + rv = match_pattern("abc", "***"); + assert_int_equal(rv, 1); + + /* Empty string handling */ + rv = match_pattern("", "*"); + assert_int_equal(rv, 1); + rv = match_pattern("", "?"); + assert_int_equal(rv, 0); + rv = match_pattern("", ""); + assert_int_equal(rv, 1); + + /* Pattern longer than string */ + rv = match_pattern("short", "short_but_longer"); + assert_int_equal(rv, 0); } /* Identity file can be specified multiple times in the configuration