diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 6aa2c15..a8caa64 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,7 +42,7 @@ jobs: needs: lint strategy: matrix: - sanitizer: [address] + sanitizer: [address, undefined] steps: - name: Checkout uses: actions/checkout@v4 @@ -56,6 +56,23 @@ jobs: - name: Unit Tests run: ctest --test-dir build-${{ matrix.sanitizer }} --output-on-failure + fuzz-build: + runs-on: ubuntu-latest + container: gitea.tap-tap.win/taptap/fastsync-ci:v7 + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install clang + run: apt-get update -qq && apt-get install -y -qq clang + + - name: Configure (clang + fuzz) + run: CC=clang CXX=clang++ cmake -B build-fuzz -S . -DENABLE_FUZZ=ON + + - name: Build fuzz targets + run: cmake --build build-fuzz -j$(nproc) + coverage: runs-on: ubuntu-latest container: gitea.tap-tap.win/taptap/fastsync-ci:v7 @@ -79,7 +96,7 @@ jobs: - name: Coverage Report run: | lcov --capture --directory build --output-file coverage.info --branch-coverage - lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info --branch-coverage --ignore-errors unused + lcov --remove coverage.info '/usr/*' '*/tests/*' '*/_deps/*' --output-file coverage.info --branch-coverage --ignore-errors unused lcov --list coverage.info valgrind: diff --git a/.gitignore b/.gitignore index fd4ffad..1001d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ test_data/ __pycache__/ build-asan coverage.info +build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aa05bb..db6a15f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,8 +9,8 @@ set(CMAKE_C_STANDARD_REQUIRED ON) add_compile_options(-Wall -g -O3) # --- Sanitizer option --- -set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)") -set_property(CACHE SANITIZER PROPERTY STRINGS address thread none) +set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, undefined, none)") +set_property(CACHE SANITIZER PROPERTY STRINGS address thread undefined none) if(SANITIZER STREQUAL "address") add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g) @@ -18,8 +18,11 @@ if(SANITIZER STREQUAL "address") elseif(SANITIZER STREQUAL "thread") add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g) add_link_options(-fsanitize=thread) +elseif(SANITIZER STREQUAL "undefined") + add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer -g) + add_link_options(-fsanitize=undefined) elseif(NOT SANITIZER STREQUAL "none") - message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, none") + message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, undefined, none") endif() # --- Strict warnings option --- @@ -84,7 +87,10 @@ add_test(NAME unit_all COMMAND tests) # --- Fuzz targets (requires clang) --- option(ENABLE_FUZZ "Build fuzz targets (requires clang)" OFF) if(ENABLE_FUZZ) - file(GLOB FUZZ_SRCS "tests/fuzz/*.c") + if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR "ENABLE_FUZZ requires Clang (compiler is ${CMAKE_C_COMPILER_ID})") + endif() + file(GLOB FUZZ_SRCS "tests/fuzz/*.c") foreach(FUZZ_SRC ${FUZZ_SRCS}) get_filename_component(FUZZ_NAME ${FUZZ_SRC} NAME_WE) add_executable(${FUZZ_NAME} ${FUZZ_SRC} ${SHARED_SRCS}) diff --git a/tests/fuzz/fuzz_chunk_deserialize.c b/tests/fuzz/fuzz_chunk_deserialize.c index 67bba49..444a297 100644 --- a/tests/fuzz/fuzz_chunk_deserialize.c +++ b/tests/fuzz/fuzz_chunk_deserialize.c @@ -2,6 +2,7 @@ #include "data.h" #include #include +#include int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) diff --git a/tests/fuzz/fuzz_compress_decompress.c b/tests/fuzz/fuzz_compress_decompress.c index ef9bdd4..e1dac1a 100644 --- a/tests/fuzz/fuzz_compress_decompress.c +++ b/tests/fuzz/fuzz_compress_decompress.c @@ -2,6 +2,7 @@ #include "data.h" #include #include +#include int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) diff --git a/tests/fuzz/fuzz_delta_deserialize.c b/tests/fuzz/fuzz_delta_deserialize.c index dcbaf6a..5159ab0 100644 --- a/tests/fuzz/fuzz_delta_deserialize.c +++ b/tests/fuzz/fuzz_delta_deserialize.c @@ -2,6 +2,7 @@ #include "data.h" #include #include +#include int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) diff --git a/tests/fuzz/fuzz_delta_signature_deserialize.c b/tests/fuzz/fuzz_delta_signature_deserialize.c index 03122a1..6e83fc7 100644 --- a/tests/fuzz/fuzz_delta_signature_deserialize.c +++ b/tests/fuzz/fuzz_delta_signature_deserialize.c @@ -2,6 +2,7 @@ #include "data.h" #include #include +#include int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) diff --git a/tests/fuzz/fuzz_metadata_from_buf.c b/tests/fuzz/fuzz_metadata_from_buf.c index 5c63dc2..00d051d 100644 --- a/tests/fuzz/fuzz_metadata_from_buf.c +++ b/tests/fuzz/fuzz_metadata_from_buf.c @@ -2,9 +2,10 @@ #include "file.h" #include #include +#include int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - if (size < sizeof(int)) + if (size < sizeof(int) + FILE_METADATA_WIRE_SIZE) return 0; char* buf = malloc(size); diff --git a/tests/test_file.c b/tests/test_file.c index 13de66c..1a65a05 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -272,6 +272,11 @@ void test_file() { test_to_disk_creates_dirs(); test_file_content_to_buffer(); if (!getenv("FASTSYNC_UNDER_VALGRIND")) { + // Fork tests are skipped under valgrind because the parent process runs + // orders of magnitude slower than the child (parent is instrumented, child + // is not), which causes pipe-based protocol handshake timeouts. The parent + // process itself has zero valgrind errors -- the failures are all in the + // forked children where inherited allocations are reported as leaks. test_file_send_receive(); test_file_send_no_path(); } diff --git a/tests/test_property.c b/tests/test_property.c index a0e96e8..503c85b 100644 --- a/tests/test_property.c +++ b/tests/test_property.c @@ -10,7 +10,6 @@ #include #include #include -#include static Data* random_data(int min_size, int max_size) { int size = min_size + rand() % (max_size - min_size + 1); @@ -21,7 +20,7 @@ static Data* random_data(int min_size, int max_size) { } static void test_property_compress_roundtrip() { - srand((unsigned)time(NULL)); + srand(42); for (int iter = 0; iter < 10; iter++) { Data* original = random_data(1, 10000); EXPECT_NOT_NULL(original); @@ -114,24 +113,8 @@ static void test_property_chunk_roundtrip() { } } -static void test_property_glob_consistency() { - const char* patterns[] = {"*.txt", "test_*", "*.c", "foo", "*.??", "a*b*c"}; - const char* strings[] = {"test.txt", "foo.c", "bar", "abc", "aXbYcZ", ""}; - int n_patterns = sizeof(patterns) / sizeof(patterns[0]); - int n_strings = sizeof(strings) / sizeof(strings[0]); - - for (int p = 0; p < n_patterns; p++) { - for (int s = 0; s < n_strings; s++) { - bool first = glob_match(patterns[p], strings[s]); - bool second = glob_match(patterns[p], strings[s]); - EXPECT_TRUE(first == second); - } - } -} - void test_property() { test_property_compress_roundtrip(); test_property_delta_roundtrip(); test_property_chunk_roundtrip(); - test_property_glob_consistency(); } diff --git a/tests/test_robustness.c b/tests/test_robustness.c index 2b9b1d4..8a3668a 100644 --- a/tests/test_robustness.c +++ b/tests/test_robustness.c @@ -126,6 +126,35 @@ static void test_delta_signature_deserialize_truncated() { delta_signature_destroy(sig); } +static void test_delta_deserialize_truncated_instructions() { + // Create a real delta with 2 LITERAL instructions, serialize, then + // truncate after the header so the instruction-loop error paths are + // exercised (earlier tests with tiny buffers die at the 12-byte + // header guard and never reach the instruction decoder). + char old_data[4096], new_data[4096]; + for (int i = 0; i < 4096; i++) { + old_data[i] = (char)(i % 256); + new_data[i] = old_data[i]; + } + // Two small changes to produce 2 LITERAL instructions + new_data[100] = 'X'; + new_data[200] = 'Y'; + + DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024); + Delta* delta = delta_compute(new_data, 4096, sig, 1024); + Data* serialized = delta_serialize(delta); + EXPECT_NOT_NULL(serialized); + + // Truncate to include the header (12 bytes) + partial first instruction + serialized->size = 14; + const Delta* result = delta_deserialize(serialized); + EXPECT_NULL(result); + + data_destroy(serialized); + delta_destroy(delta); + delta_signature_destroy(sig); +} + static void test_delta_apply_null() { const void* result = delta_apply(NULL, 0, NULL, 0); EXPECT_NULL(result); @@ -177,6 +206,7 @@ void test_robustness() { test_delta_deserialize_truncated(); test_delta_deserialize_empty(); test_delta_deserialize_garbage(); + test_delta_deserialize_truncated_instructions(); test_delta_signature_deserialize_truncated(); test_delta_apply_null(); test_protocol_receive_n_data_closed_pipe();