diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index c95da62..799bc3d 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -26,15 +26,33 @@ jobs: CC: clang CXX: clang++ - - name: Build fuzz target - run: cmake --build build_fuzz --target fuzz_bitmap --config Debug + - name: Build all fuzz targets + run: cmake --build build_fuzz --config Debug # This will build all executables configured for the Debug build - - name: Create corpus directory - run: mkdir -p build_fuzz/corpus_fuzzing # Separate from build-time corpus + - name: Create corpus directories + run: | + mkdir -p build_fuzz/corpus_fuzzing/fuzz_bitmap_corpus + mkdir -p build_fuzz/corpus_fuzzing/fuzz_bmp_tool_save_corpus + mkdir -p build_fuzz/corpus_fuzzing/fuzz_bitmap_file_corpus + mkdir -p build_fuzz/corpus_fuzzing/fuzz_image_operations_corpus + mkdir -p build_fuzz/corpus_fuzzing/fuzz_matrix_corpus + + - name: Run fuzz_bitmap + run: | + ./build_fuzz/tests/fuzz_bitmap -max_total_time=60 -print_final_stats=1 -print_pcs=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/fuzz_bitmap_corpus/ + + - name: Run fuzz_bmp_tool_save + run: | + ./build_fuzz/tests/fuzz_bmp_tool_save -max_total_time=60 -print_final_stats=1 -print_pcs=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/fuzz_bmp_tool_save_corpus/ + + - name: Run fuzz_bitmap_file + run: | + ./build_fuzz/tests/fuzz_bitmap_file -max_total_time=60 -print_final_stats=1 -print_pcs=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/fuzz_bitmap_file_corpus/ + + - name: Run fuzz_image_operations + run: | + ./build_fuzz/tests/fuzz_image_operations -max_total_time=60 -print_final_stats=1 -print_pcs=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/fuzz_image_operations_corpus/ - - name: Run fuzzer + - name: Run fuzz_matrix run: | - ./build_fuzz/tests/fuzz_bitmap -max_total_time=60 -print_final_stats=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/ - # Optionally, if you want to use existing corpus from the repo (e.g., tests/fuzz/corpus) - # ./build_fuzz/tests/fuzz_bitmap -max_total_time=60 -print_final_stats=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/ tests/fuzz/corpus/ - # If the fuzzer finds a crash, error_exitcode=1 will make the step fail. + ./build_fuzz/tests/fuzz_matrix -max_total_time=60 -print_final_stats=1 -print_pcs=1 -error_exitcode=1 build_fuzz/corpus_fuzzing/fuzz_matrix_corpus/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 83303a7..02f7a38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ if(ENABLE_FUZZING) if(ENABLE_FUZZING AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "Fuzzing enabled. Using Clang compiler.") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=address,fuzzer-no-link") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fsanitize=address,undefined,fuzzer-no-link") # For linking fuzz targets, we'll use -fsanitize=fuzzer later else() message(STATUS "Fuzzing disabled or Clang not used.") diff --git a/README.md b/README.md index b119632..4165780 100644 --- a/README.md +++ b/README.md @@ -196,3 +196,29 @@ int main() { } ``` Refer to `main.cpp` for more examples. + +## Fuzz Testing + +This project includes a suite of fuzz tests to help ensure code robustness and identify potential vulnerabilities. The fuzzing setup uses Clang's libFuzzer along with AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan). + +### Enabling Fuzzing + +To build the fuzz targets, enable the `ENABLE_FUZZING` option when configuring with CMake: + +```bash +cmake -S . -B build_fuzz -DENABLE_FUZZING=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build build_fuzz --config Debug +``` +This requires Clang to be installed and set as the C++ compiler. The GitHub Actions workflow (`.github/workflows/fuzzing.yml`) does this automatically. + +### Available Fuzz Targets + +The following fuzz targets are available and will be built when fuzzing is enabled: + +* `fuzz_bitmap`: Tests the `BmpTool::load` function from `include/bitmap.hpp`. +* `fuzz_bmp_tool_save`: Tests the `BmpTool::save` function from `include/bitmap.hpp`. +* `fuzz_bitmap_file`: Tests operations of the `Bitmap::File` class from `src/bitmapfile/bitmap_file.h`. +* `fuzz_image_operations`: Tests various image manipulation functions from `src/bitmap/bitmap.h`. +* `fuzz_matrix`: Tests operations of the `Matrix::Matrix` class from `src/matrix/matrix.h`. + +Each fuzzer will run for a short duration (e.g., 60 seconds) when executed via the GitHub Actions workflow. They maintain their own corpus directories within `build_fuzz/corpus_fuzzing/`. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8245cf5..246d741 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,8 +29,45 @@ include(GoogleTest) gtest_discover_tests(bitmap_tests) if(ENABLE_FUZZING AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(FUZZ_LINK_FLAGS "-fsanitize=address,undefined,fuzzer") # Includes 'undefined' + + # Original fuzzer, updated add_executable(fuzz_bitmap fuzz/fuzz_bitmap.cpp) target_link_libraries(fuzz_bitmap PRIVATE bitmap) - set_target_properties(fuzz_bitmap PROPERTIES LINK_FLAGS "-fsanitize=address,fuzzer") - message(STATUS "Fuzz target fuzz_bitmap added.") + set_target_properties(fuzz_bitmap PROPERTIES LINK_FLAGS "${FUZZ_LINK_FLAGS}") + message(STATUS "Fuzz target fuzz_bitmap (updated flags) added.") + + # New fuzzer for BmpTool::save + add_executable(fuzz_bmp_tool_save fuzz/fuzz_bmp_tool_save.cpp) + target_link_libraries(fuzz_bmp_tool_save PRIVATE bitmap) + set_target_properties(fuzz_bmp_tool_save PROPERTIES LINK_FLAGS "${FUZZ_LINK_FLAGS}") + message(STATUS "Fuzz target fuzz_bmp_tool_save added.") + + # New fuzzer for Bitmap::File operations + add_executable(fuzz_bitmap_file fuzz/fuzz_bitmap_file.cpp) + target_link_libraries(fuzz_bitmap_file PRIVATE bitmap) + set_target_properties(fuzz_bitmap_file PROPERTIES LINK_FLAGS "${FUZZ_LINK_FLAGS}") + message(STATUS "Fuzz target fuzz_bitmap_file added.") + + # New fuzzer for image manipulation functions + add_executable(fuzz_image_operations fuzz/fuzz_image_operations.cpp) + target_link_libraries(fuzz_image_operations PRIVATE bitmap) + set_target_properties(fuzz_image_operations PROPERTIES LINK_FLAGS "${FUZZ_LINK_FLAGS}") + message(STATUS "Fuzz target fuzz_image_operations added.") + + # New fuzzer for Matrix operations + add_executable(fuzz_matrix fuzz/fuzz_matrix.cpp) + target_link_libraries(fuzz_matrix PRIVATE bitmap) + set_target_properties(fuzz_matrix PROPERTIES LINK_FLAGS "${FUZZ_LINK_FLAGS}") + message(STATUS "Fuzz target fuzz_matrix added.") + endif() +# Note: The public include directories from the 'bitmap' target +# (configured in the main CMakeLists.txt) should be automatically inherited +# by these fuzz targets because they link against 'bitmap'. +# This means include paths like "bitmap.hpp" (for top-level include files) +# or "bitmapfile/bitmap_file.h" (for files within src/) should work +# in the fuzzers if the fuzzers are updated to use them. +# The current fuzzers use relative paths like "../../include/bitmap.hpp", +# which also work due to the file structure but are less clean. +# This is a potential future cleanup for the fuzzer source files, not this CMake update. diff --git a/tests/fuzz/fuzz_bitmap_file.cpp b/tests/fuzz/fuzz_bitmap_file.cpp new file mode 100644 index 0000000..48eba33 --- /dev/null +++ b/tests/fuzz/fuzz_bitmap_file.cpp @@ -0,0 +1,107 @@ +#include "../../src/bitmapfile/bitmap_file.h" // For Bitmap::File +#include +#include +#include +#include // For std::memcpy +#include // For std::remove_if, std::min +#include // For std::isprint +#include // For std::bad_alloc + +// Helper to consume data from the fuzzer input +template +T Consume(const uint8_t** data_ptr, size_t* size_ptr) { + if (*size_ptr < sizeof(T)) { + return T{}; + } + T value; + std::memcpy(&value, *data_ptr, sizeof(T)); + *data_ptr += sizeof(T); + *size_ptr -= sizeof(T); + return value; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size < 1) { + return 0; + } + + const uint8_t* initial_Data = Data; + size_t initial_Size = Size; + + std::string dummy_filename = "fuzz.bmp"; + size_t max_filename_len = std::min((size_t)32, Size); + + uint8_t filename_len_byte = Consume(&Data, &Size); + size_t filename_len = static_cast(filename_len_byte) % max_filename_len; + + if (filename_len > 0 && Size >= filename_len) { + dummy_filename.assign(reinterpret_cast(Data), filename_len); + dummy_filename.erase(std::remove_if(dummy_filename.begin(), dummy_filename.end(), [](char c){ + return !std::isprint(static_cast(c)) || c == '/' || c == '\\' || c == '\0'; + }), dummy_filename.end()); + + Data += filename_len; + Size -= filename_len; + + if (dummy_filename.empty() || dummy_filename.length() > max_filename_len) { + dummy_filename = "default_fuzz.bmp"; + } + } else { + dummy_filename = "short_fuzz.bmp"; + } + + Bitmap::File bmp_file_manual; + + if (Size >= sizeof(BITMAPFILEHEADER)) { + std::memcpy(&bmp_file_manual.bitmapFileHeader, Data, sizeof(BITMAPFILEHEADER)); + Data += sizeof(BITMAPFILEHEADER); + Size -= sizeof(BITMAPFILEHEADER); + + if (Size >= sizeof(BITMAPINFOHEADER)) { + std::memcpy(&bmp_file_manual.bitmapInfoHeader, Data, sizeof(BITMAPINFOHEADER)); + Data += sizeof(BITMAPINFOHEADER); + Size -= sizeof(BITMAPINFOHEADER); + + if (Size > 0) { + try { + const size_t MAX_BITMAP_DATA_ALLOC = 1024 * 1024 * 4; // 4MB limit + size_t data_to_assign = std::min(Size, MAX_BITMAP_DATA_ALLOC); + bmp_file_manual.bitmapData.assign(Data, Data + data_to_assign); + } catch (const std::bad_alloc&) { + bmp_file_manual.bitmapData.clear(); + } + } + + uint8_t set_valid_choice = 0; + if (Size > 0) { // Check if any data is left for this choice + set_valid_choice = Consume(&Data, &Size); + } else if (initial_Size > (initial_Data - Data)) { // Check if any byte left from original overall + // This condition might be tricky if Data hasn't moved or Size became 0 exactly at a boundary + // A simpler way: if Size is 0 here, use a default for set_valid_choice + set_valid_choice = initial_Data[initial_Size -1]; // Fallback to last byte of original input + } + + + if (set_valid_choice % 2 == 0) { + bmp_file_manual.SetValid(); + } + } + } + + [[maybe_unused]] bool is_valid = bmp_file_manual.IsValid(); + + bmp_file_manual.Rename(dummy_filename); + [[maybe_unused]] std::string current_filename = bmp_file_manual.Filename(); + + if (is_valid) { + bmp_file_manual.Save(); + bmp_file_manual.SaveAs(dummy_filename + "_saveas.bmp"); + } + + Bitmap::File bmp_file_default; + [[maybe_unused]] bool is_valid_default = bmp_file_default.IsValid(); + [[maybe_unused]] std::string fn_default = bmp_file_default.Filename(); + + + return 0; +} diff --git a/tests/fuzz/fuzz_bmp_tool_save.cpp b/tests/fuzz/fuzz_bmp_tool_save.cpp new file mode 100644 index 0000000..fdfd535 --- /dev/null +++ b/tests/fuzz/fuzz_bmp_tool_save.cpp @@ -0,0 +1,120 @@ +#include "../../include/bitmap.hpp" // For BmpTool::Bitmap, BmpTool::save, BmpTool::BitmapError +#include +#include +#include // For std::min +#include // For std::memcpy +#include // For std::bad_alloc + +// Helper to consume data from the fuzzer input +template +T Consume(const uint8_t** data_ptr, size_t* size_ptr) { + if (*size_ptr < sizeof(T)) { + return T{}; // Return default value if not enough data + } + T value; + std::memcpy(&value, *data_ptr, sizeof(T)); + *data_ptr += sizeof(T); + *size_ptr -= sizeof(T); + return value; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + const uint8_t* original_Data_ptr = Data; + size_t original_Size = Size; + + // 1. Construct BmpTool::Bitmap from fuzzer data + BmpTool::Bitmap bmp; + bmp.w = Consume(&Data, &Size) % 1024; + bmp.h = Consume(&Data, &Size) % 1024; + + uint8_t bpp_choice = Consume(&Data, &Size); + if (bpp_choice % 3 == 0) { + bmp.bpp = 32; + } else if (bpp_choice % 3 == 1) { + bmp.bpp = 24; + } else { + bmp.bpp = Consume(&Data, &Size); + } + + size_t bytes_per_pixel = 0; + if (bmp.bpp > 0) { + bytes_per_pixel = (bmp.bpp + 7) / 8; + } else { + bmp.bpp = 24; // Default to a valid bpp + bytes_per_pixel = 3; + } + + size_t expected_pixel_data_size = static_cast(bmp.w) * bmp.h * bytes_per_pixel; + + const size_t MAX_PIXEL_DATA_ALLOC = 1024 * 1024 * 4; // 4MB + size_t pixel_data_to_read = 0; + + if (bmp.w > 0 && bmp.h > 0) { + pixel_data_to_read = expected_pixel_data_size; + if (pixel_data_to_read > MAX_PIXEL_DATA_ALLOC) { + pixel_data_to_read = MAX_PIXEL_DATA_ALLOC; + } + if (pixel_data_to_read > Size) { + pixel_data_to_read = Size; + } + if (pixel_data_to_read > 0) { + try { + bmp.data.assign(Data, Data + pixel_data_to_read); + Data += pixel_data_to_read; + Size -= pixel_data_to_read; + } catch (const std::bad_alloc&) { + bmp.data.clear(); // Clear if assign fails + } + } else { + bmp.data.clear(); + } + } else { + bmp.data.clear(); + } + + if (bmp.data.empty() && bmp.w > 0 && bmp.h > 0 && expected_pixel_data_size > 0) { + size_t fill_size = std::min(expected_pixel_data_size, (size_t)256); + fill_size = std::min(fill_size, MAX_PIXEL_DATA_ALLOC); + if (fill_size > 0) { + try { + bmp.data.resize(fill_size, 0xAB); + } catch (const std::bad_alloc&) { + bmp.data.clear(); + } + } + } + + // 2. Prepare output buffer + uint16_t output_buffer_fuzz_val = Consume(&Data, &Size); + const size_t MAX_OUTPUT_BUFFER_ALLOC = 1024 * 1024 * 8; // 8MB + + size_t final_output_buffer_size = 54 + (output_buffer_fuzz_val % (MAX_OUTPUT_BUFFER_ALLOC - 53)); + final_output_buffer_size = std::min(final_output_buffer_size, MAX_OUTPUT_BUFFER_ALLOC); + final_output_buffer_size = std::max((size_t)54, final_output_buffer_size); + + std::vector out_buffer; + try { + out_buffer.resize(final_output_buffer_size); + if (Size > 0) { + std::memcpy(out_buffer.data(), Data, std::min(Size, out_buffer.size())); + } else { + // Try to use some portion of original data if current 'Data' pointer is past 'original_Data_ptr' + // and there's unconsumed original data. + size_t consumed_for_bmp_etc = Data - original_Data_ptr; + if (original_Size > consumed_for_bmp_etc) { + std::memcpy(out_buffer.data(), original_Data_ptr + consumed_for_bmp_etc, std::min(original_Size - consumed_for_bmp_etc, out_buffer.size())); + } + } + } catch (const std::bad_alloc&) { + try { + out_buffer.resize(54); + } catch (const std::bad_alloc&) { + return 0; + } + } + + // 3. Call BmpTool::save + [[maybe_unused]] auto result = BmpTool::save(bmp, std::span(out_buffer.data(), out_buffer.size())); + + return 0; +} diff --git a/tests/fuzz/fuzz_image_operations.cpp b/tests/fuzz/fuzz_image_operations.cpp new file mode 100644 index 0000000..07d2adc --- /dev/null +++ b/tests/fuzz/fuzz_image_operations.cpp @@ -0,0 +1,115 @@ +#include "../../src/bitmap/bitmap.h" // For image manipulation functions +#include "../../src/bitmapfile/bitmap_file.h" // For Bitmap::File +#include +#include +#include +#include // For std::memcpy +#include // For std::min, std::max +#include // For std::bad_alloc (and potentially other std::exceptions) + +// Helper to consume data from the fuzzer input +template +T Consume(const uint8_t** data_ptr, size_t* size_ptr) { + if (*size_ptr < sizeof(T)) { + return T{}; + } + T value; + std::memcpy(&value, *data_ptr, sizeof(T)); + *data_ptr += sizeof(T); + *size_ptr -= sizeof(T); + return value; +} + +// Helper to consume a float value (scaled from a byte) +float ConsumeFloat(const uint8_t** data_ptr, size_t* size_ptr, float min_val = 0.0f, float max_val = 2.0f) { // Default values for min_val and max_val + if (*size_ptr == 0) return (min_val + max_val) / 2.0f; // Default if no data + uint8_t byte_val = Consume(data_ptr, size_ptr); + if (min_val == max_val) return min_val; + return min_val + (static_cast(byte_val) / 255.0f) * (max_val - min_val); +} + +// Helper to consume an integer within a range +int ConsumeInt(const uint8_t** data_ptr, size_t* size_ptr, int min_val = 0, int max_val = 10) { // Default values for min_val and max_val + if (*size_ptr == 0) return min_val; // Default if no data + uint8_t byte_val = Consume(data_ptr, size_ptr); + if (max_val == min_val) return min_val; + int range = max_val - min_val + 1; + if (range <= 0) range = 1; + return min_val + (byte_val % range); +} + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size < sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 1) { + return 0; + } + + Bitmap::File bmp_file; + + if (Size < sizeof(BITMAPFILEHEADER)) return 0; + std::memcpy(&bmp_file.bitmapFileHeader, Data, sizeof(BITMAPFILEHEADER)); + Data += sizeof(BITMAPFILEHEADER); + Size -= sizeof(BITMAPFILEHEADER); + + if (Size < sizeof(BITMAPINFOHEADER)) return 0; + std::memcpy(&bmp_file.bitmapInfoHeader, Data, sizeof(BITMAPINFOHEADER)); + Data += sizeof(BITMAPINFOHEADER); + Size -= sizeof(BITMAPINFOHEADER); + + if (bmp_file.bitmapInfoHeader.biWidth < 0) bmp_file.bitmapInfoHeader.biWidth = 0; + bmp_file.bitmapInfoHeader.biWidth = std::min(bmp_file.bitmapInfoHeader.biWidth, (LONG)1024); + if (bmp_file.bitmapInfoHeader.biHeight < 0) bmp_file.bitmapInfoHeader.biHeight = 0; + bmp_file.bitmapInfoHeader.biHeight = std::min(bmp_file.bitmapInfoHeader.biHeight, (LONG)1024); + + short valid_bpp[] = {8, 16, 24, 32}; + bool bpp_is_valid = false; + for(short b : valid_bpp) { if(bmp_file.bitmapInfoHeader.biBitCount == b) {bpp_is_valid = true; break;}} + if (!bpp_is_valid) bmp_file.bitmapInfoHeader.biBitCount = 24; + + if (Size > 0) { + try { + const size_t MAX_BITMAP_DATA_ALLOC = 1024 * 1024 * 4; // 4MB limit + size_t data_to_assign = std::min(Size, MAX_BITMAP_DATA_ALLOC); + bmp_file.bitmapData.assign(Data, Data + data_to_assign); + Data += data_to_assign; + Size -= data_to_assign; + } catch (const std::bad_alloc&) { + bmp_file.bitmapData.clear(); + } + } + + bmp_file.SetValid(); + + uint8_t operation_choice = Consume(&Data, &Size); + + Bitmap::File result_bmp_file; + + switch (operation_choice % 24) { + case 0: result_bmp_file = ShrinkImage(bmp_file, ConsumeInt(&Data, &Size, 1, 8)); break; + case 1: result_bmp_file = RotateImageCounterClockwise(bmp_file); break; + case 2: result_bmp_file = RotateImageClockwise(bmp_file); break; + case 3: result_bmp_file = MirrorImage(bmp_file); break; + case 4: result_bmp_file = FlipImage(bmp_file); break; + case 5: result_bmp_file = GreyscaleImage(bmp_file); break; + case 6: result_bmp_file = ChangeImageBrightness(bmp_file, ConsumeFloat(&Data, &Size, 0.1f, 3.0f)); break; + case 7: result_bmp_file = ChangeImageContrast(bmp_file, ConsumeFloat(&Data, &Size, 0.1f, 3.0f)); break; + case 8: result_bmp_file = ChangeImageSaturation(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 9: result_bmp_file = ChangeImageSaturationBlue(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 10: result_bmp_file = ChangeImageSaturationGreen(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 11: result_bmp_file = ChangeImageSaturationRed(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 12: result_bmp_file = ChangeImageSaturationMagenta(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 13: result_bmp_file = ChangeImageSaturationYellow(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 14: result_bmp_file = ChangeImageSaturationCyan(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 3.0f)); break; + case 15: result_bmp_file = ChangeImageLuminanceBlue(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 16: result_bmp_file = ChangeImageLuminanceGreen(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 17: result_bmp_file = ChangeImageLuminanceRed(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 18: result_bmp_file = ChangeImageLuminanceMagenta(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 19: result_bmp_file = ChangeImageLuminanceYellow(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 20: result_bmp_file = ChangeImageLuminanceCyan(bmp_file, ConsumeFloat(&Data, &Size, 0.0f, 2.0f)); break; + case 21: result_bmp_file = InvertImageColors(bmp_file); break; + case 22: result_bmp_file = ApplySepiaTone(bmp_file); break; + case 23: result_bmp_file = ApplyBoxBlur(bmp_file, ConsumeInt(&Data, &Size, 0, 3)); break; + } + + return 0; +} diff --git a/tests/fuzz/fuzz_matrix.cpp b/tests/fuzz/fuzz_matrix.cpp new file mode 100644 index 0000000..bcba007 --- /dev/null +++ b/tests/fuzz/fuzz_matrix.cpp @@ -0,0 +1,224 @@ +#include "../../src/matrix/matrix.h" // For Matrix::Matrix +#include +#include +// #include // Not strictly needed for this fuzzer's core logic +#include // For std::memcpy +#include // For std::min, std::max +#include // For std::bad_alloc, std::out_of_range, std::invalid_argument, std::runtime_error +#include // For std::numeric_limits +#include // For std::isnan, std::isinf + +// Helper to consume data from the fuzzer input +template +T Consume(const uint8_t** data_ptr, size_t* size_ptr) { + if (*size_ptr < sizeof(T)) { + return T{}; + } + T value; + std::memcpy(&value, *data_ptr, sizeof(T)); + *data_ptr += sizeof(T); + *size_ptr -= sizeof(T); + return value; +} + +// Helper to consume a specific type, e.g., double +template +T ConsumeValue(const uint8_t** data_ptr, size_t* size_ptr) { + if (*size_ptr < sizeof(T)) { + if (std::is_floating_point::value) { + if (*size_ptr > 0 && *data_ptr != nullptr) { + T temp_val = 0; + unsigned char* p_temp_val = reinterpret_cast(&temp_val); + for(size_t i=0; i < std::min(sizeof(T), *size_ptr); ++i) { + p_temp_val[i] = (*data_ptr)[i]; + } + if (std::isnan(temp_val) || std::isinf(temp_val)) return static_cast(1.0); + return temp_val; + } + return static_cast(1.0); + } + return T{}; + } + T val; + std::memcpy(&val, *data_ptr, sizeof(T)); + *data_ptr += sizeof(T); + *size_ptr -= sizeof(T); + + if (std::is_floating_point::value) { + if (std::isnan(val) || std::isinf(val)) { + // Replace NaN/inf with a deterministic value based on its first byte if possible + if (sizeof(T) > 0) return static_cast( (reinterpret_cast(&val)[0] % 100) + 0.5); + return static_cast(1.0); // Fallback if sizeof(T) is 0 (should not happen) + } + } + return val; +} + + +using MatrixType = double; +// Reduced MAX_DIM to keep memory/time per run lower for fuzzing many ops. (e.g. 32x32) +const int MAX_DIM = 32; + +Matrix::Matrix CreateFuzzedMatrix(const uint8_t** Data, size_t* Size) { + uint8_t rows_byte = Consume(Data, Size); + uint8_t cols_byte = Consume(Data, Size); + + int r = rows_byte % MAX_DIM; + int c = cols_byte % MAX_DIM; + + uint8_t non_zero_choice = Consume(Data, Size); // To decide if dimensions should be forced non-zero + if (non_zero_choice % 10 != 0) { // ~90% of time, try to make dimensions non-zero if they landed on 0 + if (r == 0) r = 1 + (rows_byte % std::max(1, MAX_DIM-1)); // Ensure at least 1 if was 0 + if (c == 0) c = 1 + (cols_byte % std::max(1, MAX_DIM-1)); // Ensure at least 1 if was 0 + } else { // ~10% of time, allow actual 0 if byte % MAX_DIM was 0 + // If original byte was >= MAX_DIM (so it wrapped to 0), but we are in the "allow zero" path, + // it means we want it to be zero. But if it was, say, 1%MAX_DIM == 1, it stays 1. + // This logic is okay, r and c are already set. + } + if (r < 0) r = 0; // Should be impossible due to % MAX_DIM (non-negative) + if (c < 0) c = 0; + + + try { + Matrix::Matrix m(r, c); + for (int i = 0; i < r; ++i) { + for (int j = 0; j < c; ++j) { + // Allow consuming partial data for last element, or use default. + if (*Size >= sizeof(MatrixType) / 2 && *Size > 0 ) { + m[i][j] = ConsumeValue(Data, Size); + } else { + // Default pattern if not enough data for a full MatrixType or if Size is too small + m[i][j] = static_cast( (i+j) % 3 + 1 ); // e.g. 1,2,3,1,2,3... + } + } + } + return m; + } catch (const std::bad_alloc&) { + return Matrix::Matrix(0,0); // Return empty on allocation failure + } catch (const std::out_of_range&) { + // This might happen if MatrixRow constructor or resize fails in a specific way + return Matrix::Matrix(0,0); + } +} + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size < 5) { + return 0; + } + + uint8_t operation_choice = Consume(&Data, &Size); + + Matrix::Matrix m1 = CreateFuzzedMatrix(&Data, &Size); + Matrix::Matrix m2; + + // Conditionally create m2 only for operations that need it + bool m2_needed = false; + int op_mod = operation_choice % 23; // Number of cases + if (op_mod == 3 || op_mod == 4 || op_mod == 5 || op_mod == 10 || op_mod == 11 || op_mod == 18 || op_mod == 19) { + m2_needed = true; + } + + if (m2_needed) { + if (Size > 2) { // Min data for m2 dimensions (2 bytes for rows/cols byte) + m2 = CreateFuzzedMatrix(&Data, &Size); + } else { + // Not enough data for a full m2, create a default one that might match m1 for some ops + uint8_t choice = Consume(&Data, &Size); // Consume last byte if any + try { + if (choice % 2 == 0 && m1.rows() > 0 && m1.cols() > 0 && m1.rows() < MAX_DIM && m1.cols() < MAX_DIM) { + m2.resize(m1.rows(), m1.cols()); + } else { + m2.resize(choice % MAX_DIM, (Size > 0 ? Consume(&Data, &Size) : choice) % MAX_DIM); + } + // Populate m2 with default values if resized + for(size_t r_idx = 0; r_idx < m2.rows(); ++r_idx) { + for(size_t c_idx = 0; c_idx < m2.cols(); ++c_idx) { + m2[r_idx][c_idx] = static_cast((r_idx + c_idx) % 2 + 1); + } + } + } catch(...) { /* Ignore errors creating this fallback m2 */ } + } + } + + + Matrix::Matrix result_matrix; + MatrixType result_scalar = 0; + (void)result_scalar; + + try { + switch (op_mod) { + case 0: if (!m1.empty()) result_matrix = m1.Transpose(); break; + case 1: + if (m1.rows() == m1.cols() && !m1.empty()) { + result_scalar = m1.Determinant(); + } + break; + case 2: + if (m1.rows() == m1.cols() && !m1.empty()) { + result_matrix = m1.Inverse(); + } + break; + case 3: result_matrix = m1 + m2; break; + case 4: result_matrix = m1 - m2; break; + case 5: result_matrix = m1 * m2; break; + case 6: result_matrix = m1 * ConsumeValue(&Data, &Size); break; + case 7: result_matrix = m1 + ConsumeValue(&Data, &Size); break; + case 8: result_matrix = m1 - ConsumeValue(&Data, &Size); break; + case 9: + { + MatrixType scalar = ConsumeValue(&Data, &Size); + if (std::abs(scalar) < std::numeric_limits::epsilon() * 100 && scalar != 0) { /* scalar is tiny */ } + else if (scalar == 0) scalar = 1.0; // Avoid division by zero explicitly + result_matrix = m1 / scalar; + } + break; + case 10: m1 += m2; break; + case 11: m1 -= m2; break; + case 12: m1 *= ConsumeValue(&Data, &Size); break; + case 13: + { + MatrixType scalar = ConsumeValue(&Data, &Size); + if (std::abs(scalar) < std::numeric_limits::epsilon() * 100 && scalar != 0) { /* scalar is tiny */ } + else if (scalar == 0) scalar = 1.0; // Avoid division by zero + m1 /= scalar; + } + break; + case 14: if (!m1.empty()) m1.ZeroMatrix(); break; + case 15: if (m1.rows() == m1.cols() && !m1.empty()) m1.CreateIdentityMatrix(); break; + case 16: if (!m1.empty()) m1.Randomize(); break; + case 17: if (!m1.empty()) m1.SigmoidMatrix(); break; + case 18: result_matrix = m1.MergeHorizontal(m2); break; + case 19: result_matrix = m1.MergeVertical(m2); break; + case 20: + { + uint8_t r_byte = Consume(&Data, &Size); + uint8_t c_byte = Consume(&Data, &Size); + m1.resize(r_byte % MAX_DIM, c_byte % MAX_DIM); + } + break; + case 21: + { + uint8_t r_byte = Consume(&Data, &Size); + uint8_t c_byte = Consume(&Data, &Size); + MatrixType val = ConsumeValue(&Data, &Size); + m1.assign(r_byte % MAX_DIM, c_byte % MAX_DIM, val); + } + break; + case 22: + if (m1.rows() > 0 && m1.cols() > 0) { + size_t r_idx = Consume(&Data, &Size) % m1.rows(); + size_t c_idx = Consume(&Data, &Size) % m1.cols(); + [[maybe_unused]] MatrixType val_at = m1.at(r_idx, c_idx); + } + break; + // Default case removed to ensure all op_mod values map to a defined case. + } + } catch (const std::bad_alloc&) { + } catch (const std::out_of_range&) { + } catch (const std::invalid_argument&) { + } catch (const std::runtime_error&) { + } + + return 0; +}