Coverage Report

Created: 2026-02-14 07:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/tools/streaming_fuzzer.cc
Line
Count
Source
1
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2
//
3
// Use of this source code is governed by a BSD-style
4
// license that can be found in the LICENSE file.
5
6
#include <jxl/codestream_header.h>
7
#include <jxl/color_encoding.h>
8
#include <jxl/decode.h>
9
#include <jxl/decode_cxx.h>
10
#include <jxl/encode.h>
11
#include <jxl/encode_cxx.h>
12
#include <jxl/thread_parallel_runner.h>
13
#include <jxl/thread_parallel_runner_cxx.h>
14
#include <jxl/types.h>
15
16
#include <array>
17
#include <cstdint>
18
#include <cstdio>
19
#include <cstdlib>
20
#include <cstring>
21
#include <string>
22
#include <vector>
23
24
#include "lib/jxl/base/common.h"
25
#include "lib/jxl/base/compiler_specific.h"
26
#include "lib/jxl/base/status.h"
27
#include "lib/jxl/fuzztest.h"
28
#include "tools/tracking_memory_manager.h"
29
30
namespace {
31
32
using ::jpegxl::tools::kGiB;
33
using ::jpegxl::tools::TrackingMemoryManager;
34
using ::jxl::Status;
35
using ::jxl::StatusOr;
36
37
constexpr size_t kMemoryCap = kGiB;  // enough for 85.3MPx without overhead
38
constexpr size_t kBaseMaxSize = 16 << 20;  // 16MPx
39
40
51.9k
void CheckImpl(bool ok, const char* conndition, const char* file, int line) {
41
51.9k
  if (!ok) {
42
0
    fprintf(stderr, "Check(%s) failed at %s:%d\n", conndition, file, line);
43
0
    JXL_CRASH();
44
0
  }
45
51.9k
}
46
51.9k
#define Check(OK) CheckImpl((OK), #OK, __FILE__, __LINE__)
47
48
struct FuzzSpec {
49
  uint32_t xsize;
50
  uint32_t ysize;
51
  bool grayscale;
52
  bool alpha;
53
  uint8_t bit_depth;  // 1 - 16
54
55
  struct IntOptionSpec {
56
    JxlEncoderFrameSettingId flag;
57
    std::string name;
58
    int min;
59
    int max;
60
    int value;
61
  };
62
63
#define INT_OPTION(FLAG, MIN_V, MAX_V, V) \
64
  IntOptionSpec { FLAG, #FLAG, MIN_V, MAX_V, V }
65
66
  std::vector<IntOptionSpec> int_options = {
67
      INT_OPTION(JXL_ENC_FRAME_SETTING_EFFORT, 1, 9, 0),
68
      INT_OPTION(JXL_ENC_FRAME_SETTING_DECODING_SPEED, 0, 4, 0),
69
      INT_OPTION(JXL_ENC_FRAME_SETTING_NOISE, -1, 1, 0),
70
      INT_OPTION(JXL_ENC_FRAME_SETTING_DOTS, -1, 1, 0),
71
      INT_OPTION(JXL_ENC_FRAME_SETTING_PATCHES, -1, 1, 0),
72
      INT_OPTION(JXL_ENC_FRAME_SETTING_EPF, -1, 3, 0),
73
      INT_OPTION(JXL_ENC_FRAME_SETTING_GABORISH, -1, 1, 0),
74
      INT_OPTION(JXL_ENC_FRAME_SETTING_MODULAR, -1, 1, 0),
75
      INT_OPTION(JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE, -1, 1, 0),
76
      INT_OPTION(JXL_ENC_FRAME_SETTING_RESPONSIVE, -1, 1, 0),
77
      INT_OPTION(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, -1, 1, 0),
78
      INT_OPTION(JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, -1, 1, 0),
79
      INT_OPTION(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, -1, 1, 0),
80
      INT_OPTION(JXL_ENC_FRAME_SETTING_PALETTE_COLORS, -1, 255, 0),
81
      INT_OPTION(JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, -1, 1, 0),
82
      INT_OPTION(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, -1, 2, 0),
83
      INT_OPTION(JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, -1, 41, 0),
84
      INT_OPTION(JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, -1, 3, 0),
85
      INT_OPTION(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, -1, 15, 0),
86
      INT_OPTION(JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, -1, 11, 0),
87
  };
88
89
#undef INT_OPTION
90
91
  struct FloatOptionSpec {
92
    JxlEncoderFrameSettingId flag;
93
    float possible_values[4];
94
    float value;
95
  };
96
97
  std::vector<FloatOptionSpec> float_options = {
98
      FloatOptionSpec{JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT,
99
                      {-1, 0, 50, 100},
100
                      -1},
101
      FloatOptionSpec{JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT,
102
                      {-1, 0, 50, 100},
103
                      -1},
104
      FloatOptionSpec{JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT,
105
                      {-1, 0, 50, 100},
106
                      -1},
107
      FloatOptionSpec{
108
          JXL_ENC_FRAME_SETTING_PHOTON_NOISE, {-1, 200, 1600, 10000}, -1},
109
  };
110
111
  uint8_t num_threads;
112
113
  float distance;  // 0.01 - 25
114
115
  // Tiled to cover the entire image area.
116
  uint16_t pixel_data[4][64][64];
117
118
630
  static FuzzSpec FromData(const uint8_t* data, size_t len) {
119
630
    size_t pos = 0;
120
20.6M
    auto u8 = [&]() -> uint8_t {
121
20.6M
      if (pos == len) return 0;
122
5.55M
      return data[pos++];
123
20.6M
    };
124
1.26k
    auto b1 = [&]() -> bool { return static_cast<bool>(u8() % 2); };
125
10.3M
    auto u16 = [&]() -> uint16_t { return (uint16_t{u8()} << 8) | u8(); };
126
630
    FuzzSpec spec;
127
    // TODO(eustas): allow dimensions to be 130k
128
630
    spec.xsize = uint32_t{u16()} + 1;
129
630
    spec.ysize = uint32_t{u16()} + 1;
130
630
    spec.grayscale = b1();
131
630
    spec.alpha = b1();
132
630
    spec.bit_depth = u8() % 16 + 1;
133
    // constants chosen so to cover the entire 0.01 - 25 range.
134
630
    bool lossless = ((u8() % 2) == 1);
135
630
    spec.distance = lossless ? 0.0 : 0.01 + 0.00038132 * u16();
136
137
630
    spec.num_threads = u8() & 0xF;
138
139
12.6k
    for (auto& int_opt : spec.int_options) {
140
12.6k
      int_opt.value = u8() % (int_opt.max - int_opt.min + 1) + int_opt.min;
141
12.6k
    }
142
143
630
    Check(spec.int_options[15].flag == JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM);
144
630
    if (!lossless || spec.int_options[15].value == 0) {
145
447
      Check(spec.float_options[2].flag ==
146
447
            JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT);
147
447
      spec.float_options[2].possible_values[1] = 1;
148
447
    }
149
150
2.52k
    for (auto& float_opt : spec.float_options) {
151
2.52k
      float_opt.value = float_opt.possible_values[u8() % 4];
152
2.52k
    }
153
154
630
    Check(spec.int_options[7].flag == JXL_ENC_FRAME_SETTING_MODULAR);
155
630
    bool modular = (spec.int_options[7].value == 1);
156
630
    Check(spec.int_options[18].flag == JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR);
157
630
    bool slow_predictor = (spec.int_options[18].value >= 14);
158
630
    uint64_t max_size = kBaseMaxSize;
159
630
    if (modular && slow_predictor) max_size /= 2;
160
630
    if (sizeof(size_t) == 4) max_size /= 1.5;
161
630
    constexpr size_t group_dim = 256;
162
630
    uint64_t in_mem_xsize = jxl::RoundUpTo(spec.xsize, group_dim);
163
630
    if (in_mem_xsize * spec.ysize > max_size) {
164
14
      spec.ysize = max_size / in_mem_xsize;
165
14
      Check(spec.ysize > 0);
166
14
    }
167
630
    uint64_t in_mem_ysize = jxl::RoundUpTo(spec.ysize, group_dim);
168
630
    if (spec.xsize * in_mem_ysize > max_size) {
169
7
      spec.xsize = max_size / in_mem_ysize;
170
7
      Check(spec.xsize > 0);
171
7
    }
172
173
2.52k
    for (auto& x : spec.pixel_data) {
174
161k
      for (auto& y : x) {
175
10.3M
        for (auto& p : y) {
176
10.3M
          p = u16();
177
10.3M
        }
178
161k
      }
179
2.52k
    }
180
181
630
    if (false) {
182
0
      fprintf(stderr, "Image size: %d X %d, d=%f, num_threads: %d\n",
183
0
              spec.xsize, spec.ysize, spec.distance, spec.num_threads);
184
0
      for (auto& int_opt : spec.int_options) {
185
0
        fprintf(stderr, "%s = %d\n", int_opt.name.c_str(), int_opt.value);
186
0
      }
187
0
    }
188
189
630
    return spec;
190
630
  }
191
};
192
193
StatusOr<std::vector<uint8_t>> Encode(const FuzzSpec& spec,
194
                                      TrackingMemoryManager& memory_manager,
195
1.25k
                                      bool streaming) {
196
1.25k
  auto runner = JxlThreadParallelRunnerMake(nullptr, spec.num_threads);
197
1.25k
  JxlEncoderPtr enc_ptr = JxlEncoderMake(memory_manager.get());
198
1.25k
  JxlEncoder* enc = enc_ptr.get();
199
200
1.25k
  Check(JxlEncoderSetParallelRunner(enc, JxlThreadParallelRunner,
201
1.25k
                                    runner.get()) == JXL_ENC_SUCCESS);
202
1.25k
  JxlEncoderFrameSettings* frame_settings =
203
1.25k
      JxlEncoderFrameSettingsCreate(enc, nullptr);
204
1.25k
  Check(frame_settings != nullptr);
205
206
1.25k
  Check(JxlEncoderSetFrameDistance(frame_settings, spec.distance) ==
207
1.25k
        JXL_ENC_SUCCESS);
208
209
25.1k
  for (const auto& opt : spec.int_options) {
210
25.1k
    Check(JxlEncoderFrameSettingsSetOption(frame_settings, opt.flag,
211
25.1k
                                           opt.value) == JXL_ENC_SUCCESS);
212
25.1k
  }
213
5.02k
  for (const auto& opt : spec.float_options) {
214
5.02k
    if (opt.value != -1) {
215
2.52k
      Check(JxlEncoderFrameSettingsSetFloatOption(
216
2.52k
                frame_settings, opt.flag, opt.value) == JXL_ENC_SUCCESS);
217
2.52k
    }
218
5.02k
  }
219
220
1.25k
  Check(JxlEncoderFrameSettingsSetOption(frame_settings,
221
1.25k
                                         JXL_ENC_FRAME_SETTING_BUFFERING,
222
1.25k
                                         streaming ? 3 : 0) == JXL_ENC_SUCCESS);
223
224
1.25k
  JxlBasicInfo basic_info;
225
1.25k
  JxlEncoderInitBasicInfo(&basic_info);
226
1.25k
  basic_info.num_color_channels = spec.grayscale ? 1 : 3;
227
1.25k
  basic_info.xsize = spec.xsize;
228
1.25k
  basic_info.ysize = spec.ysize;
229
1.25k
  basic_info.bits_per_sample = spec.bit_depth;
230
1.25k
  bool lossless = (spec.distance == 0.0f);
231
1.25k
  basic_info.uses_original_profile = TO_JXL_BOOL(lossless);
232
1.25k
  uint32_t nchan = basic_info.num_color_channels;
233
1.25k
  if (spec.alpha) {
234
320
    nchan += 1;
235
320
    basic_info.alpha_bits = spec.bit_depth;
236
320
    basic_info.num_extra_channels = 1;
237
320
  }
238
1.25k
  Check(JxlEncoderSetBasicInfo(enc, &basic_info) == JXL_ENC_SUCCESS);
239
1.25k
  if (spec.alpha) {
240
320
    JxlExtraChannelInfo info;
241
320
    memset(&info, 0, sizeof(info));
242
320
    info.type = JxlExtraChannelType::JXL_CHANNEL_ALPHA;
243
320
    info.bits_per_sample = spec.bit_depth;
244
320
    JxlEncoderSetExtraChannelInfo(enc, 0, &info);
245
320
  }
246
1.25k
  JxlColorEncoding color_encoding;
247
1.25k
  memset(&color_encoding, 0, sizeof(color_encoding));
248
1.25k
  color_encoding.color_space = spec.grayscale
249
1.25k
                                   ? JxlColorSpace::JXL_COLOR_SPACE_GRAY
250
1.25k
                                   : JxlColorSpace::JXL_COLOR_SPACE_RGB;
251
1.25k
  color_encoding.transfer_function =
252
1.25k
      JxlTransferFunction::JXL_TRANSFER_FUNCTION_SRGB;
253
1.25k
  color_encoding.primaries = JxlPrimaries::JXL_PRIMARIES_2100;
254
1.25k
  color_encoding.white_point = JxlWhitePoint::JXL_WHITE_POINT_D65;
255
1.25k
  color_encoding.rendering_intent =
256
1.25k
      JxlRenderingIntent::JXL_RENDERING_INTENT_RELATIVE;
257
1.25k
  Check(JxlEncoderSetColorEncoding(enc, &color_encoding) == JXL_ENC_SUCCESS);
258
259
1.25k
  JxlFrameHeader frame_header;
260
1.25k
  JxlEncoderInitFrameHeader(&frame_header);
261
  // TODO(szabadka) Add more frame header options.
262
1.25k
  Check(JxlEncoderSetFrameHeader(frame_settings, &frame_header) ==
263
1.25k
        JXL_ENC_SUCCESS);
264
1.25k
  JxlPixelFormat pixelformat = {nchan, JXL_TYPE_UINT16, JXL_LITTLE_ENDIAN, 0};
265
1.25k
  std::vector<uint16_t> pixels(spec.xsize * static_cast<uint64_t>(spec.ysize) *
266
1.25k
                               nchan);
267
3.16M
  for (size_t y = 0; y < spec.ysize; y++) {
268
558M
    for (size_t x = 0; x < spec.xsize; x++) {
269
1.66G
      for (size_t c = 0; c < nchan; c++) {
270
        // TODO(eustas): make it less regular for tiles except (0, 0)
271
1.10G
        pixels[(y * spec.xsize + x) * nchan + c] =
272
1.10G
            spec.pixel_data[c][y % 64][x % 64];
273
1.10G
      }
274
555M
    }
275
3.16M
  }
276
1.25k
  JxlEncoderStatus status =
277
1.25k
      JxlEncoderAddImageFrame(frame_settings, &pixelformat, pixels.data(),
278
1.25k
                              pixels.size() * sizeof(uint16_t));
279
  // TODO(eustas): update when API will provide OOM status.
280
1.25k
  if (memory_manager.seen_oom) {
281
    // Actually, that is fine.
282
0
    return JXL_FAILURE("OOM");
283
0
  }
284
1.25k
  Check(status == JXL_ENC_SUCCESS);
285
1.25k
  JxlEncoderCloseInput(enc);
286
  // Reading compressed output
287
1.25k
  JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
288
1.25k
  std::vector<uint8_t> buf(1024);
289
1.25k
  size_t written = 0;
290
6.39k
  while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
291
5.13k
    buf.resize(buf.size() * 2);
292
5.13k
    uint8_t* next_out = buf.data() + written;
293
5.13k
    size_t avail_out = buf.size() - written;
294
5.13k
    process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out);
295
5.13k
    written = next_out - buf.data();
296
5.13k
  }
297
  // TODO(eustas): update when API will provide OOM status.
298
1.25k
  if (memory_manager.seen_oom) {
299
    // Actually, that is fine.
300
5
    return JXL_FAILURE("OOM");
301
5
  }
302
1.25k
  Check(process_result == JXL_ENC_SUCCESS);
303
1.25k
  buf.resize(written);
304
305
1.25k
  return buf;
306
1.25k
}
307
308
Status Decode(const std::vector<uint8_t>& data,
309
                                    TrackingMemoryManager& memory_manager,
310
1.25k
                                    std::vector<float>& pixels) {
311
  // Multi-threaded parallel runner.
312
1.25k
  auto dec = JxlDecoderMake(memory_manager.get());
313
1.25k
  Check(JxlDecoderSubscribeEvents(dec.get(),
314
1.25k
                                  JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE) ==
315
1.25k
        JXL_DEC_SUCCESS);
316
317
1.25k
  JxlBasicInfo info;
318
1.25k
  JxlPixelFormat format = {3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0};
319
320
1.25k
  pixels.clear();
321
322
1.25k
  JxlDecoderSetInput(dec.get(), data.data(), data.size());
323
1.25k
  JxlDecoderCloseInput(dec.get());
324
325
3.75k
  for (;;) {
326
3.75k
    JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
327
328
3.75k
    if (status == JXL_DEC_BASIC_INFO) {
329
1.25k
      Check(JxlDecoderGetBasicInfo(dec.get(), &info) == JXL_DEC_SUCCESS);
330
2.50k
    } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
331
1.25k
      size_t buffer_size;
332
1.25k
      Check(JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size) ==
333
1.25k
            JXL_DEC_SUCCESS);
334
1.25k
      pixels.resize(buffer_size / sizeof(float));
335
1.25k
      void* pixels_buffer = static_cast<void*>(pixels.data());
336
1.25k
      size_t pixels_buffer_size = pixels.size() * sizeof(float);
337
1.25k
      Check(JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels_buffer,
338
1.25k
                                        pixels_buffer_size) == JXL_DEC_SUCCESS);
