Commit 4ee499d2 for libheif

commit 4ee499d2715a62c62aa2a5c1fb5915758bd7fce4
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 5 17:55:45 2026 +0200

    Give ImageItem_Error a real context to fix a null-deref in verify_decodable (CIFuzz)

    ImageItem_Error (the placeholder for items whose type/config libheif cannot
    handle) was constructed with a null HeifContext. Any code that dereferenced its
    context therefore crashed. The verify_decodable() pre-pass added for
    GHSA-prgh-72vc-3xmc calls get_file() (-> context->get_heif_file()) on the item
    before decoding, so decoding such an item was a NULL-pointer dereference.

    A fuzzer reaches it because the depth/aux/thumbnail handle getters do not reject
    items with an item-error: an unsupported item (e.g. 'lhv1') attached as a depth
    auxiliary yields a decodable handle, which heif_decode_image() then runs.

    Pass the real context to ImageItem_Error at all construction sites instead of
    nullptr. The class never uses the context (its decode methods just return the
    stored error), so this is behavior-neutral, and get_context()/get_file() are now
    safe on every item. The two null-context guards previously added to the
    verify_decodable() reference walk are no longer needed and are removed.

    Adds a regression test that decodes an unsupported ('lhv1') depth-aux item
    through its depth-image handle and checks the item's error is returned instead
    of crashing.

