diff options
| author | Ankur Tyagi <ankur.tyagi85@gmail.com> | 2026-04-28 17:01:07 +1200 |
|---|---|---|
| committer | Anuj Mittal <anuj.mittal@oss.qualcomm.com> | 2026-04-29 10:14:29 +0530 |
| commit | 0f64da2ab994edf2086a3dc90524f18c731fa721 (patch) | |
| tree | eef64e0065b525fc593316feea97e857d3972e07 | |
| parent | 015b974b6b7e7e302725012096432e260fb2c39c (diff) | |
| download | meta-openembedded-0f64da2ab994edf2086a3dc90524f18c731fa721.tar.gz | |
libssh: patch CVE-2026-0967
Backport patch [1] as mentioned in [2]
[1] https://git.libssh.org/projects/libssh.git/commit/?id=6d74aa6138895b3662bade9bd578338b0c4f8a15
[2] https://security-tracker.debian.org/tracker/CVE-2026-0967
Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
| -rw-r--r-- | meta-oe/recipes-support/libssh/libssh/CVE-2026-0967.patch | 360 | ||||
| -rw-r--r-- | meta-oe/recipes-support/libssh/libssh_0.10.6.bb | 1 |
2 files changed, 361 insertions, 0 deletions
diff --git a/meta-oe/recipes-support/libssh/libssh/CVE-2026-0967.patch b/meta-oe/recipes-support/libssh/libssh/CVE-2026-0967.patch new file mode 100644 index 0000000000..c34bced2a8 --- /dev/null +++ b/meta-oe/recipes-support/libssh/libssh/CVE-2026-0967.patch | |||
| @@ -0,0 +1,360 @@ | |||
| 1 | From bd18dd3a2e1fc1f76a65fbdcc4d88b8d77ff7cab Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Jakub Jelen <jjelen@redhat.com> | ||
| 3 | Date: Wed, 17 Dec 2025 18:48:34 +0100 | ||
| 4 | Subject: [PATCH] CVE-2026-0967 match: Avoid recursive matching (ReDoS) | ||
| 5 | MIME-Version: 1.0 | ||
| 6 | Content-Type: text/plain; charset=UTF-8 | ||
| 7 | Content-Transfer-Encoding: 8bit | ||
| 8 | |||
| 9 | The specially crafted patterns (from configuration files) could cause | ||
| 10 | exhaustive search or timeouts. | ||
| 11 | |||
| 12 | Previous attempts to fix this by limiting recursion to depth 16 avoided | ||
| 13 | stack overflow, but not timeouts. This is due to the backtracking, | ||
| 14 | which caused the exponential time complexity O(N^16) of existing algorithm. | ||
| 15 | |||
| 16 | This is code comes from the same function from OpenSSH, where this code | ||
| 17 | originates from, which is not having this issue (due to not limiting the number | ||
| 18 | of recursion), but will also easily exhaust stack due to unbound recursion: | ||
| 19 | |||
| 20 | https://github.com/openssh/openssh-portable/commit/05bcd0cadf160fd4826a2284afa7cba6ec432633 | ||
| 21 | |||
| 22 | This is an attempt to simplify the algorithm by preventing the backtracking | ||
| 23 | to previous wildcard, which should keep the same behavior for existing inputs | ||
| 24 | while reducing the complexity to linear O(N*M). | ||
| 25 | |||
| 26 | This fixes the long-term issue we had with fuzzing as well as recently reported | ||
| 27 | security issue by Kang Yang. | ||
| 28 | |||
| 29 | Signed-off-by: Jakub Jelen <jjelen@redhat.com> | ||
| 30 | Reviewed-by: Pavol Žáčik <pzacik@redhat.com> | ||
| 31 | (cherry picked from commit a411de5ce806e3ea24d088774b2f7584d6590b5f) | ||
| 32 | (cherry picked from commit 6d74aa6138895b3662bade9bd578338b0c4f8a15) | ||
| 33 | |||
| 34 | CVE: CVE-2026-0967 | ||
| 35 | Upstream-Status: Backport [https://git.libssh.org/projects/libssh.git/commit/?id=6d74aa6138895b3662bade9bd578338b0c4f8a15] | ||
| 36 | |||
| 37 | Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com> | ||
| 38 | --- | ||
| 39 | src/match.c | 111 +++++++++++++---------------- | ||
| 40 | tests/unittests/torture_config.c | 116 +++++++++++++++++++++++-------- | ||
| 41 | 2 files changed, 135 insertions(+), 92 deletions(-) | ||
| 42 | |||
| 43 | diff --git a/src/match.c b/src/match.c | ||
| 44 | index 3e58f733..896d87cb 100644 | ||
| 45 | --- a/src/match.c | ||
| 46 | +++ b/src/match.c | ||
| 47 | @@ -43,85 +43,70 @@ | ||
| 48 | |||
| 49 | #include "libssh/priv.h" | ||
| 50 | |||
| 51 | -#define MAX_MATCH_RECURSION 16 | ||
| 52 | - | ||
| 53 | -/* | ||
| 54 | - * Returns true if the given string matches the pattern (which may contain ? | ||
| 55 | - * and * as wildcards), and zero if it does not match. | ||
| 56 | +/** | ||
| 57 | + * @brief Compare a string with a pattern containing wildcards `*` and `?` | ||
| 58 | + * | ||
| 59 | + * This function is an iterative replacement for the previously recursive | ||
| 60 | + * implementation to avoid exponential complexity (DoS) with specific patterns. | ||
| 61 | + * | ||
| 62 | + * @param[in] s The string to match. | ||
| 63 | + * @param[in] pattern The pattern to match against. | ||
| 64 | + * | ||
| 65 | + * @return 1 if the pattern matches, 0 otherwise. | ||
| 66 | */ | ||
| 67 | -static int match_pattern(const char *s, const char *pattern, size_t limit) | ||
| 68 | +static int match_pattern(const char *s, const char *pattern) | ||
| 69 | { | ||
| 70 | - bool had_asterisk = false; | ||
| 71 | + const char *s_star = NULL; /* Position in s when last `*` was met */ | ||
| 72 | + const char *p_star = NULL; /* Position in pattern after last `*` */ | ||
| 73 | |||
| 74 | - if (s == NULL || pattern == NULL || limit <= 0) { | ||
| 75 | + if (s == NULL || pattern == NULL) { | ||
| 76 | return 0; | ||
| 77 | } | ||
| 78 | |||
| 79 | - for (;;) { | ||
| 80 | - /* If at end of pattern, accept if also at end of string. */ | ||
| 81 | - if (*pattern == '\0') { | ||
| 82 | - return (*s == '\0'); | ||
| 83 | - } | ||
| 84 | - | ||
| 85 | - /* Skip all the asterisks and adjacent question marks */ | ||
| 86 | - while (*pattern == '*' || (had_asterisk && *pattern == '?')) { | ||
| 87 | - if (*pattern == '*') { | ||
| 88 | - had_asterisk = true; | ||
| 89 | - } | ||
| 90 | + while (*s) { | ||
| 91 | + /* Case 1: Exact match or '?' wildcard */ | ||
| 92 | + if (*pattern == *s || *pattern == '?') { | ||
| 93 | + s++; | ||
| 94 | pattern++; | ||
| 95 | + continue; | ||
| 96 | } | ||
| 97 | |||
| 98 | - if (had_asterisk) { | ||
| 99 | - /* If at end of pattern, accept immediately. */ | ||
| 100 | - if (!*pattern) | ||
| 101 | - return 1; | ||
| 102 | - | ||
| 103 | - /* If next character in pattern is known, optimize. */ | ||
| 104 | - if (*pattern != '?') { | ||
| 105 | - /* | ||
| 106 | - * Look instances of the next character in | ||
| 107 | - * pattern, and try to match starting from | ||
| 108 | - * those. | ||
| 109 | - */ | ||
| 110 | - for (; *s; s++) | ||
| 111 | - if (*s == *pattern && match_pattern(s + 1, pattern + 1, limit - 1)) { | ||
| 112 | - return 1; | ||
| 113 | - } | ||
| 114 | - /* Failed. */ | ||
| 115 | - return 0; | ||
| 116 | - } | ||
| 117 | - /* | ||
| 118 | - * Move ahead one character at a time and try to | ||
| 119 | - * match at each position. | ||
| 120 | + /* Case 2: '*' wildcard */ | ||
| 121 | + if (*pattern == '*') { | ||
| 122 | + /* Record the position of the star and the current string position. | ||
| 123 | + * We optimistically assume * matches 0 characters first. | ||
| 124 | */ | ||
| 125 | - for (; *s; s++) { | ||
| 126 | - if (match_pattern(s, pattern, limit - 1)) { | ||
| 127 | - return 1; | ||
| 128 | - } | ||
| 129 | - } | ||
| 130 | - /* Failed. */ | ||
| 131 | - return 0; | ||
| 132 | - } | ||
| 133 | - /* | ||
| 134 | - * There must be at least one more character in the string. | ||
| 135 | - * If we are at the end, fail. | ||
| 136 | - */ | ||
| 137 | - if (!*s) { | ||
| 138 | - return 0; | ||
| 139 | + p_star = ++pattern; | ||
| 140 | + s_star = s; | ||
| 141 | + continue; | ||
| 142 | } | ||
| 143 | |||
| 144 | - /* Check if the next character of the string is acceptable. */ | ||
| 145 | - if (*pattern != '?' && *pattern != *s) { | ||
| 146 | - return 0; | ||
| 147 | + /* Case 3: Mismatch */ | ||
| 148 | + if (p_star) { | ||
| 149 | + /* If we have seen a star previously, backtrack. | ||
| 150 | + * We restore the pattern to just after the star, | ||
| 151 | + * but advance the string position (consume one more char for the | ||
| 152 | + * star). | ||
| 153 | + * No need to backtrack to previous stars as any match of the last | ||
| 154 | + * star could be eaten the same way by the previous star. | ||
| 155 | + */ | ||
| 156 | + pattern = p_star; | ||
| 157 | + s = ++s_star; | ||
| 158 | + continue; | ||
| 159 | } | ||
| 160 | |||
| 161 | - /* Move to the next character, both in string and in pattern. */ | ||
| 162 | - s++; | ||
| 163 | + /* Case 4: Mismatch and no star to backtrack to */ | ||
| 164 | + return 0; | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + /* Handle trailing stars in the pattern | ||
| 168 | + * (e.g., pattern "abc*" matching "abc") */ | ||
| 169 | + while (*pattern == '*') { | ||
| 170 | pattern++; | ||
| 171 | } | ||
| 172 | |||
| 173 | - /* NOTREACHED */ | ||
| 174 | - return 0; | ||
| 175 | + /* If we reached the end of the pattern, it's a match */ | ||
| 176 | + return (*pattern == '\0'); | ||
| 177 | } | ||
| 178 | |||
| 179 | /* | ||
| 180 | @@ -172,7 +157,7 @@ int match_pattern_list(const char *string, const char *pattern, | ||
| 181 | sub[subi] = '\0'; | ||
| 182 | |||
| 183 | /* Try to match the subpattern against the string. */ | ||
| 184 | - if (match_pattern(string, sub, MAX_MATCH_RECURSION)) { | ||
| 185 | + if (match_pattern(string, sub)) { | ||
| 186 | if (negated) { | ||
| 187 | return -1; /* Negative */ | ||
| 188 | } else { | ||
| 189 | diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c | ||
| 190 | index b7c763af..3569b51a 100644 | ||
| 191 | --- a/tests/unittests/torture_config.c | ||
| 192 | +++ b/tests/unittests/torture_config.c | ||
| 193 | @@ -1656,80 +1656,138 @@ static void torture_config_match_pattern(void **state) | ||
| 194 | (void) state; | ||
| 195 | |||
| 196 | /* Simple test "a" matches "a" */ | ||
| 197 | - rv = match_pattern("a", "a", MAX_MATCH_RECURSION); | ||
| 198 | + rv = match_pattern("a", "a"); | ||
| 199 | assert_int_equal(rv, 1); | ||
| 200 | |||
| 201 | /* Simple test "a" does not match "b" */ | ||
| 202 | - rv = match_pattern("a", "b", MAX_MATCH_RECURSION); | ||
| 203 | + rv = match_pattern("a", "b"); | ||
| 204 | assert_int_equal(rv, 0); | ||
| 205 | |||
| 206 | /* NULL arguments are correctly handled */ | ||
| 207 | - rv = match_pattern("a", NULL, MAX_MATCH_RECURSION); | ||
| 208 | + rv = match_pattern("a", NULL); | ||
| 209 | assert_int_equal(rv, 0); | ||
| 210 | - rv = match_pattern(NULL, "a", MAX_MATCH_RECURSION); | ||
| 211 | + rv = match_pattern(NULL, "a"); | ||
| 212 | assert_int_equal(rv, 0); | ||
| 213 | |||
| 214 | /* Simple wildcard ? is handled in pattern */ | ||
| 215 | - rv = match_pattern("a", "?", MAX_MATCH_RECURSION); | ||
| 216 | + rv = match_pattern("a", "?"); | ||
| 217 | assert_int_equal(rv, 1); | ||
| 218 | - rv = match_pattern("aa", "?", MAX_MATCH_RECURSION); | ||
| 219 | + rv = match_pattern("aa", "?"); | ||
| 220 | assert_int_equal(rv, 0); | ||
| 221 | /* Wildcard in search string */ | ||
| 222 | - rv = match_pattern("?", "a", MAX_MATCH_RECURSION); | ||
| 223 | + rv = match_pattern("?", "a"); | ||
| 224 | assert_int_equal(rv, 0); | ||
| 225 | - rv = match_pattern("?", "?", MAX_MATCH_RECURSION); | ||
| 226 | + rv = match_pattern("?", "?"); | ||
| 227 | assert_int_equal(rv, 1); | ||
| 228 | |||
| 229 | /* Simple wildcard * is handled in pattern */ | ||
| 230 | - rv = match_pattern("a", "*", MAX_MATCH_RECURSION); | ||
| 231 | + rv = match_pattern("a", "*"); | ||
| 232 | assert_int_equal(rv, 1); | ||
| 233 | - rv = match_pattern("aa", "*", MAX_MATCH_RECURSION); | ||
| 234 | + rv = match_pattern("aa", "*"); | ||
| 235 | assert_int_equal(rv, 1); | ||
| 236 | /* Wildcard in search string */ | ||
| 237 | - rv = match_pattern("*", "a", MAX_MATCH_RECURSION); | ||
| 238 | + rv = match_pattern("*", "a"); | ||
| 239 | assert_int_equal(rv, 0); | ||
| 240 | - rv = match_pattern("*", "*", MAX_MATCH_RECURSION); | ||
| 241 | + rv = match_pattern("*", "*"); | ||
| 242 | assert_int_equal(rv, 1); | ||
| 243 | |||
| 244 | /* More complicated patterns */ | ||
| 245 | - rv = match_pattern("a", "*a", MAX_MATCH_RECURSION); | ||
| 246 | + rv = match_pattern("a", "*a"); | ||
| 247 | assert_int_equal(rv, 1); | ||
| 248 | - rv = match_pattern("a", "a*", MAX_MATCH_RECURSION); | ||
| 249 | + rv = match_pattern("a", "a*"); | ||
| 250 | assert_int_equal(rv, 1); | ||
| 251 | - rv = match_pattern("abababc", "*abc", MAX_MATCH_RECURSION); | ||
| 252 | + rv = match_pattern("abababc", "*abc"); | ||
| 253 | assert_int_equal(rv, 1); | ||
| 254 | - rv = match_pattern("ababababca", "*abc", MAX_MATCH_RECURSION); | ||
| 255 | + rv = match_pattern("ababababca", "*abc"); | ||
| 256 | assert_int_equal(rv, 0); | ||
| 257 | - rv = match_pattern("ababababca", "*abc*", MAX_MATCH_RECURSION); | ||
| 258 | + rv = match_pattern("ababababca", "*abc*"); | ||
| 259 | assert_int_equal(rv, 1); | ||
| 260 | |||
| 261 | /* Multiple wildcards in row */ | ||
| 262 | - rv = match_pattern("aa", "??", MAX_MATCH_RECURSION); | ||
| 263 | + rv = match_pattern("aa", "??"); | ||
| 264 | assert_int_equal(rv, 1); | ||
| 265 | - rv = match_pattern("bba", "??a", MAX_MATCH_RECURSION); | ||
| 266 | + rv = match_pattern("bba", "??a"); | ||
| 267 | assert_int_equal(rv, 1); | ||
| 268 | - rv = match_pattern("aaa", "**a", MAX_MATCH_RECURSION); | ||
| 269 | + rv = match_pattern("aaa", "**a"); | ||
| 270 | assert_int_equal(rv, 1); | ||
| 271 | - rv = match_pattern("bbb", "**a", MAX_MATCH_RECURSION); | ||
| 272 | + rv = match_pattern("bbb", "**a"); | ||
| 273 | assert_int_equal(rv, 0); | ||
| 274 | |||
| 275 | /* Consecutive asterisks do not make sense and do not need to recurse */ | ||
| 276 | - rv = match_pattern("hostname", "**********pattern", 5); | ||
| 277 | + rv = match_pattern("hostname", "**********pattern"); | ||
| 278 | assert_int_equal(rv, 0); | ||
| 279 | - rv = match_pattern("hostname", "pattern**********", 5); | ||
| 280 | + rv = match_pattern("hostname", "pattern**********"); | ||
| 281 | assert_int_equal(rv, 0); | ||
| 282 | - rv = match_pattern("pattern", "***********pattern", 5); | ||
| 283 | + rv = match_pattern("pattern", "***********pattern"); | ||
| 284 | assert_int_equal(rv, 1); | ||
| 285 | - rv = match_pattern("pattern", "pattern***********", 5); | ||
| 286 | + rv = match_pattern("pattern", "pattern***********"); | ||
| 287 | assert_int_equal(rv, 1); | ||
| 288 | |||
| 289 | - /* Limit the maximum recursion */ | ||
| 290 | - rv = match_pattern("hostname", "*p*a*t*t*e*r*n*", 5); | ||
| 291 | + rv = match_pattern("hostname", "*p*a*t*t*e*r*n*"); | ||
| 292 | assert_int_equal(rv, 0); | ||
| 293 | - /* Too much recursion */ | ||
| 294 | - rv = match_pattern("pattern", "*p*a*t*t*e*r*n*", 5); | ||
| 295 | + rv = match_pattern("pattern", "*p*a*t*t*e*r*n*"); | ||
| 296 | + assert_int_equal(rv, 1); | ||
| 297 | + | ||
| 298 | + /* Regular Expression Denial of Service */ | ||
| 299 | + rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||
| 300 | + "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a"); | ||
| 301 | + assert_int_equal(rv, 1); | ||
| 302 | + rv = match_pattern("ababababababababababababababababababababab", | ||
| 303 | + "*a*b*a*b*a*b*a*b*a*b*a*b*a*b*a*b"); | ||
| 304 | + assert_int_equal(rv, 1); | ||
| 305 | + | ||
| 306 | + /* A lot of backtracking */ | ||
| 307 | + rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax", | ||
| 308 | + "a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*ax"); | ||
| 309 | + assert_int_equal(rv, 1); | ||
| 310 | + | ||
| 311 | + /* Test backtracking: *a matches first 'a', fails on 'b', must backtrack */ | ||
| 312 | + rv = match_pattern("axaxaxb", "*a*b"); | ||
| 313 | + assert_int_equal(rv, 1); | ||
| 314 | + | ||
| 315 | + /* Test greedy consumption with suffix */ | ||
| 316 | + rv = match_pattern("foo_bar_baz_bar", "*bar"); | ||
| 317 | + assert_int_equal(rv, 1); | ||
| 318 | + | ||
| 319 | + /* Test exact suffix requirement (ensure no partial match acceptance) */ | ||
| 320 | + rv = match_pattern("foobar_extra", "*bar"); | ||
| 321 | + assert_int_equal(rv, 0); | ||
| 322 | + | ||
| 323 | + /* Test multiple distinct wildcards */ | ||
| 324 | + rv = match_pattern("a_very_long_string_with_a_pattern", "*long*pattern"); | ||
| 325 | + assert_int_equal(rv, 1); | ||
| 326 | + | ||
| 327 | + /* ? inside a * sequence */ | ||
| 328 | + rv = match_pattern("abcdefg", "a*c?e*g"); | ||
| 329 | + assert_int_equal(rv, 1); | ||
| 330 | + | ||
| 331 | + /* Consecutive mixed wildcards */ | ||
| 332 | + rv = match_pattern("abc", "*?c"); | ||
| 333 | + assert_int_equal(rv, 1); | ||
| 334 | + | ||
| 335 | + /* ? at the very end after * */ | ||
| 336 | + rv = match_pattern("abc", "ab?"); | ||
| 337 | + assert_int_equal(rv, 1); | ||
| 338 | + rv = match_pattern("abc", "ab*?"); | ||
| 339 | + assert_int_equal(rv, 1); | ||
| 340 | + | ||
| 341 | + /* Consecutive stars should be collapsed or handled gracefully */ | ||
| 342 | + rv = match_pattern("abc", "a**c"); | ||
| 343 | + assert_int_equal(rv, 1); | ||
| 344 | + rv = match_pattern("abc", "***"); | ||
| 345 | + assert_int_equal(rv, 1); | ||
| 346 | + | ||
| 347 | + /* Empty string handling */ | ||
| 348 | + rv = match_pattern("", "*"); | ||
| 349 | + assert_int_equal(rv, 1); | ||
| 350 | + rv = match_pattern("", "?"); | ||
| 351 | assert_int_equal(rv, 0); | ||
| 352 | + rv = match_pattern("", ""); | ||
| 353 | + assert_int_equal(rv, 1); | ||
| 354 | |||
| 355 | + /* Pattern longer than string */ | ||
| 356 | + rv = match_pattern("short", "short_but_longer"); | ||
| 357 | + assert_int_equal(rv, 0); | ||
| 358 | } | ||
| 359 | |||
| 360 | /* Identity file can be specified multiple times in the configuration | ||
diff --git a/meta-oe/recipes-support/libssh/libssh_0.10.6.bb b/meta-oe/recipes-support/libssh/libssh_0.10.6.bb index e0ade7f67c..e4a28af7a6 100644 --- a/meta-oe/recipes-support/libssh/libssh_0.10.6.bb +++ b/meta-oe/recipes-support/libssh/libssh_0.10.6.bb | |||
| @@ -30,6 +30,7 @@ SRC_URI = "git://git.libssh.org/projects/libssh.git;protocol=https;branch=stable | |||
| 30 | file://CVE-2026-0966-3.patch \ | 30 | file://CVE-2026-0966-3.patch \ |
| 31 | file://CVE-2026-0968-1.patch \ | 31 | file://CVE-2026-0968-1.patch \ |
| 32 | file://CVE-2026-0968-2.patch \ | 32 | file://CVE-2026-0968-2.patch \ |
| 33 | file://CVE-2026-0967.patch \ | ||
| 33 | " | 34 | " |
| 34 | SRCREV = "10e09e273f69e149389b3e0e5d44b8c221c2e7f6" | 35 | SRCREV = "10e09e273f69e149389b3e0e5d44b8c221c2e7f6" |
| 35 | 36 | ||
