Commit 542ccdfb for libheif

commit 542ccdfbc322cdb5b543e25083e038d17df63728
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 5 01:57:51 2026 +0200

    Return the item's own property position from add_property (GHSA-5w8x-856j-7x7c)

    heif_item_add_*_property() returned the index of the property box in the file-wide
    'ipco' container, while the property getters address the properties of a single item.
    The two numberings only coincide when 'ipco' holds exactly the properties of that one
    item, which is the case for a single image, but not for any file with a second item.
    Reading a property back with the returned id then indexed past the end of the item's
    property list, and the out-of-range check in HeifContext::find_property() built an
    Error object without returning it, so nothing stopped the access.

    Box_ipma::add_property_for_item_ID() now returns the position of the association within
    the item's own property list, which is what the getters subscript and what the
    enumerators heif_item_get_properties_of_type() and
    heif_item_get_transformation_properties() already hand out. This also resolves the
    question in the TODO in ImageItem::add_property(): the id stays correct when the
    property box is deduplicated.

    Also:
    - restore the missing 'return' in HeifContext::find_property()
    - refuse to add properties to a context that was read from a file, like
      HeifContext::write() already does. The item data still lives in the input file, so
      such a context cannot be written out again
    - refuse instead of truncating when a property index does not fit into the 16 bits that
      'ipma' stores, and propagate that error out of add_ispe_property(),
      add_orientation_properties() and set_auxC_property()

    Reported by hackerman70000. The reported scenario (annotating a context read from an
    untrusted file) is not supported API use, so this is not a vulnerability, but the same
    defect is reachable when building a file from scratch: encoding an image with a
    thumbnail and then adding a property to the primary item was enough to crash.