diff --git a/libheif/context.cc b/libheif/context.cc
index 31dcbd24..f433606a 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -636,14 +636,14 @@ Error HeifContext::interpret_heif_file_images()
     std::vector<std::shared_ptr<Box>> properties;
     Error err = m_heif_file->get_properties(id, properties);
     if (err) {
-      imageItem = std::make_shared<ImageItem_Error>(imageItem->get_infe_type(), id, err);
+      imageItem = std::make_shared<ImageItem_Error>(this, imageItem->get_infe_type(), id, err);
     }

     imageItem->set_properties(properties);

     err = imageItem->initialize_decoder();
     if (err) {
-      imageItem = std::make_shared<ImageItem_Error>(imageItem->get_infe_type(), id, err);
+      imageItem = std::make_shared<ImageItem_Error>(this, imageItem->get_infe_type(), id, err);
       imageItem->set_properties(properties);
     } else {
       // The decoder's input data extent must be set before any codec-config
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index f6a1b2a3..1e61f39a 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -183,14 +183,14 @@ std::shared_ptr<ImageItem> ImageItem::alloc_for_infe_box(HeifContext* ctx, const
     std::stringstream sstr;
     sstr << "Image item of type '" << fourcc_to_string(item_type) << "' is not supported.";
     Error err{ heif_error_Unsupported_feature, heif_suberror_Unsupported_image_type, sstr.str() };
-    return std::make_shared<ImageItem_Error>(item_type, id, err);
+    return std::make_shared<ImageItem_Error>(ctx, item_type, id, err);
 #endif
   }
   else if (item_type == fourcc("j2k1")) {
     return std::make_shared<ImageItem_JPEG2000>(ctx, id);
   }
   else if (item_type == fourcc("lhv1")) {
-    return std::make_shared<ImageItem_Error>(item_type, id,
+    return std::make_shared<ImageItem_Error>(ctx, item_type, id,
                                              Error{heif_error_Unsupported_feature,
                                                    heif_suberror_Unsupported_image_type,
                                                    "Layered HEVC images (lhv1) are not supported yet"});
@@ -912,14 +912,6 @@ Error check_decode_reference_cycles(const ImageItem* item,
     return Error::Ok;
   }

-  // An item that failed to parse is a detached error item with no context and
-  // no traversable references. It cannot be part of a cycle, so treat it as a
-  // leaf (and, crucially, do not dereference its null context via get_file()).
-  if (item->get_context() == nullptr) {
-    verified.insert(id);
-    return Error::Ok;
-  }
-
   on_path.insert(id);

   // Follow exactly the edges the decode recursion follows: the derived-image
@@ -1006,12 +998,6 @@ Error check_miaf_derivation_constraints(const ImageItem* item,
     return Error::Ok;  // already verified in this context
   }

-  // A failed-to-parse item is a detached error item (null context) with no
-  // traversable references; treat it as a leaf and do not dereference it.
-  if (item->get_context() == nullptr) {
-    return Error::Ok;
-  }
-
   // Rank budget passed to this item's own 'dimg' inputs.
   int child_max_rank;
   bool child_parent_is_iden;
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 9fd58550..4030ddde 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -545,8 +545,12 @@ class ImageItem_Error : public ImageItem
 public:
   // dummy ImageItem class that is a placeholder for unsupported item types

-  ImageItem_Error(uint32_t item_type, heif_item_id id, Error err)
-    : ImageItem(nullptr, id), m_item_type(item_type), m_item_error(std::move(err)) {}
+  // Carry the real context, like every other ImageItem. Error items used to be
+  // constructed with a null context, which made get_context()/get_file() a
+  // null-deref hazard for any code that reaches an error item (e.g. an error
+  // item attached as a depth/aux image, then handed to verify_decodable()).
+  ImageItem_Error(HeifContext* context, uint32_t item_type, heif_item_id id, Error err)
+    : ImageItem(context, id), m_item_type(item_type), m_item_error(std::move(err)) {}

   uint32_t get_infe_type() const override
   {
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 13d75103..b8562b65 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -88,6 +88,7 @@ add_libheif_test(iden_declared_size)
 add_libheif_test(item_properties)
 add_libheif_test(item_writing)
 add_libheif_test(overlay_amplification)
+add_libheif_test(error_item_decode)
 add_libheif_test(alpha_cycle_deadlock)
 add_libheif_test(parallel_grid_deadlock)
 # The deadlock regression tests decode on a worker thread guarded by a timeout.
diff --git a/tests/error_item_decode.cc b/tests/error_item_decode.cc
new file mode 100644
index 00000000..ce53e5b9
--- /dev/null
+++ b/tests/error_item_decode.cc
@@ -0,0 +1,217 @@
+/*
+  libheif 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.
+*/
+
+// Regression test for a null-pointer dereference reachable from a fuzzer
+// (OSS-Fuzz / CIFuzz). An item whose type libheif cannot handle (here 'lhv1',
+// layered HEVC) is represented internally by an ImageItem_Error placeholder.
+// These placeholders used to be constructed with a null HeifContext, so any
+// code that dereferenced their context crashed. ImageItem::decode_image() gained
+// a verify_decodable() pre-pass that calls get_file() (-> context->get_heif_file())
+// before decoding, so decoding such an item segfaulted.
+//
+// The depth-image handle getter does not reject items with an item-error, so an
+// error item attached as a depth auxiliary can be handed to heif_decode_image()
+// and reach the crash. The fix gives error items their real context, so decoding
+// one returns its error instead of dereferencing null.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "test_utils.h"
+
+#include <cstdint>
+#include <vector>
+
+namespace {
+
+// A minimal file: item 1 is a decodable 16x16 'mski' mask (the primary); item 2
+// is an 'lhv1' item (unsupported -> ImageItem_Error) declared as the depth
+// auxiliary of item 1 via 'auxl' + an 'auxC' depth type.
+std::vector<uint8_t> build_file_with_error_depth_item() {
+  const uint32_t W = 16, H = 16;
+
+  std::vector<uint8_t> ftyp_payload;
+  append_fourcc(ftyp_payload, "heic");
+  put_u32_be(ftyp_payload, 0);
+  append_fourcc(ftyp_payload, "mif1");
+  append_fourcc(ftyp_payload, "heic");
+  auto ftyp = make_box("ftyp", ftyp_payload);
+
+  std::vector<uint8_t> hdlr_payload;
+  put_u32_be(hdlr_payload, 0);
+  append_fourcc(hdlr_payload, "pict");
+  put_u32_be(hdlr_payload, 0);
+  put_u32_be(hdlr_payload, 0);
+  put_u32_be(hdlr_payload, 0);
+  hdlr_payload.push_back(0);
+  auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true);
+
+  std::vector<uint8_t> pitm_payload;
+  put_u16_be(pitm_payload, 1);
+  auto pitm = make_box("pitm", pitm_payload, /*full=*/true);
+
+  std::vector<uint8_t> iinf_payload;
+  put_u16_be(iinf_payload, 2);
+  {
+    std::vector<uint8_t> infe1;
+    put_u16_be(infe1, 1);
+    put_u16_be(infe1, 0);
+    append_fourcc(infe1, "mski");
+    infe1.push_back(0);
+    append(iinf_payload, make_box("infe", infe1, /*full=*/true, /*version=*/2));
+
+    std::vector<uint8_t> infe2;
+    put_u16_be(infe2, 2);
+    put_u16_be(infe2, 0);
+    append_fourcc(infe2, "lhv1");   // unsupported -> ImageItem_Error
+    infe2.push_back(0);
+    append(iinf_payload, make_box("infe", infe2, /*full=*/true, /*version=*/2));
+  }
+  auto iinf = make_box("iinf", iinf_payload, /*full=*/true);
+
+  // ipco: 1=ispe, 2=mskC, 3=auxC(depth)
+  std::vector<uint8_t> ispe_payload;
+  put_u32_be(ispe_payload, W);
+  put_u32_be(ispe_payload, H);
+  auto ispe = make_box("ispe", ispe_payload, /*full=*/true);
+
+  std::vector<uint8_t> mskC_payload;
+  mskC_payload.push_back(8);
+  auto mskC = make_box("mskC", mskC_payload, /*full=*/true);
+
+  std::vector<uint8_t> auxC_payload;
+  append_cstr(auxC_payload, "urn:mpeg:mpegB:cicp:systems:auxiliary:depth");
+  auto auxC = make_box("auxC", auxC_payload, /*full=*/true);
+
+  std::vector<uint8_t> ipco_payload;
+  append(ipco_payload, ispe);
+  append(ipco_payload, mskC);
+  append(ipco_payload, auxC);
+  auto ipco = make_box("ipco", ipco_payload);
+
+  std::vector<uint8_t> ipma_payload;
+  put_u32_be(ipma_payload, 2);
+  put_u16_be(ipma_payload, 1);            // item 1: ispe + mskC
+  ipma_payload.push_back(2);
+  ipma_payload.push_back(0x80 | 1);
+  ipma_payload.push_back(0x80 | 2);
+  put_u16_be(ipma_payload, 2);            // item 2: ispe + auxC(depth)
+  ipma_payload.push_back(2);
+  ipma_payload.push_back(0x80 | 1);
+  ipma_payload.push_back(0x80 | 3);
+  auto ipma = make_box("ipma", ipma_payload, /*full=*/true);
+
+  std::vector<uint8_t> iprp_payload;
+  append(iprp_payload, ipco);
+  append(iprp_payload, ipma);
+  auto iprp = make_box("iprp", iprp_payload);
+
+  std::vector<uint8_t> idat_payload(W * H + 16, 0x7F);   // item 1 mask + item 2 dummy
+  auto idat = make_box("idat", idat_payload);
+
+  std::vector<uint8_t> iloc_payload;
+  iloc_payload.push_back((4 << 4) | 4);
+  iloc_payload.push_back(0);
+  put_u16_be(iloc_payload, 2);
+  // item 1
+  put_u16_be(iloc_payload, 1);
+  put_u16_be(iloc_payload, 0x0001);   // idat
+  put_u16_be(iloc_payload, 0);
+  put_u16_be(iloc_payload, 1);
+  put_u32_be(iloc_payload, 0);
+  put_u32_be(iloc_payload, W * H);
+  // item 2
+  put_u16_be(iloc_payload, 2);
+  put_u16_be(iloc_payload, 0x0001);
+  put_u16_be(iloc_payload, 0);
+  put_u16_be(iloc_payload, 1);
+  put_u32_be(iloc_payload, W * H);
+  put_u32_be(iloc_payload, 16);
+  auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1);
+
+  // iref: 'auxl' from item 2 (aux) to item 1 (master)
+  std::vector<uint8_t> auxl_payload;
+  put_u16_be(auxl_payload, 2);
+  put_u16_be(auxl_payload, 1);
+  put_u16_be(auxl_payload, 1);
+  auto iref = make_box("iref", make_box("auxl", auxl_payload), /*full=*/true);
+
+  std::vector<uint8_t> meta_payload;
+  append(meta_payload, hdlr);
+  append(meta_payload, pitm);
+  append(meta_payload, iinf);
+  append(meta_payload, iprp);
+  append(meta_payload, iloc);
+  append(meta_payload, iref);
+  append(meta_payload, idat);
+  auto meta = make_box("meta", meta_payload, /*full=*/true);
+
+  std::vector<uint8_t> file;
+  append(file, ftyp);
+  append(file, meta);
+  return file;
+}
+
+} // namespace
+
+
+// Decoding an error item (reached here through its depth-image handle) must
+// return that item's error, not crash on a null context.
+TEST_CASE("error item: decoding an unsupported depth aux item does not crash") {
+  auto data = build_file_with_error_depth_item();
+
+  heif_context* ctx = heif_context_alloc();
+  REQUIRE(ctx != nullptr);
+
+  heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle* primary = nullptr;
+  err = heif_context_get_primary_image_handle(ctx, &primary);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(primary != nullptr);
+
+  int num_depth = heif_image_handle_get_number_of_depth_images(primary);
+  REQUIRE(num_depth == 1);
+
+  heif_item_id depth_id = 0;
+  heif_image_handle_get_list_of_depth_image_IDs(primary, &depth_id, 1);
+
+  heif_image_handle* depth_handle = nullptr;
+  err = heif_image_handle_get_depth_image_handle(primary, depth_id, &depth_handle);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(depth_handle != nullptr);
+
+  // The crash was here: decode_image() -> verify_decodable() -> get_file() on a
+  // null context. Reaching the assert at all proves it no longer segfaults.
+  heif_image* img = nullptr;
+  err = heif_decode_image(depth_handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+  REQUIRE(err.code != heif_error_Ok);   // the item's own error is surfaced
+  if (img) { heif_image_release(img); }
+
+  heif_image_handle_release(depth_handle);
+  heif_image_handle_release(primary);
+  heif_context_free(ctx);
+}