new file mode 100644
@@ -0,0 +1,362 @@
+From a2de885e93b39d5d834f2e6d93bdc62dd9c0322d Mon Sep 17 00:00:00 2001
+From: Jakub Jelen <jjelen@redhat.com>
+Date: Wed, 17 Dec 2025 18:48:34 +0100
+Subject: [PATCH 3/4] 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.
+
+CVE: CVE-2026-0967
+Upstream-Status: Backport [https://git.libssh.org/projects/libssh.git/commit/?id=6d74aa6138895b3662bade9bd578338b0c4f8a15]
+
+Signed-off-by: Jakub Jelen <jjelen@redhat.com>
+Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
+(cherry picked from commit a411de5ce806e3ea24d088774b2f7584d6590b5f)
+(cherry picked from commit 6d74aa6138895b3662bade9bd578338b0c4f8a15)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ 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 ada0ce8c..fcfe8fbc 100644
+--- a/tests/unittests/torture_config.c
++++ b/tests/unittests/torture_config.c
+@@ -2342,80 +2342,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
+--
+2.51.0
+
@@ -13,6 +13,7 @@ SRC_URI = "git://git.libssh.org/projects/libssh.git;protocol=https;branch=stable
file://CVE-2026-3731_p2.patch \
file://CVE-2026-0968_p1.patch \
file://CVE-2026-0968_p2.patch \
+ file://CVE-2026-0967.patch \
"
SRC_URI:append:toolchain-clang = " file://0001-CompilerChecks.cmake-drop-Wunused-variable-flag.patch"