diff --git a/libheif/api/libheif/heif_experimental.cc b/libheif/api/libheif/heif_experimental.cc
index 0dc96df2..a6a5cae1 100644
--- a/libheif/api/libheif/heif_experimental.cc
+++ b/libheif/api/libheif/heif_experimental.cc
@@ -169,10 +169,13 @@ heif_error heif_item_add_property_camera_intrinsic_matrix(const heif_context* co
   auto cmin = std::make_shared<Box_cmin>();
   cmin->set_intrinsic_matrix(matrix->matrix);

-  heif_property_id id = context->context->add_property(itemId, cmin, false);
+  auto id = context->context->add_property(itemId, cmin, false);
+  if (!id) {
+    return id.error_struct(context->context.get());
+  }

   if (out_propertyId) {
-    *out_propertyId = id;
+    *out_propertyId = *id;
   }

   return heif_error_success;
diff --git a/libheif/api/libheif/heif_properties.cc b/libheif/api/libheif/heif_properties.cc
index d43a5d0d..80c92ff8 100644
--- a/libheif/api/libheif/heif_properties.cc
+++ b/libheif/api/libheif/heif_properties.cc
@@ -184,10 +184,13 @@ heif_error heif_item_add_property_user_description(const heif_context* context,
   udes->set_description(description->description ? description->description : "");
   udes->set_tags(description->tags ? description->tags : "");

-  heif_property_id id = context->context->add_property(itemId, udes, false);
+  auto id = context->context->add_property(itemId, udes, false);
+  if (!id) {
+    return id.error_struct(context->context.get());
+  }

   if (out_propertyId) {
-    *out_propertyId = id;
+    *out_propertyId = *id;
   }

   return heif_error_success;
@@ -286,10 +289,13 @@ heif_error heif_item_add_raw_property(const heif_context* context,
   std::vector<uint8_t> data_vector(data, data + size);
   raw_box->set_raw_data(data_vector);

-  heif_property_id id = context->context->add_property(itemId, raw_box, is_essential != 0);
+  auto id = context->context->add_property(itemId, raw_box, is_essential != 0);
+  if (!id) {
+    return id.error_struct(context->context.get());
+  }

   if (out_propertyId) {
-    *out_propertyId = id;
+    *out_propertyId = *id;
   }

   return heif_error_success;
diff --git a/libheif/api/libheif/heif_tai_timestamps.cc b/libheif/api/libheif/heif_tai_timestamps.cc
index b5b4a9a1..37a580d0 100644
--- a/libheif/api/libheif/heif_tai_timestamps.cc
+++ b/libheif/api/libheif/heif_tai_timestamps.cc
@@ -117,10 +117,13 @@ heif_error heif_item_set_property_tai_clock_info(heif_context* ctx,
   auto taic = std::make_shared<Box_taic>();
   taic->set_from_tai_clock_info(clock);

-  heif_property_id id = ctx->context->add_property(itemId, taic, false);
+  auto id = ctx->context->add_property(itemId, taic, false);
+  if (!id) {
+    return id.error_struct(ctx->context.get());
+  }

   if (out_propertyId) {
-    *out_propertyId = id;
+    *out_propertyId = *id;
   }

   return heif_error_success;
@@ -198,10 +201,13 @@ heif_error heif_item_set_property_tai_timestamp(heif_context* ctx,
   auto itai = std::make_shared<Box_itai>();
   itai->set_from_tai_timestamp_packet(timestamp);

-  heif_property_id id = ctx->context->add_property(itemId, itai, false);
+  auto id = ctx->context->add_property(itemId, itai, false);
+  if (!id) {
+    return id.error_struct(ctx->context.get());
+  }

   if (out_propertyId) {
-    *out_propertyId = id;
+    *out_propertyId = *id;
   }

   return heif_error_success;
diff --git a/libheif/box.cc b/libheif/box.cc
index ebb16093..d3bdb580 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -3432,8 +3432,8 @@ bool Box_ipma::is_property_essential_for_item(heif_item_id itemId, int propertyI
 }


-void Box_ipma::add_property_for_item_ID(heif_item_id itemID,
-                                        PropertyAssociation assoc)
+heif_property_id Box_ipma::add_property_for_item_ID(heif_item_id itemID,
+                                                    PropertyAssociation assoc)
 {
   size_t idx;
   for (idx = 0; idx < m_entries.size(); idx++) {
@@ -3452,7 +3452,7 @@ void Box_ipma::add_property_for_item_ID(heif_item_id itemID,
   // If the property is already associated with the item, skip.
   for (auto const& a : m_entries[idx].associations) {
     if (a.property_index == assoc.property_index) {
-      return;
+      return get_property_id_for_item_ID(itemID, assoc.property_index);
     }

     // TODO: should we check that the essential flag matches and return an internal error if not?
@@ -3460,6 +3460,43 @@ void Box_ipma::add_property_for_item_ID(heif_item_id itemID,

   // add the property association
   m_entries[idx].associations.push_back(assoc);
+
+  return get_property_id_for_item_ID(itemID, assoc.property_index);
+}
+
+
+heif_property_id Box_ipma::get_property_id_for_item_ID(heif_item_id itemID, uint16_t property_index) const
+{
+  // Index 0 means "no property". It is skipped by Box_ipco::get_properties_for_item_ID() and
+  // hence has no position in the item's property list.
+  if (property_index == 0) {
+    return 0;
+  }
+
+  for (const auto& entry : m_entries) {
+    if (entry.item_ID != itemID) {
+      continue;
+    }
+
+    // Count the associations the way Box_ipco::get_properties_for_item_ID() does, i.e. skipping
+    // the associations with index 0, so that the returned id indexes the vector it produces.
+    heif_property_id id = 0;
+    for (const auto& assoc : entry.associations) {
+      if (assoc.property_index == 0) {
+        continue;
+      }
+
+      id++;
+
+      if (assoc.property_index == property_index) {
+        return id;
+      }
+    }
+
+    break;
+  }
+
+  return 0;
 }


diff --git a/libheif/box.h b/libheif/box.h
index 30663a06..424aebd1 100644
--- a/libheif/box.h
+++ b/libheif/box.h
@@ -835,8 +835,18 @@ public:

   bool is_property_essential_for_item(heif_item_id itemId, int propertyIndex) const;

-  void add_property_for_item_ID(heif_item_id itemID,
-                                PropertyAssociation assoc);
+  // Associates the property with the item and returns the 1-based position of the property
+  // within this item's property list, i.e. the 'heif_property_id' that the public API uses.
+  // Note that this is not the index of the property in the 'ipco' box, as one 'ipco' is shared
+  // by all items of the file. If the property is already associated with the item, the position
+  // of the existing association is returned.
+  heif_property_id add_property_for_item_ID(heif_item_id itemID,
+                                            PropertyAssociation assoc);
+
+  // Returns the 1-based position of the property with the given 'ipco' index within the item's
+  // property list, or 0 if the property is not associated with the item. This matches the
+  // indices into the vector filled by Box_ipco::get_properties_for_item_ID().
+  heif_property_id get_property_id_for_item_ID(heif_item_id itemID, uint16_t property_index) const;

   void derive_box_version() override;

diff --git a/libheif/context.cc b/libheif/context.cc
index 3b3e1c10..82158753 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1770,7 +1770,9 @@ Result<std::shared_ptr<ImageItem>> HeifContext::encode_image(const std::shared_p
     std::shared_ptr<ImageItem> heif_alpha_image = *alphaEncodingResult;

     m_heif_file->add_iref_reference(heif_alpha_image->get_id(), fourcc("auxl"), {output_image_item->get_id()});
-    m_heif_file->set_auxC_property(heif_alpha_image->get_id(), output_image_item->get_auxC_alpha_channel_type());
+    if (Error err = m_heif_file->set_auxC_property(heif_alpha_image->get_id(), output_image_item->get_auxC_alpha_channel_type())) {
+      return err;
+    }

     if (pixel_image->is_premultiplied_alpha()) {
       m_heif_file->add_iref_reference(output_image_item->get_id(), fourcc("prem"), {heif_alpha_image->get_id()});
@@ -1982,8 +1984,17 @@ Error HeifContext::add_generic_metadata(const std::shared_ptr<ImageItem>& master
 }


-heif_property_id HeifContext::add_property(heif_item_id targetItem, const std::shared_ptr<Box>& property, bool essential)
+Result<heif_property_id> HeifContext::add_property(heif_item_id targetItem, const std::shared_ptr<Box>& property, bool essential)
 {
+  // Like writing, adding properties is only implemented for contexts that were built in memory.
+  // In a context read from a file, the item data still lives in the input file and the context
+  // cannot be written out again (see HeifContext::write()).
+  if (m_heif_file->get_reader()) {
+    return Error(heif_error_Unsupported_feature,
+                 heif_suberror_Unspecified,
+                 "Adding a property to a context that was read from a file is not supported");
+  }
+
   heif_property_id id;

   if (auto img = get_image(targetItem, false)) {
@@ -1993,6 +2004,12 @@ heif_property_id HeifContext::add_property(heif_item_id targetItem, const std::s
     id = m_heif_file->add_property(targetItem, property, essential);
   }

+  if (id == 0) {
+    return Error(heif_error_Encoding_error,
+                 heif_suberror_Unspecified,
+                 "Cannot add property to item");
+  }
+
   return id;
 }

@@ -2105,8 +2122,7 @@ Result<heif_property_id> HeifContext::add_text_property(heif_item_id itemId, con
   auto elng = std::make_shared<Box_elng>();
   elng->set_lang(std::string(language));

-  heif_property_id id = add_property(itemId, elng, false);
-  return id;
+  return add_property(itemId, elng, false);
 }


diff --git a/libheif/context.h b/libheif/context.h
index 8e562be2..0128fc7a 100644
--- a/libheif/context.h
+++ b/libheif/context.h
@@ -175,7 +175,7 @@ public:
                              uint32_t item_type, const char* content_type, const char* item_uri_type,
                              heif_metadata_compression compression, heif_item_id* out_item_id);

-  heif_property_id add_property(heif_item_id targetItem, const std::shared_ptr<Box>& property, bool essential);
+  Result<heif_property_id> add_property(heif_item_id targetItem, const std::shared_ptr<Box>& property, bool essential);

   Result<heif_item_id> add_pyramid_group(const std::vector<heif_item_id>& layers);

@@ -268,7 +268,7 @@ public:
     }

     if (propertyId - 1 >= properties.size()) {
-      Error(heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range");
+      return Error(heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range");
     }

     auto box = properties[propertyId - 1];
diff --git a/libheif/file.cc b/libheif/file.cc
index 02b449c4..5263d8fc 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -61,6 +61,8 @@
 // TODO: make this a decoder option
 #define STRICT_PARSING false

+static const char* const too_many_properties_error = "Too many item properties in file to add another one";
+

 HeifFile::HeifFile()
 {
@@ -1141,7 +1143,7 @@ Result<std::shared_ptr<Box_infe>> HeifFile::add_new_meta_infe_box(uint32_t item_
 }


-void HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential)
+Error HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential)
 {
   init_for_item_properties();

@@ -1150,7 +1152,17 @@ void HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t heigh

   uint32_t index = m_ipco_box->find_or_append_child_box(ispe);

+  // 'ipma' stores the property index as a 16 bit value. Rather than truncating it and writing a
+  // wrong association, refuse to add the property.
+  if (index + 1 > 0xFFFF) {
+    return {heif_error_Encoding_error,
+            heif_suberror_Unspecified,
+            too_many_properties_error};
+  }
+
   m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
+
+  return Error::Ok;
 }


@@ -1161,9 +1173,15 @@ heif_property_id HeifFile::add_property(heif_item_id id, const std::shared_ptr<B

   uint32_t index = m_ipco_box->find_or_append_child_box(property);

-  m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
+  // 'ipma' stores the property index as a 16 bit value. Rather than truncating it and writing a
+  // wrong association, refuse to add the property.
+  if (index + 1 > 0xFFFF) {
+    return 0;
+  }

-  return index + 1;
+  // Note that we have to return the position within the item's property list and not 'index',
+  // as 'index' is the position in the file-wide 'ipco' box, which is shared by all items.
+  return m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
 }


@@ -1172,14 +1190,15 @@ heif_property_id HeifFile::add_property_without_deduplication(heif_item_id id, c
   init_for_item_properties();

   uint32_t index = m_ipco_box->append_child_box(property);
+  if (index + 1 > 0xFFFF) {
+    return 0;
+  }

-  m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
-
-  return index + 1;
+  return m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
 }


-void HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orientation)
+Error HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orientation)
 {
   init_for_item_properties();

@@ -1228,6 +1247,9 @@ void HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orie
     irot->set_rotation_ccw(rotation_ccw);

     uint32_t index = m_ipco_box->find_or_append_child_box(irot);
+    if (index + 1 > 0xFFFF) {
+      return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error};
+    }

     m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)});
   }
@@ -1237,9 +1259,14 @@ void HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orie
     imir->set_mirror_direction(mirror);

     uint32_t index = m_ipco_box->find_or_append_child_box(imir);
+    if (index + 1 > 0xFFFF) {
+      return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error};
+    }

     m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)});
   }
