From 1a26bde2d4090b2b79a3c609b9e60b6b0db1c2db Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 14 Sep 2026 16:39:14 +0200 Subject: [PATCH] fix(file_list): bound entry length and reject embedded NUL bytes Read list files through utils_getdelim_bounded() so a single multi-gigabyte line can no longer force unbounded allocation; over-long entries fail with a clear error. Also add the documented memchr() NUL-byte check (excluding the NUL delimiter in NUL-separated mode). Tests: an over-long entry is rejected with an 'exceeds' diagnostic. --- src/shared/file_list.c | 24 ++++++++++++++++++++---- tests/test_file_list.c | 30 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/shared/file_list.c b/src/shared/file_list.c index 3297a87..fc04e0b 100644 --- a/src/shared/file_list.c +++ b/src/shared/file_list.c @@ -56,8 +56,14 @@ static int normalize_entry(const char* raw, size_t len, bool strip_line_endings, snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", print_len, raw); return -1; } - /* Reject NUL bytes inside a token defensively (NUL-delimited mode splits on - * them, so this only guards against embedded garbage). */ + /* Reject NUL bytes inside a token defensively. In NUL-delimited mode the + * delimiter itself is the final byte and is expected; in line mode any NUL is + * embedded garbage (strlen-based parsing would otherwise silently truncate). */ + size_t scan_len = strip_line_endings ? len : len - 1; + if (memchr(raw, '\0', scan_len)) { + snprintf(err, err_size, "entry contains an embedded NUL byte"); + return -1; + } char* dup = malloc(len + 1); if (!dup) { snprintf(err, err_size, "memory allocation failed"); @@ -158,10 +164,20 @@ FileListSet* file_list_load(const char* path, bool null_separated, char* err, si StringList raw = {0}; char* line = NULL; size_t line_cap = 0; - ssize_t n; bool ok = true; char delim = null_separated ? '\0' : '\n'; - while (ok && (n = getdelim(&line, &line_cap, delim, fp)) != -1) { + while (ok) { + ssize_t n = utils_getdelim_bounded(fp, &line, &line_cap, delim, UTILS_MAX_LINE_LEN); + if (n < 0) { + if (errno == EFBIG) + snprintf(err, err_size, "entry in file list exceeds %d bytes", (int)UTILS_MAX_LINE_LEN); + else + snprintf(err, err_size, "error reading file list: %s", strerror(errno)); + ok = false; + break; + } + if (n == 0) + break; int r = normalize_entry(line, (size_t)n, !null_separated, &raw, err, err_size); if (r < 0) { ok = false; diff --git a/tests/test_file_list.c b/tests/test_file_list.c index 80a0ca8..777b43a 100644 --- a/tests/test_file_list.c +++ b/tests/test_file_list.c @@ -1,5 +1,6 @@ #include "test_file_list.h" #include "file_list.h" +#include "utils.h" #include "test_utils.h" #include #include @@ -189,8 +190,35 @@ static void test_deep_paths_are_bounded() { free(entry); } +/* An over-long list entry must be rejected cleanly instead of being read + without a bound (the reader never allocates beyond UTILS_MAX_LINE_LEN). */ +static void test_oversized_entry_rejected() { + const char* path = "test_file_list_oversized.txt"; + FILE* fp = fopen(path, "wb"); + EXPECT_NOT_NULL(fp); + char chunk[4096]; + memset(chunk, 'a', sizeof(chunk)); + size_t total = 0; + while (total <= UTILS_MAX_LINE_LEN) { + EXPECT_EQ_INT((int)fwrite(chunk, 1, sizeof(chunk), fp), (int)sizeof(chunk)); + total += sizeof(chunk); + } + EXPECT_EQ_INT(fputc('\n', fp), '\n'); + fclose(fp); + + char err[160]; + FileListSet* set = file_list_load(path, false, err, sizeof(err)); + if (set) { + file_list_destroy(set); + EXPECT_FAIL("over-long entry was accepted"); + } + EXPECT_TRUE(strstr(err, "exceeds") != NULL); + remove(path); +} + void test_file_list() { test_membership_matches_reference(); test_ancestor_and_descendant_queries(); test_deep_paths_are_bounded(); -} + test_oversized_entry_rejected(); +} \ No newline at end of file