Commit 6fb37974 for libheif
commit 6fb37974726abde6de7fe2206386d4a6d9ce1c39
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sun Oct 25 01:33:01 2026 +0200
Bound Exif IFD traversal by input size [GHSA-pfcm-ffqg-84hx]
diff --git a/heifio/exif.cc b/heifio/exif.cc
index ec5725c2..fd1be5f0 100644
--- a/heifio/exif.cc
+++ b/heifio/exif.cc
@@ -20,6 +20,7 @@
#include <cassert>
#include <cstdio>
+#include <vector>
#include "exif.h"
#include "common_utils.h"
@@ -109,61 +110,73 @@ static uint32_t find_exif_tag_in_ifd(const uint8_t* exif, uint32_t size,
uint32_t ifd_offset,
uint16_t query_tag,
bool littleEndian,
- int recursion_depth)
+ uint32_t remaining_bytes)
{
- const int MAX_IFD_TABLE_RECURSION_DEPTH = 5;
-
- if (recursion_depth > MAX_IFD_TABLE_RECURSION_DEPTH) {
- return 0;
- }
-
- uint32_t offset = ifd_offset;
-
- // is offset valid (i.e. can we read at least the 'size' field and the pointer to the next IFD ?)
- if (offset == 0) {
- return 0;
- }
+ struct IFDFrame {
+ uint32_t offset;
+ uint16_t count = 0;
+ uint16_t next_entry = 0;
+ bool initialized = false;
+ };
+
+ std::vector<IFDFrame> stack;
+ stack.push_back({ifd_offset});
+
+ while (!stack.empty()) {
+ IFDFrame& frame = stack.back();
+ uint32_t offset = frame.offset;
+
+ if (!frame.initialized) {
+ if (offset == 0 || size < 6 || offset > size - 6) {
+ stack.pop_back();
+ continue;
+ }
- if (size < 6 || size - 2 - 4 < offset) {
- return 0;
- }
+ uint16_t count = read16(exif, size, offset, littleEndian);
+ uint32_t table_size = 2U + count * 12U + 4U;
+ if (table_size > size - offset) {
+ stack.pop_back();
+ continue;
+ }
- uint16_t cnt = read16(exif, size, offset, littleEndian);
+ // Charge the whole IFD, including the next-IFD pointer. Repeated visits
+ // consume the budget even when an IFD has no entries.
+ if (table_size > remaining_bytes) {
+ stack.pop_back();
+ continue;
+ }
+ remaining_bytes -= table_size;
- // Does the IFD table fit into our memory range? We need this check to prevent an underflow in the following statement.
- uint32_t IFD_table_size = 2U + cnt * 12U + 4U;
- if (IFD_table_size > size) {
- return 0;
- }
+ frame.count = count;
+ frame.initialized = true;
+ }
- // end of IFD table would exceed the end of the EXIF data
- // offset + IFD_table_size > size ?
- if (size - IFD_table_size < offset) {
- return 0;
- }
+ if (frame.next_entry < frame.count) {
+ uint32_t entry_offset = offset + 2U + frame.next_entry * 12U;
+ frame.next_entry++;
+ uint16_t tag = read16(exif, size, entry_offset, littleEndian);
+ if (tag == query_tag) {
+ return entry_offset;
+ }
- for (int i = 0; i < cnt; i++) {
- int tag = read16(exif, size, offset + 2 + i * 12, littleEndian);
- if (tag == query_tag) {
- return offset + 2 + i * 12;
+ if (tag == EXIF_TAG_EXIF_IFD_POINTER) {
+ uint32_t exif_ifd_offset = read32(exif, size, entry_offset + 8, littleEndian);
+ if (exif_ifd_offset != 0) {
+ stack.push_back({exif_ifd_offset});
+ }
+ }
}
-
- if (tag == EXIF_TAG_EXIF_IFD_POINTER) {
- uint32_t exifIFD_offset = read32(exif, size, offset + 2 + i * 12 + 8, littleEndian);
- uint32_t tag_position = find_exif_tag_in_ifd(exif, size, exifIFD_offset, query_tag, littleEndian,
- recursion_depth + 1);
- if (tag_position) {
- return tag_position;
+ else {
+ uint32_t next_offset_pos = offset + 2U + frame.count * 12U;
+ uint32_t next_ifd_offset = read32(exif, size, next_offset_pos, littleEndian);
+ stack.pop_back();
+ if (next_ifd_offset != 0) {
+ stack.push_back({next_ifd_offset});
}
}
}
- // continue with next IFD table
-
- uint32_t pos = offset + 2 + cnt * 12;
- uint32_t next_ifd_offset = read32(exif, size, pos, littleEndian);
-
- return find_exif_tag_in_ifd(exif, size, next_ifd_offset, query_tag, littleEndian, recursion_depth + 1);
+ return 0;
}
@@ -192,7 +205,7 @@ static uint32_t find_exif_tag(const uint8_t* exif, uint32_t size, uint16_t query
uint32_t offset;
offset = read32(exif, size, 4, littleEndian);
- uint32_t tag_position = find_exif_tag_in_ifd(exif, size, offset, query_tag, littleEndian, 1);
+ uint32_t tag_position = find_exif_tag_in_ifd(exif, size, offset, query_tag, littleEndian, size);
return tag_position;
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 52780432..df2555c3 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -216,6 +216,9 @@ else()
message(WARNING "Tests of the 'uncompressed codec' are not compiled because the uncompressed codec is not enabled (WITH_UNCOMPRESSED_CODEC==OFF)")
endif ()
+add_heifio_test(exif_ifd_loop)
+set_tests_properties(exif_ifd_loop PROPERTIES TIMEOUT 10)
+
# find_package(TIFF) is called in the heifio subdirectory, but its result
# variables do not propagate to this sibling directory scope. Query again
# (the result is cached) so we only build the TIFF test when libtiff is present.
diff --git a/tests/exif_ifd_loop.cc b/tests/exif_ifd_loop.cc
new file mode 100644
index 00000000..cb049807
--- /dev/null
+++ b/tests/exif_ifd_loop.cc
@@ -0,0 +1,172 @@
+/*
+ libheifio Exif IFD loop unit tests
+
+ MIT License
+
+ Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+// The Exif tag lookup follows ExifIFD pointer entries (tag 0x8769) recursively.
+// An IFD whose entries all point back to itself must not cost N^depth work.
+
+#include "catch_amalgamated.hpp"
+#include "heifio/exif.h"
+
+#include <cstdint>
+#include <vector>
+
+static void put16(std::vector<uint8_t>& v, uint16_t x)
+{
+ v.push_back((uint8_t) (x >> 8));
+ v.push_back((uint8_t) (x & 0xFF));
+}
+
+static void put32(std::vector<uint8_t>& v, uint32_t x)
+{
+ put16(v, (uint16_t) (x >> 16));
+ put16(v, (uint16_t) (x & 0xFFFF));
+}
+
+// One IFD entry with a 4-byte inline value.
+static void put_entry(std::vector<uint8_t>& v, uint16_t tag, uint16_t type, uint32_t count, uint32_t value)
+{
+ put16(v, tag);
+ put16(v, type);
+ put32(v, count);
+ put32(v, value);
+}
+
+static std::vector<uint8_t> tiff_header()
+{
+ std::vector<uint8_t> v;
+ v.push_back('M');
+ v.push_back('M');
+ put16(v, 42);
+ put32(v, 8); // IFD0 at offset 8
+ return v;
+}
+
+
+TEST_CASE("exif IFD entries that point back to their own IFD")
+{
+ const int entry_count = 2000;
+
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, entry_count);
+ for (int i = 0; i < entry_count; i++) {
+ put_entry(exif, 0x8769 /* ExifIFD pointer */, 4 /* LONG */, 1, 8 /* IFD0 itself */);
+ }
+ put32(exif, 0); // no next IFD
+
+ // Without a bound on the total number of scanned entries, this would take entry_count^5 steps.
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 1);
+
+ modify_exif_orientation_tag_if_it_exists(exif.data(), (uint32_t) exif.size(), 3);
+ overwrite_exif_image_size_if_it_exists(exif.data(), (uint32_t) exif.size(), 100, 200);
+}
+
+
+TEST_CASE("empty exif IFD whose next pointer points to itself")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, 0);
+ put32(exif, 8); // next IFD is this IFD
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 1);
+}
+
+
+TEST_CASE("exif orientation after a cyclic IFD pointer is still found")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, 2);
+ put_entry(exif, 0x8769 /* ExifIFD pointer */, 4 /* LONG */, 1, 8 /* IFD0 itself */);
+ put_entry(exif, 0x0112 /* Orientation */, 3 /* SHORT */, 1, 6u << 16);
+ put32(exif, 0);
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 6);
+}
+
+
+TEST_CASE("exif orientation beyond five IFDs is found")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ for (uint32_t i = 0; i < 7; i++) {
+ put16(exif, 0);
+ put32(exif, 8 + (i + 1) * 6);
+ }
+ put16(exif, 1);
+ put_entry(exif, 0x0112 /* Orientation */, 3 /* SHORT */, 1, 7u << 16);
+ put32(exif, 0);
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 7);
+}
+
+
+TEST_CASE("deep cyclic exif IFD pointer does not exhaust the call stack")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, 1);
+ put_entry(exif, 0x8769 /* ExifIFD pointer */, 4 /* LONG */, 1, 8 /* IFD0 itself */);
+ put32(exif, 0);
+ exif.resize(18 * 10000);
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 1);
+}
+
+
+TEST_CASE("exif orientation in IFD0 is found")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, 2);
+ put_entry(exif, 0x8769 /* ExifIFD pointer */, 4 /* LONG */, 1, 8 + 2 + 2 * 12 + 4);
+ put_entry(exif, 0x0112 /* Orientation */, 3 /* SHORT */, 1, 6u << 16);
+ put32(exif, 0); // no next IFD
+
+ // Exif IFD
+ put16(exif, 1);
+ put_entry(exif, 0xA002 /* PixelXDimension */, 4 /* LONG */, 1, 10);
+ put32(exif, 0);
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 6);
+}
+
+
+TEST_CASE("exif tag in the Exif IFD is found")
+{
+ std::vector<uint8_t> exif = tiff_header();
+ put16(exif, 1);
+ put_entry(exif, 0x8769 /* ExifIFD pointer */, 4 /* LONG */, 1, 8 + 2 + 1 * 12 + 4);
+ put32(exif, 0); // no next IFD
+
+ // Exif IFD with the orientation tag (not where it belongs, but it has to be reachable through the pointer)
+ const size_t exifIFD_pos = exif.size();
+ put16(exif, 1);
+ put_entry(exif, 0x0112 /* Orientation */, 3 /* SHORT */, 1, 8u << 16);
+ put32(exif, 0);
+
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 8);
+
+ modify_exif_orientation_tag_if_it_exists(exif.data(), (uint32_t) exif.size(), 1);
+ REQUIRE(exif[exifIFD_pos + 2 + 8] == 0);
+ REQUIRE(exif[exifIFD_pos + 2 + 9] == 1);
+ REQUIRE(read_exif_orientation_tag(exif.data(), (uint32_t) exif.size()) == 1);
+}