339
1.25k
    } else if (status == JXL_DEC_FULL_IMAGE || status == JXL_DEC_SUCCESS) {
340
1.25k
      return true;
341
1.25k
    } else {
342
      // TODO(eustas): update when API will provide OOM status.
343
0
      if (memory_manager.seen_oom) {
344
        // Actually, that is fine.
345
0
        return JXL_FAILURE("OOM");
346
0
      }
347
      // Unexpected status
348
0
      Check(false);
349
0
    }
350
3.75k
  }
351
1.25k
}
352
353
630
void Run(const FuzzSpec& spec) {
354
630
  size_t memory_cap = kMemoryCap;
355
630
  size_t total_cap_multiplier = 5;
356
630
  if (spec.xsize < 64 || spec.ysize < 64) {
357
442
    total_cap_multiplier = 20;
358
442
  }
359
630
  TrackingMemoryManager memory_manager{memory_cap,
360
630
                                       memory_cap * total_cap_multiplier};
361
362
630
  std::vector<uint8_t> enc_default;
363
630
  std::vector<uint8_t> enc_streaming;
364
365
630
  const auto encode = [&]() -> Status {
366
    // It is not clear, which approach eats more memory.
367
630
    JXL_ASSIGN_OR_RETURN(enc_default, Encode(spec, memory_manager, false));
368
625
    Check(memory_manager.Reset());
369
625
    JXL_ASSIGN_OR_RETURN(enc_streaming, Encode(spec, memory_manager, true));
370
625
    Check(memory_manager.Reset());
371
625
    return true;
372
625
  };
373
  // It is fine, if encoder OOMs.
374
630
  if (!encode()) return;
375
376
  // It is NOT OK, if decoder OOMs - it should not consume more than encoder.
377
625
  std::vector<float> dec_default;
378
625
  Check(Decode(enc_default, memory_manager, dec_default));
379
625
  Check(memory_manager.Reset());
380
625
  std::vector<float> dec_streaming;
381
625
  Check(Decode(enc_streaming, memory_manager, dec_streaming));
382
625
  Check(memory_manager.Reset());
383
384
625
  Check(dec_default.size() == dec_streaming.size());
385
386
625
  Check(spec.int_options[0].flag == JXL_ENC_FRAME_SETTING_EFFORT);
387
625
  int effort = spec.int_options[0].value;
388
625
  std::array<float, 10> kThreshold = {0.00f, 0.05f, 0.05f, 0.05f, 0.05f,
389
625
                                      0.0625f, 0.0625f, 0.0625f, 0.10f, 0.10f};
390
625
  float threshold = kThreshold[effort];
391
 
392
625
  int outlier_count = 0;
393
713M
  for (size_t i = 0; i < dec_default.size(); ++i) {
394
713M
    float d1 = ::jxl::Clamp1(dec_default[i], 0.0f, 1.0f);
395
713M
    float d2 = ::jxl::Clamp1(dec_streaming[i], 0.0f, 1.0f);
396
713M
    float abs_diff = std::abs(d1 - d2);
397
713M
    if (abs_diff > threshold) outlier_count++;
398
713M
  }
399
625
  if (false) {
400
0
    fprintf(stderr, "Number of outlier values: %d / %d\n", outlier_count,
401
0
            static_cast<int>(dec_default.size()));
402
0
  }
403
625
  Check(outlier_count == 0);
404
625
}
405
406
630
int DoTestOneInput(const uint8_t* data, size_t size) {
407
630
  auto spec = FuzzSpec::FromData(data, size);
408
409
630
  Run(spec);
410
630
  return 0;
411
630
}
412
413
}  // namespace
414
415
63.5k
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
416
63.5k
  return DoTestOneInput(data, size);
417
63.5k
}
418
419
0
void TestOneInput(const std::vector<uint8_t>& data) {
420
0
  DoTestOneInput(data.data(), data.size());
421
0
}
422
423
FUZZ_TEST(StreamingFuzzTest, TestOneInput);