diff --git a/.github/workflows/on_PR_linux_fuzz.yml b/.github/workflows/on_PR_linux_fuzz.yml new file mode 100644 index 00000000..9fb1e5b2 --- /dev/null +++ b/.github/workflows/on_PR_linux_fuzz.yml @@ -0,0 +1,30 @@ +# Builds and runs the fuzz target for a short amount of time. This is +# mainly to protect the fuzz target from bitrot, but hopefully will +# also help to quickly catch some bugs before the PR is merged. + +name: Linux-Ubuntu Quick Fuzz on PRs + +on: + pull_request: + workflow_dispatch: + +jobs: + Linux: + name: 'Ubuntu 20.04 - clang/libFuzzer' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: install dependencies + run: sudo ./ci/install_dependencies.sh + - name: build and compile + run: | + mkdir build && cd build + cmake -DEXIV2_ENABLE_PNG=ON -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON -DEXIV2_ENABLE_BMFF=ON -DEXIV2_TEAM_WARNINGS_AS_ERRORS=ON -DCMAKE_CXX_COMPILER=$(which clang++) -DEXIV2_BUILD_FUZZ_TESTS=ON -DEXIV2_TEAM_USE_SANITIZERS=ON .. + make -j $(nproc) + + - name: Fuzz + run: | + cd build + mkdir corpus + ./bin/fuzz-read-print-write corpus ../test/data/ -jobs=$(nproc) -workers=$(nproc) -max_total_time=120 -max_len=4096 diff --git a/CMakeLists.txt b/CMakeLists.txt index aabb3dca..474c6d4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ option( EXIV2_ENABLE_BMFF "Build with BMFF support" option( EXIV2_BUILD_SAMPLES "Build sample applications" ON ) option( EXIV2_BUILD_EXIV2_COMMAND "Build exiv2 command-line executable" ON ) option( EXIV2_BUILD_UNIT_TESTS "Build unit tests" OFF ) +option( EXIV2_BUILD_FUZZ_TESTS "Build fuzz tests (libFuzzer)" OFF ) option( EXIV2_BUILD_DOC "Add 'doc' target to generate documentation" OFF ) # Only intended to be used by Exiv2 developers/contributors @@ -91,6 +92,14 @@ if( EXIV2_BUILD_UNIT_TESTS ) add_subdirectory ( unitTests ) endif() +if( EXIV2_BUILD_FUZZ_TESTS ) + if ((NOT COMPILER_IS_CLANG) OR (NOT EXIV2_TEAM_USE_SANITIZERS)) + message(FATAL_ERROR "You need to build with Clang and sanitizers for the fuzzers to work. " + "Use Clang and -DEXIV2_TEAM_USE_SANITIZERS=ON") + endif() + add_subdirectory ( fuzz ) +endif() + if( EXIV2_BUILD_SAMPLES ) add_subdirectory( samples ) get_directory_property(SAMPLES DIRECTORY samples DEFINITION APPLICATIONS) diff --git a/README.md b/README.md index aa4dbe64..f0ae44a2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ The file ReadMe.txt in a build bundle describes how to install the library on th 3. [Unit tests](#4-3) 4. [Python tests](#4-4) 5. [Test Summary](#4-5) + 6. [Fuzzing](#4-6) 5. [Platform Notes](#5) 1. [Linux](#5-1) 2. [macOS](#5-2) @@ -1039,6 +1040,30 @@ $ cd /build $ make python_tests 2>&1 | grep FAIL ``` +### 4.6 Fuzzing + +The code for the fuzzers is in `exiv2dir/fuzz` + +To build the fuzzers, use the *cmake* option `-DEXIV2_BUILD_FUZZ_TESTS=ON` and `-DEXIV2_TEAM_USE_SANITIZERS=ON`. +Note that it only works with clang compiler as libFuzzer is integrate with clang > 6.0 + +To build the fuzzers: + +```bash +$ cd +$ rm -rf build-fuzz ; mkdir build-fuzz ; cd build-fuzz +$ cmake .. -DCMAKE_CXX_COMPILER=$(which clang++) -DEXIV2_BUILD_FUZZ_TESTS=ON -DEXIV2_TEAM_USE_SANITIZERS=ON +$ cmake --build . +``` + +To execute a fuzzer: + +```bash +cd /build-fuzz +mkdir corpus +./bin/fuzz-read-print-write corpus ../test/data/ -jobs=$(nproc) -workers=$(nproc) -max_len=4096 +``` + [TOC](#TOC)
diff --git a/cmake/compilerFlags.cmake b/cmake/compilerFlags.cmake index 20f6ac53..8459fa0e 100644 --- a/cmake/compilerFlags.cmake +++ b/cmake/compilerFlags.cmake @@ -82,7 +82,9 @@ if ( MINGW OR UNIX OR MSYS ) # MINGW, Linux, APPLE, CYGWIN set(SANITIZER_FLAGS "-fno-omit-frame-pointer -fsanitize=address") endif() elseif( COMPILER_IS_CLANG ) - if ( CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9 ) + if ( EXIV2_BUILD_FUZZ_TESTS ) + set(SANITIZER_FLAGS "-fsanitize=fuzzer-no-link") + elseif ( CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9 ) set(SANITIZER_FLAGS "-fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-recover=all") elseif ( CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.4 ) set(SANITIZER_FLAGS "-fno-omit-frame-pointer -fsanitize=address,undefined") diff --git a/cmake/printSummary.cmake b/cmake/printSummary.cmake index 15f9bbda..71e4a0cf 100644 --- a/cmake/printSummary.cmake +++ b/cmake/printSummary.cmake @@ -66,6 +66,7 @@ endif() OptionOutput( "Building exiv2 command: " EXIV2_BUILD_EXIV2_COMMAND ) OptionOutput( "Building samples: " EXIV2_BUILD_SAMPLES ) OptionOutput( "Building unit tests: " EXIV2_BUILD_UNIT_TESTS ) +OptionOutput( "Building fuzz tests: " EXIV2_BUILD_FUZZ_TESTS ) OptionOutput( "Building doc: " EXIV2_BUILD_DOC ) OptionOutput( "Building with coverage flags: " BUILD_WITH_COVERAGE ) OptionOutput( "Using ccache: " BUILD_WITH_CCACHE ) diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt new file mode 100644 index 00000000..378b67b1 --- /dev/null +++ b/fuzz/CMakeLists.txt @@ -0,0 +1,14 @@ + +macro(fuzzer name) + add_executable(${name} ${name}.cpp) + set_target_properties(${name} + PROPERTIES + COMPILE_FLAGS "-fsanitize=fuzzer" + LINK_FLAGS "-fsanitize=fuzzer") + target_link_libraries(${name} + PRIVATE + exiv2lib + ) +endmacro() + +fuzzer(fuzz-read-print-write) diff --git a/fuzz/fuzz-read-print-write.cpp b/fuzz/fuzz-read-print-write.cpp new file mode 100644 index 00000000..b37b26ad --- /dev/null +++ b/fuzz/fuzz-read-print-write.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t * data, size_t size) { + // Invalid files generate a lot of warnings, so switch off logging. + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); + + Exiv2::XmpParser::initialize(); + ::atexit(Exiv2::XmpParser::terminate); + + try { + Exiv2::DataBuf data_copy(data, size); + Exiv2::Image::UniquePtr image = + Exiv2::ImageFactory::open(data_copy.pData_, size); + assert(image.get() != 0); + + image->readMetadata(); + image->exifData(); + + // Print to a std::ostringstream so that the fuzzer doesn't + // produce lots of garbage on stdout. + std::ostringstream buffer; + image->printStructure(buffer, Exiv2::kpsNone); + + image->writeMetadata(); + + } catch(...) { + // Exiv2 throws an exception if the metadata is invalid. + } + + return 0; +} diff --git a/src/iptc.cpp b/src/iptc.cpp index 9ae64d81..4252a988 100644 --- a/src/iptc.cpp +++ b/src/iptc.cpp @@ -474,13 +474,13 @@ namespace Exiv2 { #endif } } -#ifndef SUPPRESS_WARNINGS else { +#ifndef SUPPRESS_WARNINGS EXV_WARNING << "IPTC dataset " << IptcKey(dataSet, record) << " has invalid size " << sizeData << "; skipped.\n"; +#endif return 7; } -#endif pRead += sizeData; } diff --git a/src/jpgimage.cpp b/src/jpgimage.cpp index e722a797..6e6f0f96 100644 --- a/src/jpgimage.cpp +++ b/src/jpgimage.cpp @@ -181,7 +181,7 @@ namespace Exiv2 { #endif return -2; } -#ifndef EXIV2_DEBUG_MESSAGES +#ifdef EXIV2_DEBUG_MESSAGES if ( (dataSize & 1) && position + dataSize == static_cast(sizePsData)) { std::cerr << "Warning: " diff --git a/src/tiffvisitor_int.cpp b/src/tiffvisitor_int.cpp index 063cd250..ccf12a4d 100644 --- a/src/tiffvisitor_int.cpp +++ b/src/tiffvisitor_int.cpp @@ -1335,7 +1335,9 @@ namespace Exiv2 { tc->setStart(p); object->addChild(std::move(tc)); } else { +#ifndef SUPPRESS_WARNINGS EXV_WARNING << "Unable to handle tag " << tag << ".\n"; +#endif } p += 12; }