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.
This commit is contained in:
+20
-4
@@ -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);
|
snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", print_len, raw);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
/* Reject NUL bytes inside a token defensively (NUL-delimited mode splits on
|
/* Reject NUL bytes inside a token defensively. In NUL-delimited mode the
|
||||||
* them, so this only guards against embedded garbage). */
|
* 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);
|
char* dup = malloc(len + 1);
|
||||||
if (!dup) {
|
if (!dup) {
|
||||||
snprintf(err, err_size, "memory allocation failed");
|
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};
|
StringList raw = {0};
|
||||||
char* line = NULL;
|
char* line = NULL;
|
||||||
size_t line_cap = 0;
|
size_t line_cap = 0;
|
||||||
ssize_t n;
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
char delim = null_separated ? '\0' : '\n';
|
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);
|
int r = normalize_entry(line, (size_t)n, !null_separated, &raw, err, err_size);
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
ok = false;
|
ok = false;
|
||||||
|
|||||||
+29
-1
@@ -1,5 +1,6 @@
|
|||||||
#include "test_file_list.h"
|
#include "test_file_list.h"
|
||||||
#include "file_list.h"
|
#include "file_list.h"
|
||||||
|
#include "utils.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -189,8 +190,35 @@ static void test_deep_paths_are_bounded() {
|
|||||||
free(entry);
|
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() {
|
void test_file_list() {
|
||||||
test_membership_matches_reference();
|
test_membership_matches_reference();
|
||||||
test_ancestor_and_descendant_queries();
|
test_ancestor_and_descendant_queries();
|
||||||
test_deep_paths_are_bounded();
|
test_deep_paths_are_bounded();
|
||||||
}
|
test_oversized_entry_rejected();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user