Commit 72a2684e for libheif
commit 72a2684e19b530aca56029def250db31516b8c34
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Sep 21 18:14:02 2026 +0200
Never pass a NULL pointer to memcpy(), even for zero-length copies
An empty std::vector has a NULL data() pointer. Reading a box with an
empty payload, an item whose iloc extent has length zero, or copying an
empty ICC profile or mask therefore called memcpy() with a NULL argument
and a size of zero. This is undefined behaviour in C17 and C++ and is
reported by UBSan's nonnull-attribute check on glibc's memcpy()
declaration. C2y (WG14 N3322) makes zero-length operations on NULL
pointers well-defined, but we cannot rely on that for years.
Guard the memcpy() calls at the memory-backed StreamReader (the sink
for all box parsers), the StreamWriter, the C-API reader, and the API
entry points that copy possibly-empty vectors. The HEVC Annex-B splitter
now also skips empty NAL units, which additionally indexed an empty
vector.
Reported by @iceray00 (iceray-Li) in GHSA-2764-mqj2-c458
(StreamReader_memory::read) and GHSA-x8qp-vqp7-mm4r
(heif_item_get_item_data).
diff --git a/libheif/api/libheif/heif_color.cc b/libheif/api/libheif/heif_color.cc
index 37ad2854..684da4b1 100644
--- a/libheif/api/libheif/heif_color.cc
+++ b/libheif/api/libheif/heif_color.cc
@@ -139,9 +139,10 @@ heif_error heif_image_handle_get_raw_color_profile(const heif_image_handle* hand
auto raw_profile = handle->image->get_color_profile_icc();
if (raw_profile) {
- memcpy(out_data,
- raw_profile->get_data().data(),
- raw_profile->get_data().size());
+ const auto& profile_data = raw_profile->get_data();
+ if (!profile_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(out_data, profile_data.data(), profile_data.size());
+ }
}
else {
Error err(heif_error_Color_profile_does_not_exist,
@@ -347,9 +348,10 @@ heif_error heif_image_get_raw_color_profile(const heif_image* image,
auto raw_profile = image->image->get_color_profile_icc();
if (raw_profile) {
- memcpy(out_data,
- raw_profile->get_data().data(),
- raw_profile->get_data().size());
+ const auto& profile_data = raw_profile->get_data();
+ if (!profile_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(out_data, profile_data.data(), profile_data.size());
+ }
}
else {
Error err(heif_error_Color_profile_does_not_exist,
diff --git a/libheif/api/libheif/heif_items.cc b/libheif/api/libheif/heif_items.cc
index 113d8d72..1ae03ce6 100644
--- a/libheif/api/libheif/heif_items.cc
+++ b/libheif/api/libheif/heif_items.cc
@@ -167,7 +167,9 @@ heif_error heif_item_get_item_data(const heif_context* ctx,
if (out_data) {
*out_data = new uint8_t[dataResult->size()];
- memcpy(*out_data, dataResult->data(), dataResult->size());
+ if (!dataResult->empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(*out_data, dataResult->data(), dataResult->size());
+ }
}
return heif_error_success;
diff --git a/libheif/api/libheif/heif_regions.cc b/libheif/api/libheif/heif_regions.cc
index ee550504..54d582c1 100644
--- a/libheif/api/libheif/heif_regions.cc
+++ b/libheif/api/libheif/heif_regions.cc
@@ -748,7 +748,9 @@ heif_error heif_region_get_inline_mask_data(const heif_region* region,
*y = mask->y;
*width = mask->width;
*height = mask->height;
- memcpy(data, mask->mask_data.data(), mask->mask_data.size());
+ if (!mask->mask_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(data, mask->mask_data.data(), mask->mask_data.size());
+ }
return heif_error_success;
}
return heif_error_invalid_parameter_value;
diff --git a/libheif/bitstream.cc b/libheif/bitstream.cc
index 57d617af..ec6a02ea 100644
--- a/libheif/bitstream.cc
+++ b/libheif/bitstream.cc
@@ -79,7 +79,9 @@ StreamReader_memory::StreamReader_memory(const uint8_t* data, size_t size, bool
{
if (copy) {
m_owned_data = new uint8_t[m_length];
- memcpy(m_owned_data, data, size);
+ if (size > 0) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see read()
+ memcpy(m_owned_data, data, size);
+ }
m_data = m_owned_data;
}
@@ -112,7 +114,14 @@ bool StreamReader_memory::read(void* data, size_t size)
return false;
}
- memcpy(data, &m_data[m_position], size);
+ // Do not call memcpy() with a NULL pointer, even when size == 0. 'data' is NULL when the
+ // caller reads into an empty std::vector (e.g. a box with an empty payload), and passing
+ // NULL to memcpy() is undefined behaviour in C17 and C++. It trips UBSan's nonnull-attribute
+ // check on glibc's memcpy() declaration. C2y (WG14 N3322) makes zero-length operations on
+ // NULL pointers well-defined, but we cannot rely on that for many years.
+ if (size > 0) {
+ memcpy(data, &m_data[m_position], size);
+ }
m_position += size;
return true;
@@ -1053,7 +1062,9 @@ void StreamWriter::write(const std::vector<uint8_t>& vec)
m_data.resize(required_size);
}
- memcpy(m_data.data() + m_position, vec.data(), vec.size());
+ if (!vec.empty()) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(m_data.data() + m_position, vec.data(), vec.size());
+ }
m_position += vec.size();
}
@@ -1068,7 +1079,9 @@ void StreamWriter::write(const StreamWriter& writer)
const auto& data = writer.get_data();
- memcpy(m_data.data() + m_position, data.data(), data.size());
+ if (!data.empty()) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(m_data.data() + m_position, data.data(), data.size());
+ }
m_position += data.size();
}
diff --git a/libheif/bitstream.h b/libheif/bitstream.h
index a56ebf9d..d0a12fe2 100644
--- a/libheif/bitstream.h
+++ b/libheif/bitstream.h
@@ -159,7 +159,16 @@ public:
StreamReader::grow_status wait_for_file_size(uint64_t target_size) override;
- bool read(void* data, size_t size) override { return !m_func_table->read(data, size, m_userdata); }
+ bool read(void* data, size_t size) override
+ {
+ if (size == 0) {
+ // Do not hand a NULL buffer (an empty std::vector) to the user's read callback; it may pass
+ // it on to memcpy(), which is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read().
+ return true;
+ }
+
+ return !m_func_table->read(data, size, m_userdata);
+ }
bool seek(uint64_t position) override { return !m_func_table->seek(position, m_userdata); }
diff --git a/libheif/codecs/hevc_boxes.cc b/libheif/codecs/hevc_boxes.cc
index 2af42e7a..1658ac15 100644
--- a/libheif/codecs/hevc_boxes.cc
+++ b/libheif/codecs/hevc_boxes.cc
@@ -312,7 +312,9 @@ void Box_hvcC::append_nal_data(const uint8_t* data, size_t size)
{
std::vector<uint8_t> nal;
nal.resize(size);
- memcpy(nal.data(), data, size);
+ if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(nal.data(), data, size);
+ }
append_nal_data(nal);
}
diff --git a/libheif/codecs/jpeg_enc.cc b/libheif/codecs/jpeg_enc.cc
index 98741ec3..ea397d97 100644
--- a/libheif/codecs/jpeg_enc.cc
+++ b/libheif/codecs/jpeg_enc.cc
@@ -79,9 +79,11 @@ Result<Encoder::CodedImageData> Encoder_JPEG::encode(const std::shared_ptr<HeifP
break;
}
- size_t oldsize = vec.size();
- vec.resize(oldsize + size);
- memcpy(vec.data() + oldsize, data, size);
+ if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ size_t oldsize = vec.size();
+ vec.resize(oldsize + size);
+ memcpy(vec.data() + oldsize, data, size);
+ }
}
#if 0
diff --git a/libheif/codecs/vvc_boxes.cc b/libheif/codecs/vvc_boxes.cc
index 96f16871..5c042c20 100644
--- a/libheif/codecs/vvc_boxes.cc
+++ b/libheif/codecs/vvc_boxes.cc
@@ -230,7 +230,9 @@ void Box_vvcC::append_nal_data(const uint8_t* data, size_t size)
{
std::vector<uint8_t> nal;
nal.resize(size);
- memcpy(nal.data(), data, size);
+ if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ memcpy(nal.data(), data, size);
+ }
append_nal_data(nal);
}
diff --git a/libheif/context.cc b/libheif/context.cc
index f433606a..04e6566b 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1982,8 +1982,10 @@ Error HeifContext::add_generic_metadata(const std::shared_ptr<ImageItem>& master
else {
// uncompressed data, plain copy
- data_array.resize(size);
- memcpy(data_array.data(), data, size);
+ if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read()
+ data_array.resize(size);
+ memcpy(data_array.data(), data, size);
+ }
}
// copy the data into the file, store the pointer to it in an iloc box entry
diff --git a/libheif/image-items/hevc.cc b/libheif/image-items/hevc.cc
index 104020bd..606df83c 100644
--- a/libheif/image-items/hevc.cc
+++ b/libheif/image-items/hevc.cc
@@ -169,36 +169,41 @@ void ImageItem_HEVC::set_preencoded_hevc_image(const std::vector<uint8_t>& data)
first = false;
}
else {
- std::vector<uint8_t> nal_data;
- size_t length = start_code_start - (prev_start_code_start + 3);
-
- nal_data.resize(length);
-
assert(prev_start_code_start >= 0);
- memcpy(nal_data.data(), data.data() + prev_start_code_start + 3, length);
-
- int nal_type = (nal_data[0] >> 1);
-
- switch (nal_type) {
- case 0x20:
- case 0x21:
- case 0x22:
- hvcC->append_nal_data(nal_data);
- break;
-
- default: {
- std::vector<uint8_t> nal_data_with_size;
- nal_data_with_size.resize(nal_data.size() + 4);
-
- memcpy(nal_data_with_size.data() + 4, nal_data.data(), nal_data.size());
- nal_data_with_size[0] = ((nal_data.size() >> 24) & 0xFF);
- nal_data_with_size[1] = ((nal_data.size() >> 16) & 0xFF);
- nal_data_with_size[2] = ((nal_data.size() >> 8) & 0xFF);
- nal_data_with_size[3] = ((nal_data.size() >> 0) & 0xFF);
+ size_t length = start_code_start - (prev_start_code_start + 3);
- get_file()->append_iloc_data(get_id(), nal_data_with_size, 0);
+ // Skip empty NAL units (two consecutive start codes). Besides being invalid, an empty
+ // NAL unit would read nal_data[0] of an empty vector and pass NULL to memcpy(), which
+ // is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read().
+ if (length > 0) {
+ std::vector<uint8_t> nal_data;
+ nal_data.resize(length);
+
+ memcpy(nal_data.data(), data.data() + prev_start_code_start + 3, length);
+
+ int nal_type = (nal_data[0] >> 1);
+
+ switch (nal_type) {
+ case 0x20:
+ case 0x21:
+ case 0x22:
+ hvcC->append_nal_data(nal_data);
+ break;
+
+ default: {
+ std::vector<uint8_t> nal_data_with_size;
+ nal_data_with_size.resize(nal_data.size() + 4);
+
+ memcpy(nal_data_with_size.data() + 4, nal_data.data(), nal_data.size());
+ nal_data_with_size[0] = ((nal_data.size() >> 24) & 0xFF);
+ nal_data_with_size[1] = ((nal_data.size() >> 16) & 0xFF);
+ nal_data_with_size[2] = ((nal_data.size() >> 8) & 0xFF);
+ nal_data_with_size[3] = ((nal_data.size() >> 0) & 0xFF);
+
+ get_file()->append_iloc_data(get_id(), nal_data_with_size, 0);
+ }
+ break;
}
- break;
}
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ed7941f5..fdfa748f 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -95,6 +95,7 @@ add_libheif_test(encode)
add_libheif_test(encode_grid)
add_libheif_test(entity_groups)
add_libheif_test(extended_type)
+add_libheif_test(zero_length_memcpy)
add_libheif_test(grid_tile_missing)
add_libheif_test(iden_declared_size)
add_libheif_test(inband_coded_size_limit)
diff --git a/tests/zero_length_memcpy.cc b/tests/zero_length_memcpy.cc
new file mode 100644
index 00000000..ad21c257
--- /dev/null
+++ b/tests/zero_length_memcpy.cc
@@ -0,0 +1,144 @@
+/*
+ libheif regression tests for zero-length reads and copies into empty buffers
+ (GHSA-2764-mqj2-c458).
+
+ 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.
+*/
+
+// A box with an empty payload is read into an empty std::vector, whose data() pointer is
+// NULL. The same happens for an item whose iloc extent has length zero. Passing that NULL
+// pointer to memcpy() is undefined behaviour (until C2y, WG14 N3322) and is reported by
+// UBSan's nonnull-attribute check. These tests only detect a regression when they are run
+// in a build with -fsanitize=undefined; in a plain build they merely exercise the paths.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_items.h"
+#include "test_utils.h"
+
+#include <cstdint>
+#include <vector>
+
+namespace {
+
+// ftyp + meta, where meta holds an unknown box with an empty payload (parsed as Box_other)
+// and one item whose single iloc extent has length zero.
+std::vector<uint8_t> build_heif_with_empty_box_and_empty_item()
+{
+ std::vector<uint8_t> ftyp_payload;
+ append_fourcc(ftyp_payload, "mif1");
+ put_u32_be(ftyp_payload, 0);
+ append_fourcc(ftyp_payload, "mif1");
+ auto ftyp = make_box("ftyp", ftyp_payload);
+
+ // hdlr: handler type 'null' so that no image items are required.
+ std::vector<uint8_t> hdlr_payload;
+ put_u32_be(hdlr_payload, 0); // pre_defined
+ append_fourcc(hdlr_payload, "null"); // handler_type
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ hdlr_payload.push_back(0); // name
+ auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true);
+
+ // infe (version 2): item 1, unprotected, item type 'test', empty name
+ std::vector<uint8_t> infe_payload;
+ put_u16_be(infe_payload, 1);
+ put_u16_be(infe_payload, 0);
+ append_fourcc(infe_payload, "test");
+ infe_payload.push_back(0);
+ auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2);
+
+ std::vector<uint8_t> iinf_payload;
+ put_u16_be(iinf_payload, 1);
+ append(iinf_payload, infe);
+ auto iinf = make_box("iinf", iinf_payload, /*full=*/true);
+
+ // iloc (version 0): offset_size=4, length_size=4, base_offset_size=0,
+ // one item with a single extent of length 0
+ std::vector<uint8_t> iloc_payload;
+ iloc_payload.push_back(0x44);
+ iloc_payload.push_back(0x00);
+ put_u16_be(iloc_payload, 1); // item_count
+ put_u16_be(iloc_payload, 1); // item_ID
+ put_u16_be(iloc_payload, 0); // data_reference_index
+ put_u16_be(iloc_payload, 1); // extent_count
+ put_u32_be(iloc_payload, 0); // extent_offset
+ put_u32_be(iloc_payload, 0); // extent_length
+ auto iloc = make_box("iloc", iloc_payload, /*full=*/true);
+
+ // Unknown box type with a header only. Box_other::parse() reads its zero-length payload.
+ auto empty_box = make_box("zzzz", {});
+
+ std::vector<uint8_t> meta_payload;
+ append(meta_payload, hdlr);
+ append(meta_payload, iinf);
+ append(meta_payload, iloc);
+ append(meta_payload, empty_box);
+ auto meta = make_box("meta", meta_payload, /*full=*/true);
+
+ std::vector<uint8_t> file;
+ append(file, ftyp);
+ append(file, meta);
+ return file;
+}
+
+} // namespace
+
+
+TEST_CASE("zero-length box payload and item data do not pass NULL to memcpy") {
+ auto data = build_heif_with_empty_box_and_empty_item();
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ // Read with a copy so that the memory-backed StreamReader is used for both the
+ // input copy and every box payload read.
+ heif_error err = heif_context_read_from_memory(ctx, data.data(), data.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+
+ REQUIRE(heif_context_get_number_of_items(ctx) == 1);
+
+ uint8_t* item_data = nullptr;
+ size_t item_data_size = 123;
+ err = heif_item_get_item_data(ctx, 1, nullptr, &item_data, &item_data_size);
+ REQUIRE(err.code == heif_error_Ok);
+ CHECK(item_data_size == 0);
+ CHECK(item_data != nullptr);
+ heif_release_item_data(ctx, &item_data);
+
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("reading an empty memory buffer does not pass NULL to memcpy") {
+ std::vector<uint8_t> empty;
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ heif_error err = heif_context_read_from_memory(ctx, empty.data(), 0, nullptr);
+ CHECK(err.code != heif_error_Ok);
+
+ heif_context_free(ctx);
+}