+
+  return Error::Ok;
 }


@@ -1530,7 +1557,7 @@ std::shared_ptr<Box_EntityToGroup> HeifFile::get_entity_group(heif_entity_group_
 }


-void HeifFile::set_auxC_property(heif_item_id id, const std::string& type)
+Error HeifFile::set_auxC_property(heif_item_id id, const std::string& type)
 {
   init_for_item_properties();

@@ -1538,8 +1565,13 @@ void HeifFile::set_auxC_property(heif_item_id id, const std::string& type)
   auxC->set_aux_type(type);

   uint32_t index = m_ipco_box->find_or_append_child_box(auxC);
+  if (index + 1 > 0xFFFF) {
+    return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error};
+  }

   m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)});
+
+  return Error::Ok;
 }

 #if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER)
diff --git a/libheif/file.h b/libheif/file.h
index 7691342f..b7e0c966 100644
--- a/libheif/file.h
+++ b/libheif/file.h
@@ -209,10 +209,10 @@ public:

   Result<std::shared_ptr<Box_infe>> add_new_meta_infe_box(uint32_t item_type);

-  void add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential);
+  Error add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential);

   // set irot/imir according to heif_orientation
-  void add_orientation_properties(heif_item_id id, heif_orientation);
+  Error add_orientation_properties(heif_item_id id, heif_orientation);

   // TODO: can we remove the 'essential' parameter and take this from the box? Or is that depending on the context?
   heif_property_id add_property(heif_item_id id, const std::shared_ptr<Box>& property, bool essential);
