fix(utils): bound glob matching and line reads
Replace the recursive glob matcher with an iterative O(pattern*string) dynamic program. The old recursion explored exponentially many paths for overlapping '*'/'**' wildcards (e.g. '*a*a*...*b' against a long run of 'a'), a CPU DoS reachable from --exclude/--include patterns and .rsync-filter. A differential fuzz against the original matcher confirms identical results. Doc: has_path_traversal() is a lexical '..' check only. Add utils_getdelim_bounded(): a getdelim-style reader that never allocates beyond UTILS_MAX_LINE_LEN, used to cap untrusted list/filter line reads. Tests: pathological glob completes quickly; bounded reader returns EFBIG on an over-long record.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
#include "test_glob.h"
|
||||
#include "utils.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static void test_glob_exact_match() {
|
||||
EXPECT_TRUE(glob_match("foo", "foo"));
|
||||
@@ -76,6 +78,34 @@ static void test_glob_doublestar_mid() {
|
||||
EXPECT_FALSE(glob_match("a/**/b", "a/x/bad"));
|
||||
}
|
||||
|
||||
/* The old backtracking matcher explored an exponential number of paths for a
|
||||
* pattern with many `*` wildcards against a long run that never matches the
|
||||
* trailing literal. The iterative matcher must stay bounded: 30 `*a` groups
|
||||
* followed by `b` against ten thousand `a`s is a few hundred thousand states,
|
||||
* not 2^30 recursion nodes. */
|
||||
static void test_glob_pathological_is_bounded() {
|
||||
char pattern[128];
|
||||
size_t pos = 0;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
pattern[pos++] = '*';
|
||||
pattern[pos++] = 'a';
|
||||
}
|
||||
pattern[pos++] = 'b';
|
||||
pattern[pos] = '\0';
|
||||
|
||||
char* text = malloc(10001);
|
||||
EXPECT_NOT_NULL(text);
|
||||
memset(text, 'a', 10000);
|
||||
text[10000] = '\0';
|
||||
|
||||
clock_t start = clock();
|
||||
EXPECT_FALSE(glob_match(pattern, text));
|
||||
double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC;
|
||||
EXPECT_TRUE(elapsed < 5.0);
|
||||
|
||||
free(text);
|
||||
}
|
||||
|
||||
void test_glob() {
|
||||
test_glob_exact_match();
|
||||
test_glob_question_mark();
|
||||
@@ -91,4 +121,5 @@ void test_glob() {
|
||||
test_glob_doublestar_prefix();
|
||||
test_glob_doublestar_suffix();
|
||||
test_glob_doublestar_mid();
|
||||
test_glob_pathological_is_bounded();
|
||||
}
|
||||
|
||||
@@ -519,9 +519,38 @@ static void test_path_index_semantics() {
|
||||
path_index_free(&empty);
|
||||
}
|
||||
|
||||
/* utils_getdelim_bounded must return normal short lines unchanged and refuse an
|
||||
* over-long record with EFBIG rather than allocating without bound. */
|
||||
static void test_getdelim_bounded() {
|
||||
FILE* fp = tmpfile();
|
||||
EXPECT_NOT_NULL(fp);
|
||||
const char* short_line = "short\n";
|
||||
EXPECT_EQ_INT((int)fwrite(short_line, 1, strlen(short_line), fp), (int)strlen(short_line));
|
||||
char big[32];
|
||||
memset(big, 'x', 20);
|
||||
big[20] = '\n';
|
||||
EXPECT_EQ_INT((int)fwrite(big, 1, 21, fp), 21);
|
||||
rewind(fp);
|
||||
|
||||
char* line = NULL;
|
||||
size_t cap = 0;
|
||||
ssize_t n = utils_getdelim_bounded(fp, &line, &cap, '\n', 64);
|
||||
EXPECT_EQ_INT((int)n, 6);
|
||||
EXPECT_EQ_STR(line, "short\n");
|
||||
|
||||
errno = 0;
|
||||
n = utils_getdelim_bounded(fp, &line, &cap, '\n', 10);
|
||||
EXPECT_EQ_INT((int)n, -1);
|
||||
EXPECT_EQ_INT(errno, EFBIG);
|
||||
|
||||
free(line);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
void test_shared_utils() {
|
||||
test_path_index_bounded();
|
||||
test_path_index_semantics();
|
||||
test_getdelim_bounded();
|
||||
test_walker_removes_extras_keeps_manifest_and_protected();
|
||||
test_walker_keeps_nested_manifest_dirs();
|
||||
test_walker_max_delete_exceeded_deletes_nothing();
|
||||
|
||||
Reference in New Issue
Block a user