From 8147ff7b503ebba57a814efe814b1e822308e805 Mon Sep 17 00:00:00 2001 From: TapTap Date: Sun, 13 Sep 2026 01:36:51 +0200 Subject: [PATCH] fix(scanner): free chunk_data on chunk-create failure --- src/client/scanner.c | 8 +++++-- tests/test_scanner.c | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/client/scanner.c b/src/client/scanner.c index 4911ccf..6fd3407 100644 --- a/src/client/scanner.c +++ b/src/client/scanner.c @@ -587,12 +587,16 @@ void directory_scanner_destroy(DirectoryScanner* scanner) { static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) { void** chunk_items = array_list_to_array(chunk_data); - if (!chunk_items) + if (!chunk_items) { + array_list_delete(chunk_data); return NULL; + } Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size); free(chunk_items); - if (!chunk) + if (!chunk) { + array_list_delete(chunk_data); return NULL; + } chunk_data->item_destroyer = NULL; array_list_delete(chunk_data); return chunk; diff --git a/tests/test_scanner.c b/tests/test_scanner.c index 06e74ad..3ba35ed 100644 --- a/tests/test_scanner.c +++ b/tests/test_scanner.c @@ -1317,6 +1317,58 @@ static void test_scanner_captures_directory_times() { rmdir(root); } +/* Ownership guard for chunk_data_to_chunk(): a returned Chunk owns its File + * objects, so destroying the chunk must free them exactly once and the scanner + * must never free them again. chunk_size = 1 forces the mid-directory + * conversion branch (chunk_data_size > chunk_size) for every file, and the + * chunk is destroyed immediately, catching a double free / use-after-free under + * ASan if ownership transfer regressed. + * + * The failure path (array_list_to_array() or chunk_create() returning NULL) is + * not reachable from a unit test: both allocate through protocol_alloc(), and + * each allocation they perform is no larger than the array_list allocations + * that already succeeded while building the list (array_list_to_array() copies + * exactly `size` pointers, which never exceeds the capacity just grown, and + * sizeof(Chunk) is far below the initial 100-entry item array). Binding a + * small --max-alloc session therefore always fails *before* this function, not + * inside it, so fault injection cannot isolate these paths. */ +static void test_scanner_chunk_ownership() { + const char* dir = "test_scan_ownership"; + const char* file1 = "test_scan_ownership/a.txt"; + const char* file2 = "test_scan_ownership/b.txt"; + const char* file3 = "test_scan_ownership/c.txt"; + + EXPECT_EQ_INT(mkdir(dir, 0755), 0); + create_test_file(file1, "aaaa"); + create_test_file(file2, "bbbb"); + create_test_file(file3, "cccc"); + + ScannerOptions options = {0}; + options.chunk_size = 1; + DirectoryScanner* scanner = directory_scanner_create_with_options(dir, &options); + EXPECT_NOT_NULL(scanner); + + int chunks = 0; + int files = 0; + Chunk* chunk; + while ((chunk = directory_scanner_next(scanner)) != NULL) { + chunks++; + files += chunk->element_count; + EXPECT_EQ_INT(chunk->element_count, 1); + chunk_destroy(chunk); + EXPECT_FALSE(directory_scanner_failed(scanner)); + } + EXPECT_EQ_INT(files, 3); + EXPECT_EQ_INT(chunks, 3); + EXPECT_FALSE(directory_scanner_failed(scanner)); + + directory_scanner_destroy(scanner); + unlink(file1); + unlink(file2); + unlink(file3); + rmdir(dir); +} + void test_scanner() { test_scanner_single_file(); test_scanner_multiple_files(); @@ -1353,4 +1405,5 @@ void test_scanner() { test_dirs_files_from(); test_files_from_relative_send_path(); test_scanner_captures_directory_times(); + test_scanner_chunk_ownership(); }