@@ -250,7 +250,7 @@ public:

   void add_entity_group_box(const std::shared_ptr<Box>& entity_group_box);

-  void set_auxC_property(heif_item_id id, const std::string& type);
+  Error set_auxC_property(heif_item_id id, const std::string& type);

 #if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER)
   static std::wstring convert_utf8_path_to_utf16(std::string pathutf8);
diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc
index bb05f54a..c7c3a7a5 100644
--- a/libheif/image-items/grid.cc
+++ b/libheif/image-items/grid.cc
@@ -800,7 +800,9 @@ Result<std::shared_ptr<ImageItem_Grid>> ImageItem_Grid::add_new_grid_item(HeifCo
   file->add_iref_reference(grid_id, fourcc("dimg"), tile_ids);

   // Add ISPE property
-  file->add_ispe_property(grid_id, output_width, output_height, false);
+  if (Error err = file->add_ispe_property(grid_id, output_width, output_height, false)) {
+    return err;
+  }

   // PIXI property will be added when the first tile is set

@@ -866,7 +868,9 @@ Error ImageItem_Grid::add_image_tile(uint32_t tile_x, uint32_t tile_y,

     // Add transformative properties

-    get_context()->get_heif_file()->add_orientation_properties(get_id(), m_grid_orientation);
+    if (Error err = get_context()->get_heif_file()->add_orientation_properties(get_id(), m_grid_orientation)) {
+      return err;
+    }
   }

   return Error::Ok;
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index b377e863..5b79f981 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -37,6 +37,7 @@
 #include "plugin_registry.h"
 #include "security_limits.h"

+#include <algorithm>
 #include <limits>
 #include <cassert>
 #include <cstring>
@@ -85,8 +86,13 @@ heif_property_id ImageItem::add_property(const std::shared_ptr<Box>& property, b
     return 0;
   }

-  // TODO: is this correct? What happens when add_property does deduplicate the property?
-  m_properties.push_back(property);
+  // HeifFile::add_property() deduplicates the property box, so only remember it here if the
+  // item does not hold it yet. The returned id is the position in the item's property list and
+  // is correct in both cases.
+  if (std::find(m_properties.begin(), m_properties.end(), property) == m_properties.end()) {
+    m_properties.push_back(property);
+  }
+
   return get_file()->add_property(get_id(), property, essential);
 }

