Commit c5cd4f1c for libheif
commit c5cd4f1cd38cda40eadcf78e1c8f1ad3621749fb
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon May 18 16:27:16 2026 +0200
add tile_fuzzer to exercise tiling API (#1807)
diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt
index 7169121c..19bb53e5 100644
--- a/fuzzing/CMakeLists.txt
+++ b/fuzzing/CMakeLists.txt
@@ -15,5 +15,8 @@ configure_file(encoder_fuzzer_lsan_suppressions.txt ${CMAKE_CURRENT_BINARY_DIR}/
add_executable(file_fuzzer file_fuzzer.cc)
target_link_libraries(file_fuzzer PRIVATE heif)
+add_executable(tile_fuzzer tile_fuzzer.cc)
+target_link_libraries(tile_fuzzer PRIVATE heif)
+
add_executable(api_fuzzer api_fuzzer.cc)
target_link_libraries(api_fuzzer PRIVATE heif)
diff --git a/fuzzing/tile_fuzzer.cc b/fuzzing/tile_fuzzer.cc
new file mode 100644
index 00000000..4caf16c0
--- /dev/null
+++ b/fuzzing/tile_fuzzer.cc
@@ -0,0 +1,168 @@
+/*
+ * HEIF codec.
+ * Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Fuzz harness for the per-tile read API (heif_image_handle_get_image_tiling,
+// heif_image_handle_get_grid_image_tile_id, heif_image_handle_decode_image_tile).
+// file_fuzzer.cc only calls heif_decode_image, which routes grid images through
+// decode_full_grid_image and never exercises decode_grid_tile or the surrounding
+// tiling-API surface. This harness fills that gap.
+
+#include <assert.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "libheif/heif.h"
+#include "libheif/heif_tiling.h"
+
+static const enum heif_colorspace kFuzzColorSpace = heif_colorspace_YCbCr;
+static const enum heif_chroma kFuzzChroma = heif_chroma_420;
+
+// Cap how many tiles we try to decode per image. Synthetic grids can claim
+// millions of tiles; bounding here keeps the per-input time budget sane
+// without losing meaningful coverage (crashes show up on the first few tiles).
+static const uint32_t kMaxTilesPerImage = 32;
+
+static void TestTileAPI(const struct heif_image_handle* handle,
+ int process_image_transformations)
+{
+ struct heif_image_tiling tiling = {};
+ struct heif_error err = heif_image_handle_get_image_tiling(
+ handle, process_image_transformations, &tiling);
+ if (err.code != heif_error_Ok) {
+ return;
+ }
+
+ // Probe per-tile id lookup and decode within the declared grid, capped.
+ uint32_t cols = tiling.num_columns;
+ uint32_t rows = tiling.num_rows;
+ uint32_t total = 0;
+
+ for (uint32_t ty = 0; ty < rows && total < kMaxTilesPerImage; ty++) {
+ for (uint32_t tx = 0; tx < cols && total < kMaxTilesPerImage; tx++) {
+ heif_item_id tile_id = 0;
+ // Return value intentionally ignored; we only care that the call does
+ // not crash on malformed input.
+ heif_image_handle_get_grid_image_tile_id(
+ handle, process_image_transformations, tx, ty, &tile_id);
+
+ struct heif_image* tile_img = nullptr;
+ err = heif_image_handle_decode_image_tile(
+ handle, &tile_img, kFuzzColorSpace, kFuzzChroma, nullptr, tx, ty);
+ if (err.code == heif_error_Ok && tile_img != nullptr) {
+ heif_image_release(tile_img);
+ } else if (tile_img != nullptr) {
+ // Defensive: some error paths may still produce an image we own.
+ heif_image_release(tile_img);
+ }
+
+ total++;
+ }
+ }
+}
+
+static int clip_int(size_t size)
+{
+ return size > INT_MAX ? INT_MAX : static_cast<int>(size);
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ struct heif_context* ctx;
+ struct heif_error err;
+ struct heif_image_handle* primary_handle = nullptr;
+ int images_count;
+ heif_item_id* image_IDs = nullptr;
+ bool explicit_init = size == 0 || data[size - 1] & 1;
+
+ if (explicit_init) {
+ heif_init(nullptr);
+ }
+
+ heif_check_filetype(data, clip_int(size));
+
+ ctx = heif_context_alloc();
+ assert(ctx);
+
+ auto* limits = heif_context_get_security_limits(ctx);
+ limits->max_total_memory = UINT64_C(2) * 1024 * 1024 * 1024;
+ limits->max_memory_block_size = 128 * 1024 * 1024;
+
+ err = heif_context_read_from_memory(ctx, data, size, nullptr);
+ if (err.code != heif_error_Ok) {
+ goto quit;
+ }
+
+ err = heif_context_get_primary_image_handle(ctx, &primary_handle);
+ if (err.code == heif_error_Ok) {
+ TestTileAPI(primary_handle, /*process_image_transformations=*/1);
+ TestTileAPI(primary_handle, /*process_image_transformations=*/0);
+ heif_image_handle_release(primary_handle);
+ primary_handle = nullptr;
+ }
+
+ images_count = heif_context_get_number_of_top_level_images(ctx);
+ if (!images_count) {
+ goto quit;
+ }
+
+ image_IDs = static_cast<heif_item_id*>(malloc(images_count * sizeof(heif_item_id)));
+ assert(image_IDs);
+ images_count = heif_context_get_list_of_top_level_image_IDs(ctx, image_IDs, images_count);
+ if (!images_count) {
+ goto quit;
+ }
+
+ for (int i = 0; i < images_count; ++i) {
+ struct heif_image_handle* image_handle = nullptr;
+ err = heif_context_get_image_handle(ctx, image_IDs[i], &image_handle);
+ if (err.code != heif_error_Ok) {
+ heif_image_handle_release(image_handle);
+ continue;
+ }
+
+ TestTileAPI(image_handle, /*process_image_transformations=*/1);
+
+ // Also iterate thumbnails — these can themselves be grid/overlay items
+ // and have separate decoder paths.
+ int num_thumbnails = heif_image_handle_get_number_of_thumbnails(image_handle);
+ for (int t = 0; t < num_thumbnails; ++t) {
+ struct heif_image_handle* thumbnail_handle = nullptr;
+ heif_image_handle_get_thumbnail(image_handle, t, &thumbnail_handle);
+ if (thumbnail_handle) {
+ TestTileAPI(thumbnail_handle, /*process_image_transformations=*/1);
+ heif_image_handle_release(thumbnail_handle);
+ }
+ }
+
+ heif_image_handle_release(image_handle);
+ }
+
+quit:
+ heif_image_handle_release(primary_handle);
+ heif_context_free(ctx);
+ free(image_IDs);
+
+ if (explicit_init) {
+ heif_deinit();
+ }
+
+ return 0;
+}
diff --git a/scripts/run-ci.sh b/scripts/run-ci.sh
index 9a852838..f6340640 100755
--- a/scripts/run-ci.sh
+++ b/scripts/run-ci.sh
@@ -282,6 +282,7 @@ fi
if [ ! -z "$FUZZER" ] && [ "$CURRENT_OS" = "linux" ]; then
./fuzzing/color_conversion_fuzzer ./fuzzing/data/corpus/*color-conversion-fuzzer*
./fuzzing/file_fuzzer ./fuzzing/data/corpus/*.heic
+ ./fuzzing/tile_fuzzer ./fuzzing/data/corpus/*.heic
echo "Running color conversion fuzzer ..."
./fuzzing/color_conversion_fuzzer -max_total_time=120
@@ -289,7 +290,10 @@ if [ ! -z "$FUZZER" ] && [ "$CURRENT_OS" = "linux" ]; then
# Do not run encoder_fuzzer because it will just find errors in x265...
#echo "Running encoder fuzzer ..."
#./fuzzing/encoder_fuzzer -max_total_time=120
-
+
echo "Running file fuzzer ..."
./fuzzing/file_fuzzer -dict=./fuzzing/data/dictionary.txt -max_total_time=120
+
+ echo "Running tile fuzzer ..."
+ ./fuzzing/tile_fuzzer -dict=./fuzzing/data/dictionary.txt -max_total_time=120
fi