diff --git a/CMakeLists.txt b/CMakeLists.txt index b785a8e6..f9e255dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ option(WEBP_BUILD_LIBWEBPMUX "Build the libwebpmux library." ON) option(WEBP_BUILD_WEBPMUX "Build the webpmux command line tool." ON) option(WEBP_BUILD_EXTRAS "Build extras." ON) option(WEBP_BUILD_WEBP_JS "Emscripten build of webp.js." OFF) +option(WEBP_BUILD_FUZZTEST "Build the fuzztest tests." OFF) option(WEBP_USE_THREAD "Enable threading support" ON) option(WEBP_NEAR_LOSSLESS "Enable near-lossless encoding" ON) option(WEBP_ENABLE_SWAP_16BIT_CSP "Enable byte swap for 16 bit colorspaces." @@ -375,9 +376,11 @@ if(XCODE) endif() target_link_libraries(webpdecoder ${WEBP_DEP_LIBRARIES}) target_include_directories( - webpdecoder PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} - INTERFACE $ - $) + webpdecoder + PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} + INTERFACE + "$" + $) set_target_properties( webpdecoder PROPERTIES PUBLIC_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/src/webp/decode.h;\ @@ -771,6 +774,10 @@ if(WEBP_BUILD_ANIM_UTILS) target_include_directories(anim_dump PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/src) endif() +if(WEBP_BUILD_FUZZTEST) + add_subdirectory(tests/fuzzer) +endif() + # Install the different headers and libraries. install( TARGETS ${INSTALLED_LIBRARIES} diff --git a/tests/README.md b/tests/README.md index 91daba26..60180c95 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,8 +11,9 @@ https://chromium.googlesource.com/webm/libwebp-test-data Follow the [build instructions](../doc/building.md) for libwebp, optionally adding build flags for various sanitizers (e.g., -fsanitize=address). -`fuzzer/makefile.unix` can then be used to compile the fuzzer targets: +`-DWEBP_BUILD_FUZZTEST=ON` can then be used to compile the fuzzer targets: ```shell -$ make -C fuzzer -f makefile.unix +$ cmake -B ./build -S . -DWEBP_BUILD_FUZZTEST=ON +$ make -C build ``` diff --git a/tests/fuzzer/CMakeLists.txt b/tests/fuzzer/CMakeLists.txt new file mode 100644 index 00000000..dbd5d030 --- /dev/null +++ b/tests/fuzzer/CMakeLists.txt @@ -0,0 +1,61 @@ +# Copyright (c) 2024 Google LLC +# +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file in the root of the source +# tree. An additional intellectual property rights grant can be found +# in the file PATENTS. All contributing project authors may +# be found in the AUTHORS file in the root of the source tree. + +# Adds a fuzztest from file TEST_NAME.cc located in the gtest folder. Extra +# arguments are considered as extra source files. + +if(CMAKE_VERSION VERSION_LESS "3.19.0") + return() +endif() + +macro(add_webp_fuzztest TEST_NAME) + add_executable(${TEST_NAME} ${TEST_NAME}.cc) + # FuzzTest bundles GoogleTest so no need to link to gtest libraries. + target_link_libraries(${TEST_NAME} PRIVATE fuzz_utils webp ${ARGN}) + link_fuzztest(${TEST_NAME}) + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + set_property( + TEST ${TEST_NAME} + PROPERTY ENVIRONMENT "TEST_DATA_DIRS=${CMAKE_CURRENT_SOURCE_DIR}/data/") +endmacro() + +enable_language(CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +set(FETCHCONTENT_QUIET FALSE) +set(fuzztest_SOURCE_DIR ${CMAKE_BINARY_DIR}/_deps/fuzztest-src) +FetchContent_Declare( + fuzztest + GIT_REPOSITORY https://github.com/google/fuzztest.git + GIT_TAG a40caf40aaf621dd0e04f9d8b47d1153fd2682d2 + GIT_PROGRESS TRUE + PATCH_COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/patch.sh) + +FetchContent_MakeAvailable(fuzztest) + +fuzztest_setup_fuzzing_flags() + +add_library(fuzz_utils fuzz_utils.h fuzz_utils.cc img_alpha.h img_grid.h + img_peak.h) +target_link_libraries(fuzz_utils PUBLIC webpdecoder) +link_fuzztest(fuzz_utils) + +add_webp_fuzztest(advanced_api_fuzzer) +add_webp_fuzztest(enc_dec_fuzzer) +add_webp_fuzztest(huffman_fuzzer) +add_webp_fuzztest(simple_api_fuzzer) + +if(WEBP_BUILD_LIBWEBPMUX) + add_webp_fuzztest(animation_api_fuzzer webpdemux) + add_webp_fuzztest(animdecoder_fuzzer imageioutil webpdemux) + add_webp_fuzztest(animencoder_fuzzer libwebpmux) + add_webp_fuzztest(mux_demux_api_fuzzer libwebpmux webpdemux) +endif() diff --git a/tests/fuzzer/advanced_api_fuzzer.c b/tests/fuzzer/advanced_api_fuzzer.cc similarity index 66% rename from tests/fuzzer/advanced_api_fuzzer.c rename to tests/fuzzer/advanced_api_fuzzer.cc index 22c689bb..5d24af81 100644 --- a/tests/fuzzer/advanced_api_fuzzer.c +++ b/tests/fuzzer/advanced_api_fuzzer.cc @@ -14,54 +14,59 @@ // //////////////////////////////////////////////////////////////////////////////// -#include -#include +#include +#include +#include +#include #include "./fuzz_utils.h" #include "src/utils/rescaler_utils.h" #include "src/webp/decode.h" -int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { +namespace { + +void AdvancedApiTest(std::string_view blob, uint8_t factor_u8, bool flip, + bool bypass_filtering, bool no_fancy_upsampling, + bool use_threads, bool use_cropping, bool use_scaling, + bool use_dithering, int colorspace, bool incremental) { WebPDecoderConfig config; - if (!WebPInitDecoderConfig(&config)) return 0; - if (WebPGetFeatures(data, size, &config.input) != VP8_STATUS_OK) return 0; - if ((size_t)config.input.width * config.input.height > kFuzzPxLimit) return 0; + if (!WebPInitDecoderConfig(&config)) return; + const uint8_t* const data = reinterpret_cast(blob.data()); + const size_t size = blob.size(); + if (WebPGetFeatures(data, size, &config.input) != VP8_STATUS_OK) return; + if ((size_t)config.input.width * config.input.height > + fuzz_utils::kFuzzPxLimit) { + return; + } // Using two independent criteria ensures that all combinations of options // can reach each path at the decoding stage, with meaningful differences. - const uint8_t value = FuzzHash(data, size); - const float factor = value / 255.f; // 0-1 + const uint8_t value = fuzz_utils::FuzzHash(data, size); + const float factor = factor_u8 / 255.f; // 0-1 - config.options.flip = value & 1; - config.options.bypass_filtering = value & 2; - config.options.no_fancy_upsampling = value & 4; - config.options.use_threads = value & 8; - if (size & 1) { + config.options.flip = flip; + config.options.bypass_filtering = bypass_filtering; + config.options.no_fancy_upsampling = no_fancy_upsampling; + config.options.use_threads = use_threads; + if (use_cropping) { config.options.use_cropping = 1; config.options.crop_width = (int)(config.input.width * (1 - factor)); config.options.crop_height = (int)(config.input.height * (1 - factor)); config.options.crop_left = config.input.width - config.options.crop_width; config.options.crop_top = config.input.height - config.options.crop_height; } - if (size & 2) { + if (use_dithering) { int strength = (int)(factor * 100); config.options.dithering_strength = strength; config.options.alpha_dithering_strength = 100 - strength; } - if (size & 4) { + if (use_scaling) { config.options.use_scaling = 1; config.options.scaled_width = (int)(config.input.width * factor * 2); config.options.scaled_height = (int)(config.input.height * factor * 2); } - -#if defined(WEBP_REDUCE_CSP) - config.output.colorspace = (value & 1) - ? ((value & 2) ? MODE_RGBA : MODE_BGRA) - : ((value & 2) ? MODE_rgbA : MODE_bgrA); -#else - config.output.colorspace = (WEBP_CSP_MODE)(value % MODE_LAST); -#endif // WEBP_REDUCE_CSP + config.output.colorspace = static_cast(colorspace); for (int i = 0; i < 2; ++i) { if (i == 1) { @@ -80,7 +85,7 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { if (WebPRescalerGetScaledDimensions(config.input.width, config.input.height, &scaled_width, &scaled_height)) { - size_t fuzz_px_limit = kFuzzPxLimit; + size_t fuzz_px_limit = fuzz_utils::kFuzzPxLimit; if (scaled_width != config.input.width || scaled_height != config.input.height) { // Using the WebPRescalerImport internally can significantly slow @@ -92,18 +97,18 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { // very wide input image to a very tall canvas can be as slow as // decoding a huge number of pixels. Avoid timeouts due to these. const uint64_t max_num_operations = - (uint64_t)Max(scaled_width, config.input.width) * - Max(scaled_height, config.input.height); + (uint64_t)std::max(scaled_width, config.input.width) * + std::max(scaled_height, config.input.height); if (max_num_operations > fuzz_px_limit) { break; } } } } - if (size % 3) { + if (incremental) { // Decodes incrementally in chunks of increasing size. WebPIDecoder* idec = WebPIDecode(NULL, 0, &config); - if (!idec) return 0; + if (!idec) return; VP8StatusCode status; if (size & 8) { size_t available_size = value + 1; @@ -135,5 +140,28 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPFreeDecBuffer(&config.output); } - return 0; } + +} // namespace + +FUZZ_TEST(AdvancedApi, AdvancedApiTest) + .WithDomains( + fuzztest::String() + .WithMaxSize(fuzz_utils::kMaxWebPFileSize + 1), + /*factor_u8=*/fuzztest::Arbitrary(), + /*flip=*/fuzztest::Arbitrary(), + /*bypass_filtering=*/fuzztest::Arbitrary(), + /*no_fancy_upsampling=*/fuzztest::Arbitrary(), + /*use_threads=*/fuzztest::Arbitrary(), + /*use_cropping=*/fuzztest::Arbitrary(), + /*use_scaling=*/fuzztest::Arbitrary(), + /*use_dithering=*/fuzztest::Arbitrary(), +#if defined(WEBP_REDUCE_CSP) + fuzztest::ElementOf({static_cast(MODE_RGBA), + static_cast(MODE_BGRA), + static_cast(MODE_rgbA), + static_cast(MODE_bgrA)}), +#else + fuzztest::InRange(0, static_cast(MODE_LAST) - 1), +#endif + /*incremental=*/fuzztest::Arbitrary()); diff --git a/tests/fuzzer/animation_api_fuzzer.c b/tests/fuzzer/animation_api_fuzzer.cc similarity index 60% rename from tests/fuzzer/animation_api_fuzzer.c rename to tests/fuzzer/animation_api_fuzzer.cc index 187ed24e..efe0e1db 100644 --- a/tests/fuzzer/animation_api_fuzzer.c +++ b/tests/fuzzer/animation_api_fuzzer.cc @@ -14,37 +14,46 @@ // //////////////////////////////////////////////////////////////////////////////// +#include +#include +#include + #include "./fuzz_utils.h" #include "src/webp/decode.h" #include "src/webp/demux.h" #include "src/webp/mux_types.h" -int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { +namespace { + +void AnimationApiTest(std::string_view blob, bool use_threads, + WEBP_CSP_MODE color_mode) { + const size_t size = blob.size(); WebPData webp_data; WebPDataInit(&webp_data); webp_data.size = size; - webp_data.bytes = data; + webp_data.bytes = reinterpret_cast(blob.data()); // WebPAnimDecoderNew uses WebPDemux internally to calloc canvas size. WebPDemuxer* const demux = WebPDemux(&webp_data); - if (!demux) return 0; + if (!demux) return; const uint32_t cw = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); const uint32_t ch = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); - if ((size_t)cw * ch > kFuzzPxLimit) { + if ((size_t)cw * ch > fuzz_utils::kFuzzPxLimit) { WebPDemuxDelete(demux); - return 0; + return; } // In addition to canvas size, check each frame separately. WebPIterator iter; - for (int i = 0; i < kFuzzFrameLimit; i++) { + for (int i = 0; i < fuzz_utils::kFuzzFrameLimit; i++) { if (!WebPDemuxGetFrame(demux, i + 1, &iter)) break; int w, h; if (WebPGetInfo(iter.fragment.bytes, iter.fragment.size, &w, &h)) { - if ((size_t)w * h > kFuzzPxLimit) { // image size of the frame payload + if ((size_t)w * h > + fuzz_utils::kFuzzPxLimit) { // image size of the frame payload WebPDemuxReleaseIterator(&iter); WebPDemuxDelete(demux); - return 0; + return; } } } @@ -53,26 +62,30 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPDemuxDelete(demux); WebPAnimDecoderOptions dec_options; - if (!WebPAnimDecoderOptionsInit(&dec_options)) return 0; + if (!WebPAnimDecoderOptionsInit(&dec_options)) return; - dec_options.use_threads = size & 1; - // Animations only support 4 (of 12) modes. - dec_options.color_mode = (WEBP_CSP_MODE)(size % MODE_LAST); - if (dec_options.color_mode != MODE_BGRA && - dec_options.color_mode != MODE_rgbA && - dec_options.color_mode != MODE_bgrA) { - dec_options.color_mode = MODE_RGBA; - } + dec_options.use_threads = use_threads; + dec_options.color_mode = color_mode; WebPAnimDecoder* dec = WebPAnimDecoderNew(&webp_data, &dec_options); - if (!dec) return 0; + if (!dec) return; - for (int i = 0; i < kFuzzFrameLimit; i++) { + for (int i = 0; i < fuzz_utils::kFuzzFrameLimit; i++) { uint8_t* buf; int timestamp; if (!WebPAnimDecoderGetNext(dec, &buf, ×tamp)) break; } WebPAnimDecoderDelete(dec); - return 0; } + +} // namespace + +FUZZ_TEST(AnimationApi, AnimationApiTest) + .WithDomains( + fuzztest::String() + .WithMaxSize(fuzz_utils::kMaxWebPFileSize + 1), + /*use_threads=*/fuzztest::Arbitrary(), + // Animations only support 4 (out of 12) modes. + fuzztest::ElementOf({MODE_RGBA, MODE_BGRA, MODE_rgbA, + MODE_bgrA})); diff --git a/tests/fuzzer/animdecoder_fuzzer.cc b/tests/fuzzer/animdecoder_fuzzer.cc index c3ea4758..9826a644 100644 --- a/tests/fuzzer/animdecoder_fuzzer.cc +++ b/tests/fuzzer/animdecoder_fuzzer.cc @@ -16,13 +16,20 @@ #include #include +#include +#include "./fuzz_utils.h" #include "imageio/imageio_util.h" #include "src/webp/decode.h" #include "src/webp/demux.h" #include "src/webp/mux_types.h" -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { +namespace { + +void AnimDecoderTest(std::string_view blob) { + const uint8_t* const data = reinterpret_cast(blob.data()); + const size_t size = blob.size(); + // WebPAnimDecoderGetInfo() is too late to check the canvas size as // WebPAnimDecoderNew() will handle the allocations. const size_t kMaxNumBytes = 2684354560; // RSS (resident set size) limit. @@ -34,14 +41,14 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { features.height) || static_cast(features.width) * features.height > kMaxNumPixelsSafe) { - return 0; + return; } } // decode everything as an animation WebPData webp_data = {data, size}; WebPAnimDecoder* const dec = WebPAnimDecoderNew(&webp_data, nullptr); - if (dec == nullptr) return 0; + if (dec == nullptr) return; WebPAnimInfo info; if (!WebPAnimDecoderGetInfo(dec, &info)) goto End; @@ -57,5 +64,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { } End: WebPAnimDecoderDelete(dec); - return 0; } + +} // namespace + +FUZZ_TEST(AnimDecoder, AnimDecoderTest) + .WithDomains( + fuzztest::String() + .WithMaxSize(fuzz_utils::kMaxWebPFileSize + 1)); diff --git a/tests/fuzzer/animencoder_fuzzer.cc b/tests/fuzzer/animencoder_fuzzer.cc index ef6ec1e4..85b603a4 100644 --- a/tests/fuzzer/animencoder_fuzzer.cc +++ b/tests/fuzzer/animencoder_fuzzer.cc @@ -14,21 +14,48 @@ // //////////////////////////////////////////////////////////////////////////////// -#include -#include +#include +#include +#include +#include +#include +#include +#include #include "./fuzz_utils.h" +#include "src/dsp/cpu.h" #include "src/webp/encode.h" #include "src/webp/mux.h" +#include "src/webp/mux_types.h" namespace { -const VP8CPUInfo default_VP8GetCPUInfo = VP8GetCPUInfo; +const VP8CPUInfo default_VP8GetCPUInfo = fuzz_utils::VP8GetCPUInfo; + +struct FrameConfig { + int use_argb; + int timestamp; + WebPConfig webp_config; + fuzz_utils::CropOrScaleParams crop_or_scale_params; + int source_image_index; +}; + +auto ArbitraryKMinKMax() { + return fuzztest::FlatMap( + [](int kmax) { + const int min_kmin = (kmax > 1) ? (kmax / 2) : 0; + const int max_kmin = (kmax > 1) ? (kmax - 1) : 0; + return fuzztest::PairOf(fuzztest::InRange(min_kmin, max_kmin), + fuzztest::Just(kmax)); + }, + fuzztest::InRange(0, 15)); +} int AddFrame(WebPAnimEncoder** const enc, const WebPAnimEncoderOptions& anim_config, int* const width, - int* const height, int timestamp_ms, const uint8_t data[], - size_t size, uint32_t* const bit_pos) { + int* const height, int timestamp_ms, + const FrameConfig& frame_config, const uint8_t data[], size_t size, + uint32_t* const bit_pos) { if (enc == nullptr || width == nullptr || height == nullptr) { fprintf(stderr, "NULL parameters.\n"); if (enc != nullptr) WebPAnimEncoderDelete(*enc); @@ -36,27 +63,12 @@ int AddFrame(WebPAnimEncoder** const enc, } // Init the source picture. - WebPPicture pic; - if (!WebPPictureInit(&pic)) { - fprintf(stderr, "WebPPictureInit failed.\n"); - WebPAnimEncoderDelete(*enc); - abort(); - } - pic.use_argb = Extract(1, data, size, bit_pos); - - // Read the source picture. - if (!ExtractSourcePicture(&pic, data, size, bit_pos)) { - const WebPEncodingError error_code = pic.error_code; - WebPAnimEncoderDelete(*enc); - WebPPictureFree(&pic); - if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY) return 0; - fprintf(stderr, "Can't read input image. Error code: %d\n", error_code); - abort(); - } + WebPPicture pic = fuzz_utils::GetSourcePicture( + frame_config.source_image_index, frame_config.use_argb); // Crop and scale. if (*enc == nullptr) { // First frame will set canvas width and height. - if (!ExtractAndCropOrScale(&pic, data, size, bit_pos)) { + if (!fuzz_utils::CropOrScale(&pic, frame_config.crop_or_scale_params)) { const WebPEncodingError error_code = pic.error_code; WebPPictureFree(&pic); if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY) return 0; @@ -89,13 +101,7 @@ int AddFrame(WebPAnimEncoder** const enc, } // Create frame encoding config. - WebPConfig config; - if (!ExtractWebPConfig(&config, data, size, bit_pos)) { - fprintf(stderr, "ExtractWebPConfig failed.\n"); - WebPAnimEncoderDelete(*enc); - WebPPictureFree(&pic); - abort(); - } + WebPConfig config = frame_config.webp_config; // Skip slow settings on big images, it's likely to timeout. if (pic.width * pic.height > 32 * 32) { config.method = (config.method > 4) ? 4 : config.method; @@ -125,14 +131,17 @@ int AddFrame(WebPAnimEncoder** const enc, return 1; } -} // namespace - -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { +void AnimEncoderTest(std::string_view blob, bool minimize_size, + std::pair kmin_kmax, bool allow_mixed, + const std::vector& frame_configs, + int optimization_index) { WebPAnimEncoder* enc = nullptr; int width = 0, height = 0, timestamp_ms = 0; uint32_t bit_pos = 0; + const uint8_t* const data = reinterpret_cast(blob.data()); + const size_t size = blob.size(); - ExtractAndDisableOptimizations(default_VP8GetCPUInfo, data, size, &bit_pos); + fuzz_utils::SetOptimization(default_VP8GetCPUInfo, optimization_index); // Extract a configuration from the packed bits. WebPAnimEncoderOptions anim_config; @@ -140,26 +149,20 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { fprintf(stderr, "WebPAnimEncoderOptionsInit failed.\n"); abort(); } - anim_config.minimize_size = Extract(1, data, size, &bit_pos); - anim_config.kmax = Extract(15, data, size, &bit_pos); - const int min_kmin = (anim_config.kmax > 1) ? (anim_config.kmax / 2) : 0; - const int max_kmin = (anim_config.kmax > 1) ? (anim_config.kmax - 1) : 0; - anim_config.kmin = - min_kmin + Extract((uint32_t)(max_kmin - min_kmin), data, size, &bit_pos); - anim_config.allow_mixed = Extract(1, data, size, &bit_pos); + anim_config.minimize_size = minimize_size; + anim_config.kmin = kmin_kmax.first; + anim_config.kmax = kmin_kmax.second; + anim_config.allow_mixed = allow_mixed; anim_config.verbose = 0; - const int nb_frames = 1 + Extract(15, data, size, &bit_pos); - // For each frame. - for (int i = 0; i < nb_frames; ++i) { - if (!AddFrame(&enc, anim_config, &width, &height, timestamp_ms, data, size, - &bit_pos)) { - return 0; + for (const FrameConfig& frame_config : frame_configs) { + if (!AddFrame(&enc, anim_config, &width, &height, timestamp_ms, + frame_config, data, size, &bit_pos)) { + return; } - timestamp_ms += (1 << (2 + Extract(15, data, size, &bit_pos))) + - Extract(1, data, size, &bit_pos); // [1..131073], arbitrary + timestamp_ms += frame_config.timestamp; } // Assemble. @@ -184,5 +187,22 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPAnimEncoderDelete(enc); WebPDataClear(&webp_data); - return 0; } + +} // namespace + +FUZZ_TEST(AnimEncoder, AnimEncoderTest) + .WithDomains( + fuzztest::String(), + /*minimize_size=*/fuzztest::Arbitrary(), ArbitraryKMinKMax(), + /*allow_mixed=*/fuzztest::Arbitrary(), + fuzztest::VectorOf( + fuzztest::StructOf( + fuzztest::InRange(0, 1), fuzztest::InRange(0, 131073), + fuzz_utils::ArbitraryWebPConfig(), + fuzz_utils::ArbitraryCropOrScaleParams(), + fuzztest::InRange(0, fuzz_utils::kNumSourceImages - 1))) + .WithMinSize(1) + .WithMaxSize(15), + /*optimization_index=*/ + fuzztest::InRange(0, fuzz_utils::kMaxOptimizationIndex)); diff --git a/tests/fuzzer/enc_dec_fuzzer.cc b/tests/fuzzer/enc_dec_fuzzer.cc index c5d46ae0..c6769aef 100644 --- a/tests/fuzzer/enc_dec_fuzzer.cc +++ b/tests/fuzzer/enc_dec_fuzzer.cc @@ -14,57 +14,37 @@ // //////////////////////////////////////////////////////////////////////////////// -#include -#include +#include +#include +#include #include "./fuzz_utils.h" +#include "src/dsp/cpu.h" #include "src/webp/decode.h" #include "src/webp/encode.h" namespace { -const VP8CPUInfo default_VP8GetCPUInfo = VP8GetCPUInfo; +const VP8CPUInfo default_VP8GetCPUInfo = fuzz_utils::VP8GetCPUInfo; -} // namespace - -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { - uint32_t bit_pos = 0; - - ExtractAndDisableOptimizations(default_VP8GetCPUInfo, data, size, &bit_pos); +void EncDecTest(bool use_argb, int source_image_index, WebPConfig config, + int optimization_index, + const fuzz_utils::CropOrScaleParams& crop_or_scale_params) { + fuzz_utils::SetOptimization(default_VP8GetCPUInfo, optimization_index); // Init the source picture. - WebPPicture pic; - if (!WebPPictureInit(&pic)) { - fprintf(stderr, "WebPPictureInit failed.\n"); - abort(); - } - pic.use_argb = Extract(1, data, size, &bit_pos); - - // Read the source picture. - if (!ExtractSourcePicture(&pic, data, size, &bit_pos)) { - const WebPEncodingError error_code = pic.error_code; - WebPPictureFree(&pic); - if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY) return 0; - fprintf(stderr, "Can't read input image. Error code: %d\n", error_code); - abort(); - } + WebPPicture pic = fuzz_utils::GetSourcePicture(source_image_index, use_argb); // Crop and scale. - if (!ExtractAndCropOrScale(&pic, data, size, &bit_pos)) { + if (!fuzz_utils::CropOrScale(&pic, crop_or_scale_params)) { const WebPEncodingError error_code = pic.error_code; WebPPictureFree(&pic); - if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY) return 0; + if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY) return; fprintf(stderr, "ExtractAndCropOrScale failed. Error code: %d\n", error_code); abort(); } - // Extract a configuration from the packed bits. - WebPConfig config; - if (!ExtractWebPConfig(&config, data, size, &bit_pos)) { - fprintf(stderr, "ExtractWebPConfig failed.\n"); - abort(); - } // Skip slow settings on big images, it's likely to timeout. if (pic.width * pic.height > 32 * 32) { if (config.lossless) { @@ -93,7 +73,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPPictureFree(&pic); if (error_code == VP8_ENC_ERROR_OUT_OF_MEMORY || error_code == VP8_ENC_ERROR_BAD_WRITE) { - return 0; + return; } fprintf(stderr, "WebPEncode failed. Error code: %d\n", error_code); abort(); @@ -157,5 +137,16 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPFreeDecBuffer(&dec_config.output); WebPMemoryWriterClear(&memory_writer); WebPPictureFree(&pic); - return 0; } + +} // namespace + +FUZZ_TEST(EncDec, EncDecTest) + .WithDomains(/*use_argb=*/fuzztest::Arbitrary(), + /*source_image_index=*/ + fuzztest::InRange(0, fuzz_utils::kNumSourceImages - 1), + fuzz_utils::ArbitraryWebPConfig(), + /*optimization_index=*/ + fuzztest::InRange(0, + fuzz_utils::kMaxOptimizationIndex), + fuzz_utils::ArbitraryCropOrScaleParams()); diff --git a/tests/fuzzer/fuzz_utils.cc b/tests/fuzzer/fuzz_utils.cc new file mode 100644 index 00000000..72fd2fee --- /dev/null +++ b/tests/fuzzer/fuzz_utils.cc @@ -0,0 +1,201 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +#include "./fuzz_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "./img_alpha.h" +#include "./img_grid.h" +#include "./img_peak.h" +#include "src/dsp/cpu.h" +#include "src/webp/decode.h" +#include "src/webp/encode.h" +#include "src/webp/types.h" + +namespace fuzz_utils { + +WebPPicture GetSourcePicture(int image_index, bool use_argb) { + WebPPicture pic; + if (!WebPPictureInit(&pic)) abort(); + pic.use_argb = use_argb; + + // Pick a source picture. + const int kImagesWidth[] = {kImgAlphaWidth, kImgGridWidth, kImgPeakWidth}; + const int kImagesHeight[] = {kImgAlphaHeight, kImgGridHeight, kImgPeakHeight}; + const uint8_t* const image_data = kImagesData[image_index]; + pic.width = kImagesWidth[image_index]; + pic.height = kImagesHeight[image_index]; + pic.argb_stride = pic.width * 4 * sizeof(uint8_t); + + // Read the bytes. + if (!WebPPictureImportRGBA(&pic, image_data, pic.argb_stride)) abort(); + return pic; +} + +//------------------------------------------------------------------------------ + +int CropOrScale(WebPPicture* const pic, const CropOrScaleParams& params) { + if (pic == NULL) return 0; +#if !defined(WEBP_REDUCE_SIZE) + if (params.alter_input) { + if (params.crop_or_scale) { + const int cropped_width = std::max(1, pic->width / params.width_ratio); + const int cropped_height = std::max(1, pic->height / params.height_ratio); + const int cropped_left = (pic->width - cropped_width) / params.left_ratio; + const int cropped_top = (pic->height - cropped_height) / params.top_ratio; + return WebPPictureCrop(pic, cropped_left, cropped_top, cropped_width, + cropped_height); + } else { + const int scaled_width = 1 + (pic->width * params.width_ratio) / 8; + const int scaled_height = 1 + (pic->height * params.height_ratio) / 8; + return WebPPictureRescale(pic, scaled_width, scaled_height); + } + } +#else // defined(WEBP_REDUCE_SIZE) + (void)data; + (void)size; + (void)bit_pos; +#endif // !defined(WEBP_REDUCE_SIZE) + return 1; +} + +extern "C" VP8CPUInfo VP8GetCPUInfo; +static VP8CPUInfo GetCPUInfo; + +static WEBP_INLINE int GetCPUInfoNoSSE41(CPUFeature feature) { + if (feature == kSSE4_1 || feature == kAVX) return 0; + return GetCPUInfo(feature); +} + +static WEBP_INLINE int GetCPUInfoNoAVX(CPUFeature feature) { + if (feature == kAVX) return 0; + return GetCPUInfo(feature); +} + +static WEBP_INLINE int GetCPUInfoForceSlowSSSE3(CPUFeature feature) { + if (feature == kSlowSSSE3 && GetCPUInfo(kSSE3)) { + return 1; // we have SSE3 -> force SlowSSSE3 + } + return GetCPUInfo(feature); +} + +static WEBP_INLINE int GetCPUInfoOnlyC(CPUFeature feature) { + (void)feature; + return 0; +} + +void SetOptimization(VP8CPUInfo default_VP8GetCPUInfo, uint32_t index) { + assert(index <= kMaxOptimizationIndex); + GetCPUInfo = default_VP8GetCPUInfo; + const VP8CPUInfo kVP8CPUInfos[kMaxOptimizationIndex + 1] = { + GetCPUInfoOnlyC, GetCPUInfoForceSlowSSSE3, GetCPUInfoNoSSE41, + GetCPUInfoNoAVX, GetCPUInfo}; + VP8GetCPUInfo = kVP8CPUInfos[index]; +} + +//------------------------------------------------------------------------------ + +std::vector ReadFilesFromDirectory(std::string_view dir) { + std::vector> tuples = + fuzztest::ReadFilesFromDirectory(dir); + std::vector strings(tuples.size()); + for (size_t i = 0; i < tuples.size(); ++i) { + using std::swap; + swap(std::get<0>(tuples[i]), strings[i]); + } + return strings; +} + +//------------------------------------------------------------------------------ +// The code in this section is copied from +// https://github.com/webmproject/sjpeg/blob/ +// 1c025b3dbc2246de3e1d7c287970f1a01291800f/src/jpeg_tools.cc#L47 +// (same license as this file). + +namespace { +// Constants below are marker codes defined in JPEG spec +// ISO/IEC 10918-1 : 1993(E) Table B.1 +// See also: http://www.w3.org/Graphics/JPEG/itu-t81.pdf + +#define M_SOF0 0xffc0 +#define M_SOF1 0xffc1 + +const uint8_t* GetSOFData(const uint8_t* src, int size) { + if (src == NULL) return NULL; + const uint8_t* const end = src + size - 8; // 8 bytes of safety, for marker + src += 2; // skip M_SOI + for (; src < end && *src != 0xff; ++src) { /* search first 0xff marker */ + } + while (src < end) { + const uint32_t marker = static_cast((src[0] << 8) | src[1]); + if (marker == M_SOF0 || marker == M_SOF1) return src; + const size_t s = 2 + ((src[2] << 8) | src[3]); + src += s; + } + return NULL; // No SOF marker found +} + +bool SjpegDimensions(const uint8_t* src0, size_t size, int* width, int* height, + int* is_yuv420) { + if (width == NULL || height == NULL) return false; + const uint8_t* src = GetSOFData(src0, size); + const size_t left_over = size - (src - src0); + if (src == NULL || left_over < 8 + 3 * 1) return false; + if (height != NULL) *height = (src[5] << 8) | src[6]; + if (width != NULL) *width = (src[7] << 8) | src[8]; + if (is_yuv420 != NULL) { + const size_t nb_comps = src[9]; + *is_yuv420 = (nb_comps == 3); + if (left_over < 11 + 3 * nb_comps) return false; + for (int c = 0; *is_yuv420 && c < 3; ++c) { + const int expected_dim = (c == 0 ? 0x22 : 0x11); + *is_yuv420 &= (src[11 + c * 3] == expected_dim); + } + } + return true; +} +} // namespace + +//------------------------------------------------------------------------------ + +bool IsImageTooBig(const uint8_t* data, size_t size) { + int width, height, components; + if (SjpegDimensions(data, size, &width, &height, &components) || + WebPGetInfo(data, size, &width, &height)) { + // Look at the number of 8x8px blocks rather than the overall pixel count + // when comparing to memory and duration thresholds. + const size_t ceiled_width = ((size_t)width + 7) / 8 * 8; + const size_t ceiled_height = ((size_t)height + 7) / 8 * 8; + // Threshold to avoid out-of-memory and timeout issues. + // The threshold is arbitrary but below the fuzzer limit of 2 GB. + // The value cannot be 2 GB because of the added memory by MSAN. + if (ceiled_width * ceiled_height > kFuzzPxLimit) return true; + } + return false; +} + +} // namespace fuzz_utils diff --git a/tests/fuzzer/fuzz_utils.h b/tests/fuzzer/fuzz_utils.h index c3fc366d..1d92a077 100644 --- a/tests/fuzzer/fuzz_utils.h +++ b/tests/fuzzer/fuzz_utils.h @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. +// Copyright 2018-2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,14 +17,23 @@ #ifndef WEBP_TESTS_FUZZER_FUZZ_UTILS_H_ #define WEBP_TESTS_FUZZER_FUZZ_UTILS_H_ -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include "./img_alpha.h" #include "./img_grid.h" #include "./img_peak.h" -#include "src/dsp/dsp.h" +#include "src/dsp/cpu.h" #include "src/webp/encode.h" +#include "fuzztest/fuzztest.h" + +namespace fuzz_utils { //------------------------------------------------------------------------------ // Arbitrary limits to prevent OOM, timeout, or slow execution. @@ -54,170 +63,139 @@ static WEBP_INLINE uint8_t FuzzHash(const uint8_t* const data, size_t size) { return value; } -//------------------------------------------------------------------------------ -// Extract an integer in [0, max_value]. - -static WEBP_INLINE uint32_t Extract(uint32_t max_value, - const uint8_t data[], size_t size, - uint32_t* const bit_pos) { - uint32_t v = 0; - uint32_t range = 1; - while (*bit_pos < 8 * size && range <= max_value) { - const uint8_t mask = 1u << (*bit_pos & 7); - v = (v << 1) | !!(data[*bit_pos >> 3] & mask); - range <<= 1; - ++*bit_pos; - } - return v % (max_value + 1); -} - -//------------------------------------------------------------------------------ -// Some functions to override VP8GetCPUInfo and disable some optimizations. - #ifdef __cplusplus extern "C" VP8CPUInfo VP8GetCPUInfo; #else extern VP8CPUInfo VP8GetCPUInfo; #endif -static VP8CPUInfo GetCPUInfo; - -static WEBP_INLINE int GetCPUInfoNoSSE41(CPUFeature feature) { - if (feature == kSSE4_1 || feature == kAVX) return 0; - return GetCPUInfo(feature); -} - -static WEBP_INLINE int GetCPUInfoNoAVX(CPUFeature feature) { - if (feature == kAVX) return 0; - return GetCPUInfo(feature); -} - -static WEBP_INLINE int GetCPUInfoForceSlowSSSE3(CPUFeature feature) { - if (feature == kSlowSSSE3 && GetCPUInfo(kSSE3)) { - return 1; // we have SSE3 -> force SlowSSSE3 - } - return GetCPUInfo(feature); -} - -static WEBP_INLINE int GetCPUInfoOnlyC(CPUFeature feature) { - (void)feature; - return 0; -} - -static WEBP_INLINE void ExtractAndDisableOptimizations( - VP8CPUInfo default_VP8GetCPUInfo, const uint8_t data[], size_t size, - uint32_t* const bit_pos) { - GetCPUInfo = default_VP8GetCPUInfo; - const VP8CPUInfo kVP8CPUInfos[5] = {GetCPUInfoOnlyC, GetCPUInfoForceSlowSSSE3, - GetCPUInfoNoSSE41, GetCPUInfoNoAVX, - GetCPUInfo}; - int VP8GetCPUInfo_index = Extract(4, data, size, bit_pos); - VP8GetCPUInfo = kVP8CPUInfos[VP8GetCPUInfo_index]; -} //------------------------------------------------------------------------------ -static WEBP_INLINE int ExtractWebPConfig(WebPConfig* const config, - const uint8_t data[], size_t size, - uint32_t* const bit_pos) { - if (config == NULL || !WebPConfigInit(config)) return 0; - config->lossless = Extract(1, data, size, bit_pos); - config->quality = Extract(100, data, size, bit_pos); - config->method = Extract(6, data, size, bit_pos); - config->image_hint = - (WebPImageHint)Extract(WEBP_HINT_LAST - 1, data, size, bit_pos); - config->segments = 1 + Extract(3, data, size, bit_pos); - config->sns_strength = Extract(100, data, size, bit_pos); - config->filter_strength = Extract(100, data, size, bit_pos); - config->filter_sharpness = Extract(7, data, size, bit_pos); - config->filter_type = Extract(1, data, size, bit_pos); - config->autofilter = Extract(1, data, size, bit_pos); - config->alpha_compression = Extract(1, data, size, bit_pos); - config->alpha_filtering = Extract(2, data, size, bit_pos); - config->alpha_quality = Extract(100, data, size, bit_pos); - config->pass = 1 + Extract(9, data, size, bit_pos); - config->show_compressed = 1; - config->preprocessing = Extract(2, data, size, bit_pos); - config->partitions = Extract(3, data, size, bit_pos); - config->partition_limit = 10 * Extract(10, data, size, bit_pos); - config->emulate_jpeg_size = Extract(1, data, size, bit_pos); - config->thread_level = Extract(1, data, size, bit_pos); - config->low_memory = Extract(1, data, size, bit_pos); - config->near_lossless = 20 * Extract(5, data, size, bit_pos); - config->exact = Extract(1, data, size, bit_pos); - config->use_delta_palette = Extract(1, data, size, bit_pos); - config->use_sharp_yuv = Extract(1, data, size, bit_pos); - return WebPValidateConfig(config); +constexpr const uint8_t* kImagesData[] = {kImgAlphaData, kImgGridData, + kImgPeakData}; +constexpr size_t kNumSourceImages = + sizeof(kImagesData) / sizeof(kImagesData[0]); + +WebPPicture GetSourcePicture(int image_index, bool use_argb); + +static inline auto ArbitraryWebPConfig() { + return fuzztest::Map( + [](int lossless, int quality, int method, int image_hint, int segments, + int sns_strength, int filter_strength, int filter_sharpness, + int filter_type, int autofilter, int alpha_compression, + int alpha_filtering, int alpha_quality, int pass, int preprocessing, + int partitions, int partition_limit, int emulate_jpeg_size, + int thread_level, int low_memory, int near_lossless, int exact, + int use_delta_palette, int use_sharp_yuv) -> WebPConfig { + WebPConfig config; + if (!WebPConfigInit(&config)) abort(); + config.lossless = lossless; + config.quality = quality; + config.method = method; + config.image_hint = (WebPImageHint)image_hint; + config.segments = segments; + config.sns_strength = sns_strength; + config.filter_strength = filter_strength; + config.filter_sharpness = filter_sharpness; + config.filter_type = filter_type; + config.autofilter = autofilter; + config.alpha_compression = alpha_compression; + config.alpha_filtering = alpha_filtering; + config.alpha_quality = alpha_quality; + config.pass = pass; + config.show_compressed = 1; + config.preprocessing = preprocessing; + config.partitions = partitions; + config.partition_limit = 10 * partition_limit; + config.emulate_jpeg_size = emulate_jpeg_size; + config.thread_level = thread_level; + config.low_memory = low_memory; + config.near_lossless = 20 * near_lossless; + config.exact = exact; + config.use_delta_palette = use_delta_palette; + config.use_sharp_yuv = use_sharp_yuv; + if (!WebPValidateConfig(&config)) abort(); + return config; + }, + /*lossless=*/fuzztest::InRange(0, 1), + /*quality=*/fuzztest::InRange(0, 100), + /*method=*/fuzztest::InRange(0, 6), + /*image_hint=*/fuzztest::InRange(0, WEBP_HINT_LAST - 1), + /*segments=*/fuzztest::InRange(1, 4), + /*sns_strength=*/fuzztest::InRange(0, 100), + /*filter_strength=*/fuzztest::InRange(0, 100), + /*filter_sharpness=*/fuzztest::InRange(0, 7), + /*filter_type=*/fuzztest::InRange(0, 1), + /*autofilter=*/fuzztest::InRange(0, 1), + /*alpha_compression=*/fuzztest::InRange(0, 1), + /*alpha_filtering=*/fuzztest::InRange(0, 2), + /*alpha_quality=*/fuzztest::InRange(0, 100), + /*pass=*/fuzztest::InRange(1, 10), + /*preprocessing=*/fuzztest::InRange(0, 2), + /*partitions=*/fuzztest::InRange(0, 3), + /*partition_limit=*/fuzztest::InRange(0, 10), + /*emulate_jpeg_size=*/fuzztest::InRange(0, 1), + /*thread_level=*/fuzztest::InRange(0, 1), + /*low_memory=*/fuzztest::InRange(0, 1), + /*near_lossless=*/fuzztest::InRange(0, 5), + /*exact=*/fuzztest::InRange(0, 1), + /*use_delta_palette=*/fuzztest::InRange(0, 1), + /*use_sharp_yuv=*/fuzztest::InRange(0, 1)); } +struct CropOrScaleParams { + bool alter_input; + bool crop_or_scale; + int width_ratio; + int height_ratio; + int left_ratio; + int top_ratio; +}; + +static inline auto ArbitraryCropOrScaleParams() { + return fuzztest::Map( + [](const std::optional>& width_height_ratio, + const std::optional>& left_top_ratio) + -> CropOrScaleParams { + CropOrScaleParams params; + params.alter_input = width_height_ratio.has_value(); + if (params.alter_input) { + params.width_ratio = width_height_ratio->first; + params.height_ratio = width_height_ratio->second; + params.crop_or_scale = left_top_ratio.has_value(); + if (params.crop_or_scale) { + params.left_ratio = left_top_ratio->first; + params.top_ratio = left_top_ratio->second; + } + } + return params; + }, + fuzztest::OptionalOf( + fuzztest::PairOf(fuzztest::InRange(1, 8), fuzztest::InRange(1, 8))), + fuzztest::OptionalOf( + fuzztest::PairOf(fuzztest::InRange(1, 8), fuzztest::InRange(1, 8)))); +} + +// Crops or scales a picture according to the given params. +int CropOrScale(WebPPicture* pic, const CropOrScaleParams& params); + +// Imposes a level of optimization among one of the kMaxOptimizationIndex+1 +// possible values: OnlyC, ForceSlowSSSE3, NoSSE41, NoAVX, default. +static constexpr uint32_t kMaxOptimizationIndex = 4; +void SetOptimization(VP8CPUInfo default_VP8GetCPUInfo, uint32_t index); + //------------------------------------------------------------------------------ -static WEBP_INLINE int ExtractSourcePicture(WebPPicture* const pic, - const uint8_t data[], size_t size, - uint32_t* const bit_pos) { - if (pic == NULL) return 0; +// See https://developers.google.com/speed/webp/docs/riff_container. +static constexpr size_t kMaxWebPFileSize = (1ull << 32) - 2; // 4 GiB - 2 - // Pick a source picture. - const uint8_t* kImagesData[] = { - kImgAlphaData, - kImgGridData, - kImgPeakData - }; - const int kImagesWidth[] = { - kImgAlphaWidth, - kImgGridWidth, - kImgPeakWidth - }; - const int kImagesHeight[] = { - kImgAlphaHeight, - kImgGridHeight, - kImgPeakHeight - }; - const size_t kNbImages = sizeof(kImagesData) / sizeof(kImagesData[0]); - const size_t image_index = Extract(kNbImages - 1, data, size, bit_pos); - const uint8_t* const image_data = kImagesData[image_index]; - pic->width = kImagesWidth[image_index]; - pic->height = kImagesHeight[image_index]; - pic->argb_stride = pic->width * 4 * sizeof(uint8_t); +std::vector GetDictionaryFromFiles( + const std::vector& file_paths); - // Read the bytes. - return WebPPictureImportRGBA(pic, image_data, pic->argb_stride); -} +// Checks whether the binary blob containing a JPEG or WebP is too big for the +// fuzzer. +bool IsImageTooBig(const uint8_t* data, size_t size); -//------------------------------------------------------------------------------ - -static WEBP_INLINE int Max(int a, int b) { return ((a < b) ? b : a); } - -static WEBP_INLINE int ExtractAndCropOrScale(WebPPicture* const pic, - const uint8_t data[], size_t size, - uint32_t* const bit_pos) { - if (pic == NULL) return 0; -#if !defined(WEBP_REDUCE_SIZE) - const int alter_input = Extract(1, data, size, bit_pos); - const int crop_or_scale = Extract(1, data, size, bit_pos); - const int width_ratio = 1 + Extract(7, data, size, bit_pos); - const int height_ratio = 1 + Extract(7, data, size, bit_pos); - if (alter_input) { - if (crop_or_scale) { - const uint32_t left_ratio = 1 + Extract(7, data, size, bit_pos); - const uint32_t top_ratio = 1 + Extract(7, data, size, bit_pos); - const int cropped_width = Max(1, pic->width / width_ratio); - const int cropped_height = Max(1, pic->height / height_ratio); - const int cropped_left = (pic->width - cropped_width) / left_ratio; - const int cropped_top = (pic->height - cropped_height) / top_ratio; - return WebPPictureCrop(pic, cropped_left, cropped_top, cropped_width, - cropped_height); - } else { - const int scaled_width = 1 + (pic->width * width_ratio) / 8; - const int scaled_height = 1 + (pic->height * height_ratio) / 8; - return WebPPictureRescale(pic, scaled_width, scaled_height); - } - } -#else // defined(WEBP_REDUCE_SIZE) - (void)data; - (void)size; - (void)bit_pos; -#endif // !defined(WEBP_REDUCE_SIZE) - return 1; -} +} // namespace fuzz_utils #endif // WEBP_TESTS_FUZZER_FUZZ_UTILS_H_ diff --git a/tests/fuzzer/huffman_fuzzer.c b/tests/fuzzer/huffman_fuzzer.cc similarity index 75% rename from tests/fuzzer/huffman_fuzzer.c rename to tests/fuzzer/huffman_fuzzer.cc index 03e1fdc4..6d0fbe6d 100644 --- a/tests/fuzzer/huffman_fuzzer.c +++ b/tests/fuzzer/huffman_fuzzer.cc @@ -14,22 +14,29 @@ // //////////////////////////////////////////////////////////////////////////////// -#include -#include +#include +#include +#include +#include "./fuzz_utils.h" #include "src/dec/vp8li_dec.h" #include "src/utils/bit_reader_utils.h" #include "src/utils/huffman_utils.h" #include "src/utils/utils.h" #include "src/webp/format_constants.h" -int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { +namespace { + +void HuffmanTest(std::string_view blob) { + const uint8_t* const data = reinterpret_cast(blob.data()); + const size_t size = blob.size(); + // Number of bits to initialize data. static const int kColorCacheBitsBits = 4; // 'num_htree_groups' is contained in the RG channel, hence 16 bits. static const int kNumHtreeGroupsBits = 16; if (size * sizeof(*data) < kColorCacheBitsBits + kNumHtreeGroupsBits) { - return 0; + return; } // A non-NULL mapping brings minor changes that are tested by the normal @@ -39,27 +46,32 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { memset(&huffman_tables, 0, sizeof(huffman_tables)); HTreeGroup* htree_groups = NULL; + int num_htree_groups, num_htree_groups_max, color_cache_bits; + VP8LBitReader* br; VP8LDecoder* dec = VP8LNew(); if (dec == NULL) goto Error; - VP8LBitReader* const br = &dec->br_; + br = &dec->br_; VP8LInitBitReader(br, data, size); - const int color_cache_bits = VP8LReadBits(br, kColorCacheBitsBits); + color_cache_bits = VP8LReadBits(br, kColorCacheBitsBits); if (color_cache_bits < 1 || color_cache_bits > MAX_CACHE_BITS) goto Error; - const int num_htree_groups = VP8LReadBits(br, kNumHtreeGroupsBits); + num_htree_groups = VP8LReadBits(br, kNumHtreeGroupsBits); // 'num_htree_groups' cannot be 0 as it is built from a non-empty image. - if (num_htree_groups == 0) goto Error; + if (num_htree_groups == 0) return; // This variable is only useful when mapping is not NULL. - const int num_htree_groups_max = num_htree_groups; + num_htree_groups_max = num_htree_groups; (void)ReadHuffmanCodesHelper(color_cache_bits, num_htree_groups, num_htree_groups_max, mapping, dec, &huffman_tables, &htree_groups); - Error: +Error: WebPSafeFree(mapping); VP8LHtreeGroupsFree(htree_groups); VP8LHuffmanTablesDeallocate(&huffman_tables); VP8LDelete(dec); - return 0; } + +} // namespace + +FUZZ_TEST(Huffman, HuffmanTest).WithDomains(fuzztest::String()); diff --git a/tests/fuzzer/makefile.unix b/tests/fuzzer/makefile.unix deleted file mode 100644 index 3a3aff0a..00000000 --- a/tests/fuzzer/makefile.unix +++ /dev/null @@ -1,31 +0,0 @@ -# This Makefile will compile all fuzzing targets. It doesn't check tool -# requirements and paths may need to be updated depending on your environment. -# Note a clang 6+ toolchain is assumed for use of -fsanitize=fuzzer. - -CC = clang -CXX = clang++ -CFLAGS = -fsanitize=fuzzer -I../../src -I../.. -Wall -Wextra -CXXFLAGS = $(CFLAGS) -LDFLAGS = -fsanitize=fuzzer -LDLIBS = ../../src/mux/libwebpmux.a ../../src/demux/libwebpdemux.a -LDLIBS += ../../src/libwebp.a ../../imageio/libimageio_util.a -LDLIBS += ../../sharpyuv/libsharpyuv.a - -FUZZERS = advanced_api_fuzzer animation_api_fuzzer animdecoder_fuzzer -FUZZERS += animencoder_fuzzer enc_dec_fuzzer huffman_fuzzer -FUZZERS += mux_demux_api_fuzzer simple_api_fuzzer - -%.o: fuzz_utils.h img_alpha.h img_grid.h img_peak.h -all: $(FUZZERS) - -define FUZZER_template -$(1): $$(addsuffix .o, $(1)) $(LDLIBS) -OBJS += $$(addsuffix .o, $(1)) -endef - -$(foreach fuzzer, $(FUZZERS), $(eval $(call FUZZER_template, $(fuzzer)))) - -clean: - $(RM) $(FUZZERS) $(OBJS) - -.PHONY: all clean diff --git a/tests/fuzzer/mux_demux_api_fuzzer.c b/tests/fuzzer/mux_demux_api_fuzzer.cc similarity index 79% rename from tests/fuzzer/mux_demux_api_fuzzer.c rename to tests/fuzzer/mux_demux_api_fuzzer.cc index f5983e8d..4d853518 100644 --- a/tests/fuzzer/mux_demux_api_fuzzer.c +++ b/tests/fuzzer/mux_demux_api_fuzzer.cc @@ -14,23 +14,30 @@ // //////////////////////////////////////////////////////////////////////////////// +#include +#include +#include + #include "./fuzz_utils.h" #include "src/webp/demux.h" #include "src/webp/mux.h" -int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { +namespace { + +void MuxDemuxApiTest(std::string_view data_in, bool mux) { + const size_t size = data_in.size(); WebPData webp_data; WebPDataInit(&webp_data); webp_data.size = size; - webp_data.bytes = data; + webp_data.bytes = reinterpret_cast(data_in.data()); // Extracted chunks and frames are not processed or decoded, // which is already covered extensively by the other fuzz targets. - if (size & 1) { + if (mux) { // Mux API WebPMux* mux = WebPMuxCreate(&webp_data, size & 2); - if (!mux) return 0; + if (!mux) return; WebPData chunk; (void)WebPMuxGetChunk(mux, "EXIF", &chunk); @@ -45,7 +52,7 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPMuxError status; WebPMuxFrameInfo info; - for (int i = 0; i < kFuzzFrameLimit; i++) { + for (int i = 0; i < fuzz_utils::kFuzzFrameLimit; i++) { status = WebPMuxGetFrame(mux, i + 1, &info); if (status == WEBP_MUX_NOT_FOUND) { break; @@ -63,11 +70,11 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { demux = WebPDemuxPartial(&webp_data, &state); if (state < WEBP_DEMUX_PARSED_HEADER) { WebPDemuxDelete(demux); - return 0; + return; } } else { demux = WebPDemux(&webp_data); - if (!demux) return 0; + if (!demux) return; } WebPChunkIterator chunk_iter; @@ -83,7 +90,7 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPIterator iter; if (WebPDemuxGetFrame(demux, 1, &iter)) { - for (int i = 1; i < kFuzzFrameLimit; i++) { + for (int i = 1; i < fuzz_utils::kFuzzFrameLimit; i++) { if (!WebPDemuxNextFrame(&iter)) break; } } @@ -91,6 +98,12 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { WebPDemuxReleaseIterator(&iter); WebPDemuxDelete(demux); } - - return 0; } + +} // namespace + +FUZZ_TEST(MuxDemuxApi, MuxDemuxApiTest) + .WithDomains( + fuzztest::String() + .WithMaxSize(fuzz_utils::kMaxWebPFileSize + 1), + /*mux=*/fuzztest::Arbitrary()); diff --git a/tests/fuzzer/oss-fuzz/build.sh b/tests/fuzzer/oss-fuzz/build.sh index f2931e8b..e8c34eca 100644 --- a/tests/fuzzer/oss-fuzz/build.sh +++ b/tests/fuzzer/oss-fuzz/build.sh @@ -15,61 +15,69 @@ # ################################################################################ +# This script is meant to be run by the oss-fuzz infrastructure from the script +# https://github.com/google/oss-fuzz/blob/master/projects/libwebp/build.sh +# It builds the different fuzz targets. + +# To test changes to this file: +# - make changes and commit to your REPO +# - run: +# git clone --depth=1 git@github.com:google/oss-fuzz.git +# cd oss-fuzz +# - modify projects/libwebp/Dockerfile to point to your REPO +# - run: +# python3 infra/helper.py build_image libwebp +# # enter 'y' and wait for everything to be downloaded +# - run: +# python3 infra/helper.py build_fuzzers --sanitizer address libwebp +# # wait for the tests to be built +# And then run the fuzzer locally, for example: +# python3 infra/helper.py run_fuzzer libwebp \ +# --sanitizer address \ +# animencoder_fuzzer__AnimEncoder.AnimEncoderTest + set -eu # limit allocation size to reduce spurious OOMs WEBP_CFLAGS="$CFLAGS -DWEBP_MAX_IMAGE_SIZE=838860800" # 800MiB -./autogen.sh -CFLAGS="$WEBP_CFLAGS" ./configure \ - --enable-asserts \ - --enable-libwebpdemux \ - --enable-libwebpmux \ - --disable-shared \ - --disable-jpeg \ - --disable-tiff \ - --disable-gif \ - --disable-wic -make clean -make -j$(nproc) +export CFLAGS="$WEBP_CFLAGS" +cmake -S . -B build -DWEBP_BUILD_FUZZTEST=ON +cd build && make -j$(nproc) && cd .. find $SRC/libwebp-test-data -type f -size -32k -iname "*.webp" \ -exec zip -qju fuzz_seed_corpus.zip "{}" \; -webp_libs=( - src/demux/.libs/libwebpdemux.a - src/mux/.libs/libwebpmux.a - src/.libs/libwebp.a - imageio/.libs/libimageio_util.a - sharpyuv/.libs/libsharpyuv.a -) -webp_c_fuzzers=( - advanced_api_fuzzer - animation_api_fuzzer - huffman_fuzzer - mux_demux_api_fuzzer - simple_api_fuzzer -) -webp_cxx_fuzzers=( - animdecoder_fuzzer - animencoder_fuzzer - enc_dec_fuzzer -) - -for fuzzer in "${webp_c_fuzzers[@]}"; do - $CC $CFLAGS -Isrc -I. tests/fuzzer/${fuzzer}.c -c -o tests/fuzzer/${fuzzer}.o - $CXX $CXXFLAGS $LIB_FUZZING_ENGINE \ - tests/fuzzer/${fuzzer}.o -o $OUT/${fuzzer} \ - "${webp_libs[@]}" -done - -for fuzzer in "${webp_cxx_fuzzers[@]}"; do - $CXX $CXXFLAGS -Isrc -I. $LIB_FUZZING_ENGINE \ - tests/fuzzer/${fuzzer}.cc -o $OUT/${fuzzer} \ - "${webp_libs[@]}" -done - -for fuzzer in "${webp_c_fuzzers[@]}" "${webp_cxx_fuzzers[@]}"; do - cp fuzz_seed_corpus.zip $OUT/${fuzzer}_seed_corpus.zip - cp tests/fuzzer/fuzz.dict $OUT/${fuzzer}.dict -done +# Restrict fuzztest tests to the only compatible fuzz engine: libfuzzer. +if [[ "$FUZZING_ENGINE" == "libfuzzer" ]]; then + # build fuzztests + # The following is taken from https://github.com/google/oss-fuzz/blob/31ac7244748ea7390015455fb034b1f4eda039d9/infra/base-images/base-builder/compile_fuzztests.sh#L59 + # Iterate the fuzz binaries and list each fuzz entrypoint in the binary. For + # each entrypoint create a wrapper script that calls into the binaries the + # given entrypoint as argument. + # The scripts will be named: + # {binary_name}@{fuzztest_entrypoint} + FUZZ_TEST_BINARIES_OUT_PATHS=$(find ./build/tests/fuzzer/ -executable -type f) + echo "Fuzz binaries: $FUZZ_TEST_BINARIES_OUT_PATHS" + for fuzz_main_file in $FUZZ_TEST_BINARIES_OUT_PATHS; do + FUZZ_TESTS=$($fuzz_main_file --list_fuzz_tests | cut -d ' ' -f 4) + cp -f ${fuzz_main_file} $OUT/ + fuzz_basename=$(basename $fuzz_main_file) + chmod -x $OUT/$fuzz_basename + for fuzz_entrypoint in $FUZZ_TESTS; do + TARGET_FUZZER="${fuzz_basename}@$fuzz_entrypoint" + # Write executer script + echo "#!/bin/sh +# LLVMFuzzerTestOneInput for fuzzer detection. +this_dir=\$(dirname \"\$0\") +export TEST_DATA_DIRS=\$this_dir/corpus +chmod +x \$this_dir/$fuzz_basename +\$this_dir/$fuzz_basename --fuzz=$fuzz_entrypoint -- \$@ +chmod -x \$this_dir/$fuzz_basename" > $OUT/$TARGET_FUZZER + chmod +x $OUT/$TARGET_FUZZER + done + # Copy data. + cp fuzz_seed_corpus.zip $OUT/${fuzz_basename}_seed_corpus.zip + cp tests/fuzzer/fuzz.dict $OUT/${fuzz_basename}.dict + done +fi diff --git a/tests/fuzzer/patch.sh b/tests/fuzzer/patch.sh new file mode 100755 index 00000000..7034d10f --- /dev/null +++ b/tests/fuzzer/patch.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Fixes for https://github.com/google/fuzztest/issues/1124 +sed -i -e "s/-fsanitize=address//g" -e "s/-DADDRESS_SANITIZER//g" \ + ./cmake/FuzzTestFlagSetup.cmake +# Fixes for https://github.com/google/fuzztest/issues/1125 +before="if (IsEnginePlaceholderInput(data)) return;" +after="if (data.size() == 0) return;" +sed -i "s/${before}/${after}/" ./fuzztest/internal/compatibility_mode.cc +sed -i "s/set(GTEST_HAS_ABSL ON)/set(GTEST_HAS_ABSL OFF)/" \ + ./cmake/BuildDependencies.cmake diff --git a/tests/fuzzer/simple_api_fuzzer.c b/tests/fuzzer/simple_api_fuzzer.cc similarity index 84% rename from tests/fuzzer/simple_api_fuzzer.c rename to tests/fuzzer/simple_api_fuzzer.cc index 3a4288a4..3d1b5c3b 100644 --- a/tests/fuzzer/simple_api_fuzzer.c +++ b/tests/fuzzer/simple_api_fuzzer.cc @@ -14,15 +14,23 @@ // //////////////////////////////////////////////////////////////////////////////// +#include +#include +#include + #include "./fuzz_utils.h" #include "src/webp/decode.h" -int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { - int w, h; - if (!WebPGetInfo(data, size, &w, &h)) return 0; - if ((size_t)w * h > kFuzzPxLimit) return 0; +namespace { - const uint8_t value = FuzzHash(data, size); +void SimpleApiTest(std::string_view data_in) { + const uint8_t* const data = reinterpret_cast(data_in.data()); + const size_t size = data_in.size(); + int w, h; + if (!WebPGetInfo(data, size, &w, &h)) return; + if ((size_t)w * h > fuzz_utils::kFuzzPxLimit) return; + + const uint8_t value = fuzz_utils::FuzzHash(data, size); uint8_t* buf = NULL; // For *Into functions, which decode into an external buffer, an @@ -84,6 +92,11 @@ int LLVMFuzzerTestOneInput(const uint8_t* const data, size_t size) { } if (buf) WebPFree(buf); - - return 0; } + +} // namespace + +FUZZ_TEST(SimpleApi, SimpleApiTest) + .WithDomains( + fuzztest::String() + .WithMaxSize(fuzz_utils::kMaxWebPFileSize + 1));