@@ -436,7 +442,9 @@ Error ImageItem::encode_to_item(HeifContext* ctx,
   }

   // TODO: move this into encode_to_bistream_and_boxes()
-  ctx->get_heif_file()->add_orientation_properties(image_id, options.image_orientation);
+  if (Error err = ctx->get_heif_file()->add_orientation_properties(image_id, options.image_orientation)) {
+    return err;
+  }

   return Error::Ok;
 }
diff --git a/libheif/image-items/tiled.cc b/libheif/image-items/tiled.cc
index 936b43b8..3e4e8a40 100644
--- a/libheif/image-items/tiled.cc
+++ b/libheif/image-items/tiled.cc
@@ -936,7 +936,9 @@ Error ImageItem_Tiled::add_image_tile(uint32_t tile_x, uint32_t tile_y,
         break;
     }

-    get_file()->add_orientation_properties(get_id(), m_image_orientation);
+    if (Error err = get_file()->add_orientation_properties(get_id(), m_image_orientation)) {
+      return err;
+    }
   }

   //get_file()->set_brand(encoder->plugin->compression_format,
diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
index ff490252..32e0552d 100644
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -282,7 +282,9 @@ Result<std::shared_ptr<ImageItem_uncompressed>> ImageItem_uncompressed::add_unci

   // Add transformative properties

-  ctx->get_heif_file()->add_orientation_properties(unci_id, encoding_options->image_orientation);
+  if (Error err = ctx->get_heif_file()->add_orientation_properties(unci_id, encoding_options->image_orientation)) {
+    return err;
+  }


   // Create empty image. If we use compression, we append the data piece by piece.
diff --git a/libheif/mini.cc b/libheif/mini.cc
index 27720624..5054751f 100644
--- a/libheif/mini.cc
+++ b/libheif/mini.cc
@@ -1551,9 +1551,13 @@ Error Box_mini::create_expanded_boxes(class HeifFile* file)
   // mini's orientation field uses standard EXIF orientation values 1..8,
   // matching the heif_orientation enum.
   auto orientation = static_cast<heif_orientation>(get_orientation());
