diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index cb00f98c..6a29ec22 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -9,3 +9,6 @@ endif() add_executable(ojph_expand_fuzz_target fuzz_targets/ojph_expand_fuzz_target.cpp) target_link_libraries(ojph_expand_fuzz_target PRIVATE openjph) + +add_executable(ojph_compress_fuzz_target fuzz_targets/ojph_compress_fuzz_target.cpp) +target_link_libraries(ojph_compress_fuzz_target PRIVATE openjph) diff --git a/fuzzing/fuzz_targets/ojph_compress_fuzz_target.cpp b/fuzzing/fuzz_targets/ojph_compress_fuzz_target.cpp new file mode 100644 index 00000000..8deefc56 --- /dev/null +++ b/fuzzing/fuzz_targets/ojph_compress_fuzz_target.cpp @@ -0,0 +1,131 @@ +//***************************************************************************/ +// This software is released under the 2-Clause BSD license, included +// below. +// +// Copyright (c) 2019, Aous Naman +// Copyright (c) 2019, Kakadu Software Pty Ltd, Australia +// Copyright (c) 2019, The University of New South Wales, Australia +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +//***************************************************************************/ +// This file is part of the OpenJPH software implementation. +// File: ojph_compress_fuzz_target.cpp +// Fuzz target for the HTJ2K encoding (compression) path. +//***************************************************************************/ + +#include +#include +#include + +#include "ojph_mem.h" +#include "ojph_file.h" +#include "ojph_codestream.h" +#include "ojph_params.h" + +// Input layout (4 control bytes + pixel data): +// byte 0: [6:0] width-1 (1..128) +// byte 1: [6:0] height-1 (1..128) +// byte 2: [1:0] num_components-1 (1..4) +// [3:2] bit_depth selector (8,10,12,16) +// [4] is_signed +// [5] reversible +// [6] color_transform +// byte 3: [2:0] num_decompositions (0..5, clamped) +// [3] planar +// bytes 4+: pixel data (each byte becomes one sample) + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + if (Size < 5) + return 0; + + ojph::ui32 width = (Data[0] & 0x7F) + 1; + ojph::ui32 height = (Data[1] & 0x7F) + 1; + ojph::ui32 num_comps = (Data[2] & 0x03) + 1; + ojph::ui32 bit_depth = (ojph::ui32[]){8, 10, 12, 16}[(Data[2] >> 2) & 0x03]; + bool is_signed = (Data[2] >> 4) & 1; + bool reversible = (Data[2] >> 5) & 1; + bool color_transform = (Data[2] >> 6) & 1; + ojph::ui32 num_decomps = Data[3] & 0x07; + bool planar = (Data[3] >> 3) & 1; + + if (num_decomps > 5) num_decomps = 5; + if (num_comps < 3) color_transform = false; + if (color_transform) planar = false; + + const uint8_t *pixels = Data + 4; + size_t pixels_len = Size - 4; + size_t pix_idx = 0; + + try + { + ojph::codestream cs; + + ojph::param_siz siz = cs.access_siz(); + siz.set_image_extent(ojph::point(width, height)); + siz.set_num_components(num_comps); + for (ojph::ui32 c = 0; c < num_comps; ++c) + siz.set_component(c, ojph::point(1, 1), bit_depth, is_signed); + + ojph::param_cod cod = cs.access_cod(); + cod.set_num_decomposition(num_decomps); + cod.set_color_transform(color_transform); + cod.set_reversible(reversible); + + if (!reversible) + cs.access_qcd().set_irrev_quant(0.0005f); + + cs.set_planar(planar); + + ojph::mem_outfile outfile; + outfile.open(); + cs.write_headers(&outfile); + + // Total rows to push: planar processes each component fully, + // interleaved processes one row from all components at a time. + ojph::ui32 total_rows = num_comps * height; + ojph::ui32 next_comp; + ojph::line_buf *line = cs.exchange(NULL, next_comp); + + for (ojph::ui32 r = 0; r < total_rows; ++r) + { + ojph::si32 *dp = line->i32; + for (ojph::ui32 x = 0; x < width; ++x) + { + // Use fuzz bytes as sample values, wrapping around as needed + ojph::si32 val = (ojph::si32)pixels[pix_idx % pixels_len]; + pix_idx++; + dp[x] = is_signed ? val - 128 : val; + } + line = cs.exchange(line, next_comp); + } + + cs.flush(); + cs.close(); + } + catch (const std::exception &) + { + } + return 0; +} diff --git a/fuzzing/fuzz_targets/ojph_expand_fuzz_target.cpp b/fuzzing/fuzz_targets/ojph_expand_fuzz_target.cpp index 1dae21d1..48514bff 100644 --- a/fuzzing/fuzz_targets/ojph_expand_fuzz_target.cpp +++ b/fuzzing/fuzz_targets/ojph_expand_fuzz_target.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -45,54 +46,147 @@ #include #include #include -#include extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + // The first 2 bytes are used to control decoder options: + // byte 0 bit 1: force planar mode + // byte 0 bit 2: force interleaved mode + // byte 1: number of resolutions to skip (0-7) + if (Size < 3) + return 0; + + uint8_t opts = Data[0]; + uint8_t skip_res = Data[1] & 0x07; + Data += 2; + Size -= 2; + + bool force_planar = (opts & 0x02) != 0; + bool force_interleaved = (opts & 0x04) != 0; + try { - ojph::mem_infile infile; infile.open(reinterpret_cast(Data), Size); ojph::codestream cs; + + // Always enable resilience: all fuzzer inputs are untrusted/mutated, + // so the decoder must use its error-recovery path. + cs.enable_resilience(); + cs.read_headers(&infile); + // Guard against inputs that cause excessive decoding work. + { + ojph::param_siz siz = cs.access_siz(); + ojph::point extent = siz.get_image_extent(); + ojph::point offset = siz.get_image_offset(); + ojph::ui64 w = extent.x - offset.x; + ojph::ui64 h = extent.y - offset.y; + if (w * h * siz.get_num_components() > 65536) + { + cs.close(); + return 0; + } + + ojph::param_cod cod = cs.access_cod(); + if (cod.get_num_decompositions() > 5) + { + cs.close(); + return 0; + } + + // Large precincts cause huge internal buffers and very expensive + // per-row wavelet transforms even for small images. + for (ojph::ui32 lev = 0; lev <= cod.get_num_decompositions(); ++lev) + { + ojph::size psiz = cod.get_precinct_size(lev); + if (psiz.w > 256 || psiz.h > 256) + { + cs.close(); + return 0; + } + } + } + + if (skip_res > 0) + cs.restrict_input_resolution(skip_res, skip_res); + + if (force_planar) + cs.set_planar(true); + else if (force_interleaved) + cs.set_planar(false); + cs.create(); - if (cs.is_planar()) + ojph::param_siz siz = cs.access_siz(); + + // Second guard: cap reconstructed dimensions after create(). { - ojph::param_siz siz = cs.access_siz(); + ojph::ui64 total_recon = 0; for (ojph::ui32 c = 0; c < siz.get_num_components(); ++c) + total_recon += (ojph::ui64)siz.get_recon_width(c) + * (ojph::ui64)siz.get_recon_height(c); + if (total_recon > 65536) + { + cs.close(); + return 0; + } + } + + // Time budget: abort if decoding takes too long. + struct timespec start_ts; + clock_gettime(CLOCK_MONOTONIC, &start_ts); + ojph::ui32 pull_count = 0; + const ojph::ui32 MAX_SECONDS = 10; + bool timed_out = false; + + if (cs.is_planar()) + { + for (ojph::ui32 c = 0; c < siz.get_num_components() && !timed_out; ++c) { ojph::ui32 height = siz.get_recon_height(c); - for (ojph::ui32 i = height; i > 0; --i) + for (ojph::ui32 i = height; i > 0 && !timed_out; --i) { ojph::ui32 comp_num; cs.pull(comp_num); - assert(comp_num == c); + if (++pull_count % 64 == 0) + { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if ((ojph::ui32)(now.tv_sec - start_ts.tv_sec) >= MAX_SECONDS) + timed_out = true; + } } } } else { - ojph::param_siz siz = cs.access_siz(); ojph::ui32 height = siz.get_recon_height(0); - for (ojph::ui32 i = 0; i < height; ++i) + for (ojph::ui32 i = 0; i < height && !timed_out; ++i) { for (ojph::ui32 c = 0; c < siz.get_num_components(); ++c) { ojph::ui32 comp_num; cs.pull(comp_num); - assert(comp_num == c); + if (++pull_count % 64 == 0) + { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if ((ojph::ui32)(now.tv_sec - start_ts.tv_sec) >= MAX_SECONDS) + timed_out = true; + } } } } + + cs.close(); } - catch (const std::exception &e) + catch (const std::exception &) { - std::cerr << e.what() << '\n'; } + return 0; } @@ -109,8 +203,11 @@ int main(int argc, char **argv) { return -1; } rewind(f); - std::vector buf(len); - size_t n = fread(buf.data(), 1, len, f); + // Prepend 2 control bytes (default: no skip) + std::vector buf(len + 2); + buf[0] = 0; + buf[1] = 0; + size_t n = fread(buf.data() + 2, 1, len, f); if(n != static_cast(len)) { return -1; }