Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions .github/workflows/fuzzing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`.
41 changes: 39 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
107 changes: 107 additions & 0 deletions tests/fuzz/fuzz_bitmap_file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "../../src/bitmapfile/bitmap_file.h" // For Bitmap::File
#include <cstdint>
#include <vector>
#include <string>
#include <cstring> // For std::memcpy
#include <algorithm> // For std::remove_if, std::min
#include <cctype> // For std::isprint
#include <stdexcept> // For std::bad_alloc

// Helper to consume data from the fuzzer input
template <typename T>
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<uint8_t>(&Data, &Size);
size_t filename_len = static_cast<size_t>(filename_len_byte) % max_filename_len;

if (filename_len > 0 && Size >= filename_len) {
dummy_filename.assign(reinterpret_cast<const char*>(Data), filename_len);
dummy_filename.erase(std::remove_if(dummy_filename.begin(), dummy_filename.end(), [](char c){
return !std::isprint(static_cast<unsigned char>(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<uint8_t>(&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;
}
120 changes: 120 additions & 0 deletions tests/fuzz/fuzz_bmp_tool_save.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#include "../../include/bitmap.hpp" // For BmpTool::Bitmap, BmpTool::save, BmpTool::BitmapError
#include <cstdint>
#include <vector>
#include <algorithm> // For std::min
#include <cstring> // For std::memcpy
#include <stdexcept> // For std::bad_alloc

// Helper to consume data from the fuzzer input
template <typename T>
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<uint32_t>(&Data, &Size) % 1024;
bmp.h = Consume<uint32_t>(&Data, &Size) % 1024;

uint8_t bpp_choice = Consume<uint8_t>(&Data, &Size);
if (bpp_choice % 3 == 0) {
bmp.bpp = 32;
} else if (bpp_choice % 3 == 1) {
bmp.bpp = 24;
} else {
bmp.bpp = Consume<uint8_t>(&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<size_t>(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<uint16_t>(&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<uint8_t> 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<uint8_t>(out_buffer.data(), out_buffer.size()));

return 0;
}
Loading
Loading