-  file->add_orientation_properties(1, orientation);
+  if (Error err = file->add_orientation_properties(1, orientation)) {
+    return err;
+  }
   if (get_alpha_item_data_size() != 0) {
-    file->add_orientation_properties(2, orientation);
+    if (Error err = file->add_orientation_properties(2, orientation)) {
+      return err;
+    }
   }

   auto iloc_box = std::make_shared<Box_iloc>();
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a3c1054b..bb693ad8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -85,6 +85,7 @@ add_libheif_test(entity_groups)
 add_libheif_test(extended_type)
 add_libheif_test(grid_tile_missing)
 add_libheif_test(iden_declared_size)
+add_libheif_test(item_properties)
 add_libheif_test(item_writing)
 add_libheif_test(overlay_amplification)
 add_libheif_test(alpha_cycle_deadlock)
diff --git a/tests/item_properties.cc b/tests/item_properties.cc
new file mode 100644
index 00000000..a164adb4
--- /dev/null
+++ b/tests/item_properties.cc
@@ -0,0 +1,213 @@
+/*
+  libheif integration tests for item property ids
+
+  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.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_items.h"
+#include "libheif/heif_properties.h"
+#include "test_utils.h"
+#include <cstdint>
+#include <vector>
+
+
+// The 'heif_property_id' returned by heif_item_add_*_property() has to be usable with the
+// property getters. The property boxes of all items are stored in a single, file-wide 'ipco'
+// box, while the getters address the properties of one item. As soon as the file holds more
+// properties than the item we add to, the two numberings differ. Using an 'ipco' index as
+// property id then reads past the end of the item's property list.
+
+TEST_CASE("property id round trip with several items") {
+  heif_init(nullptr);
+  heif_context* ctx = heif_context_alloc();
+
+  const std::vector<uint8_t> payload1{'i', 't', 'e', 'm', '1'};
+  const std::vector<uint8_t> payload2{'i', 't', 'e', 'm', '2'};
+
+  heif_item_id item1, item2;
+  heif_error err;
+
+  err = heif_context_add_item(ctx, "mime", payload1.data(), (int) payload1.size(), &item1);
+  REQUIRE(err.code == heif_error_Ok);
+  err = heif_context_add_item(ctx, "mime", payload2.data(), (int) payload2.size(), &item2);
+  REQUIRE(err.code == heif_error_Ok);
+
+  // Add a property to the second item first, so that the 'ipco' box holds a property that the
+  // first item does not associate.
+
+  const std::vector<uint8_t> body2{0x02, 0x02, 0x02, 0x02};
+  heif_property_id propertyId2;
+  err = heif_item_add_raw_property(ctx, item2, heif_fourcc('p', 'r', 'p', '2'), nullptr,
+                                   body2.data(), body2.size(), 0, &propertyId2);
+  REQUIRE(err.code == heif_error_Ok);
+
+  const std::vector<uint8_t> body1{0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
+  heif_property_id propertyId1;
+  err = heif_item_add_raw_property(ctx, item1, heif_fourcc('p', 'r', 'p', '1'), nullptr,
+                                   body1.data(), body1.size(), 0, &propertyId1);
+  REQUIRE(err.code == heif_error_Ok);
+
+  // Both items hold exactly one property, hence both ids have to be 1. Before this was fixed,
+  // 'propertyId1' was 2 (the position in 'ipco'), which read past the end of item1's list.
+
+  REQUIRE(propertyId1 == 1);
+  REQUIRE(propertyId2 == 1);
+
+  // The ids the enumerator hands out have to match the ids we got when adding.
+
+  heif_property_id enumerated[8];
+  int n = heif_item_get_properties_of_type(ctx, item1, heif_item_property_type_invalid, enumerated, 8);
+  REQUIRE(n == 1);
+  REQUIRE(enumerated[0] == propertyId1);
+
+  // Read the properties back through the ids that were returned when adding them.
+
+  size_t size = 0;
+  err = heif_item_get_property_raw_size(ctx, item1, propertyId1, &size);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(size == body1.size());
+
+  std::vector<uint8_t> readback(size);
+  err = heif_item_get_property_raw_data(ctx, item1, propertyId1, readback.data());
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(readback == body1);
+
+  err = heif_item_get_property_raw_size(ctx, item2, propertyId2, &size);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(size == body2.size());
+
+  readback.resize(size);
+  err = heif_item_get_property_raw_data(ctx, item2, propertyId2, readback.data());
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(readback == body2);
+
+  // An id past the end of the item's property list has to be rejected instead of reading
+  // out of bounds.
+
+  err = heif_item_get_property_raw_size(ctx, item1, propertyId1 + 1, &size);
+  REQUIRE(err.code == heif_error_Usage_error);
+
+  heif_context_free(ctx);
+  heif_deinit();
+}
+
+
+// The same divergence occurs for a plain image with a thumbnail: the thumbnail contributes its
+// own codec configuration and 'ispe' to the shared 'ipco' box.
+
+TEST_CASE("property id round trip with thumbnail") {
+  heif_init(nullptr);
+  // Query the encoder before allocating anything, so that a skipped test leaks nothing.
+  heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC);
+  heif_image* input_image = createImage_RGB_planar();
+  heif_context* ctx = heif_context_alloc();
+
+  heif_image_handle* image_handle;
+  heif_error err = heif_context_encode_image(ctx, input_image, encoder, nullptr, &image_handle);
+  UNSCOPED_INFO("heif_context_encode_image: " << err.message);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle* thumbnail_handle = nullptr;
+  err = heif_context_encode_thumbnail(ctx, input_image, image_handle, encoder, nullptr, 16,
+                                      &thumbnail_handle);
+  UNSCOPED_INFO("heif_context_encode_thumbnail: " << err.message);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(thumbnail_handle != nullptr);
+
+  heif_item_id itemId;
+  err = heif_context_get_primary_image_ID(ctx, &itemId);
+  REQUIRE(err.code == heif_error_Ok);
+
+  int n_before = heif_item_get_properties_of_type(ctx, itemId, heif_item_property_type_invalid,
+                                                  nullptr, 0);
+  REQUIRE(n_before > 0);
+
+  const std::vector<uint8_t> body{0xfa, 0xde, 0x99, 0x04};
+  heif_property_id propertyId;
+  err = heif_item_add_raw_property(ctx, itemId, heif_fourcc('p', 'r', 'p', 'x'), nullptr,
+                                   body.data(), body.size(), 0, &propertyId);
+  REQUIRE(err.code == heif_error_Ok);
+
+  // The property was appended to the item's property list, so its id is the new list length.
+  REQUIRE(propertyId == (heif_property_id) (n_before + 1));
+
+  size_t size = 0;
+  err = heif_item_get_property_raw_size(ctx, itemId, propertyId, &size);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(size == body.size());
+
+  std::vector<uint8_t> readback(size);
+  err = heif_item_get_property_raw_data(ctx, itemId, propertyId, readback.data());
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(readback == body);
+
+  heif_image_handle_release(thumbnail_handle);
+  heif_image_handle_release(image_handle);
+  heif_encoder_release(encoder);
+  heif_image_release(input_image);
+  heif_context_free(ctx);
+  heif_deinit();
+}
+
+
+// Adding properties to a context that was read from a file is not supported: the item data
+// still lives in the input file and the context cannot be written out again. This has to be
+// refused rather than half-succeeding.
+
+TEST_CASE("adding a property to a loaded context is refused") {
+  heif_init(nullptr);
+  heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC);
+  heif_context* ctx = heif_context_alloc();
+
+  heif_image* input_image = createImage_RGB_planar();
+  heif_image_handle* image_handle;
+  heif_error err = heif_context_encode_image(ctx, input_image, encoder, nullptr, &image_handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  err = heif_context_write_to_file(ctx, "property_ids.heif");
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle_release(image_handle);
+  heif_image_release(input_image);
+  heif_encoder_release(encoder);
+  heif_context_free(ctx);
+
+  heif_context* read_ctx = heif_context_alloc();
+  err = heif_context_read_from_file(read_ctx, "property_ids.heif", nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_item_id itemId;
+  err = heif_context_get_primary_image_ID(read_ctx, &itemId);
+  REQUIRE(err.code == heif_error_Ok);
+
+  const std::vector<uint8_t> body{0x01, 0x02, 0x03, 0x04};
+  heif_property_id propertyId = 0;
+  err = heif_item_add_raw_property(read_ctx, itemId, heif_fourcc('p', 'r', 'p', 'y'), nullptr,
+                                   body.data(), body.size(), 0, &propertyId);
+  REQUIRE(err.code == heif_error_Unsupported_feature);
+
+  heif_context_free(read_ctx);
+  heif_deinit();
+}