From dcabf97e712ddbb1f28e4e14f9f14d7bfe19ac67 Mon Sep 17 00:00:00 2001 From: 17krishna8 <17krishna8@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:39:26 +0000 Subject: [PATCH] Reject image sequences with total sample size exceeding 16x file size avifCodecDecodeInputFillFromSampleTable() materializes one avifDecodeSample per sample declared by the stsc/stsz/stco boxes. Each sample is individually checked against the io sizeHint, but overlapping chunks are not, so a tiny file declaring many chunks at the same offset and the default imageCountLimit can materialize the whole budget of samples: a 4 KiB file parses successfully with imageCount = 2 592 000 and about 125 MiB of samples, before any decoding. Reject the sample table instead when the sum of the sample sizes exceeds 16x the sizeHint, mirroring the equivalent iloc guard for still images. Chunks are disjoint in a well-formed file, so the sum of the sample sizes is at most the file size there; 16x leaves a comfortable margin. Files streamed through a custom avifIO with no sizeHint are unaffected. The new avifsampletabletest builds hand-crafted minimal 'avis' files; OverlappingChunksExceedTotalSizeBudget fails at head (parse returns OK with imageCount = 2 592 000) and passes with this change. --- src/read.c | 16 +++ tests/CMakeLists.txt | 1 + tests/gtest/avifsampletabletest.cc | 220 +++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 tests/gtest/avifsampletabletest.cc diff --git a/src/read.c b/src/read.c index 51f31e407c..b8db6ae21e 100644 --- a/src/read.c +++ b/src/read.c @@ -596,6 +596,12 @@ static avifResult avifCodecDecodeInputFillFromSampleTable(avifCodecDecodeInput * const uint64_t sizeHint, avifDiagnostics * diag) { + // The sum of all sample sizes is bounded by the file size because chunks are disjoint in a + // well-formed file. Allowing anything above that would let a tiny file materialize the whole + // imageCountLimit budget of samples (about 125 MiB with the default limit). + const uint64_t maxTotalSampleSize = (sizeHint > 0 && sizeHint <= UINT64_MAX / 16) ? sizeHint * 16 : 0; // 0 means unlimited + uint64_t totalSampleSize = 0; + if (imageCountLimit) { // Verify that the we're not about to exceed the frame count limit. @@ -643,6 +649,16 @@ static avifResult avifCodecDecodeInputFillFromSampleTable(avifCodecDecodeInput * sampleSize = sampleSizePtr->size; } + if (sampleSize > UINT64_MAX - totalSampleSize) { + avifDiagnosticsPrintf(diag, "Total sample size overflows"); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + totalSampleSize += sampleSize; + if (maxTotalSampleSize != 0 && totalSampleSize > maxTotalSampleSize) { + avifDiagnosticsPrintf(diag, "Total sample size exceeds 16x file size; overlapping chunks are not supported"); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + avifDecodeSample * sample = (avifDecodeSample *)avifArrayPush(&decodeInput->samples); AVIF_CHECKERR(sample != NULL, AVIF_RESULT_OUT_OF_MEMORY); sample->offset = sampleOffset; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a3dd797a9..b49dd6d59e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -129,6 +129,7 @@ if(AVIF_GTEST) add_avif_gtest(avifrgbtest) add_avif_gtest(avifrgbtoyuvtest) add_avif_gtest(avifrgbtoyuvthreadingtest) + add_avif_gtest(avifsampletabletest) add_avif_gtest(avifsampletransformtest) add_avif_gtest_with_data(avifscaletest) add_avif_gtest_with_data(avifsize0test) diff --git a/tests/gtest/avifsampletabletest.cc b/tests/gtest/avifsampletabletest.cc new file mode 100644 index 0000000000..4e0e954dbc --- /dev/null +++ b/tests/gtest/avifsampletabletest.cc @@ -0,0 +1,220 @@ +// Copyright 2026 the libavif contributors +// SPDX-License-Identifier: BSD-2-Clause + +#include +#include +#include + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace avif { +namespace { + +//------------------------------------------------------------------------------ +// Hand-crafted minimal AVIF sequence ('avis') files. + +void Put16(std::vector* b, uint16_t v) { + b->push_back(static_cast(v >> 8)); + b->push_back(static_cast(v)); +} + +void Put32(std::vector* b, uint32_t v) { + b->push_back(static_cast(v >> 24)); + b->push_back(static_cast(v >> 16)); + b->push_back(static_cast(v >> 8)); + b->push_back(static_cast(v)); +} + +void PutType(std::vector* b, const char* type) { + b->insert(b->end(), type, type + 4); +} + +void PutBox(std::vector* b, const char* type, + const std::vector& payload) { + Put32(b, static_cast(8 + payload.size())); + PutType(b, type); + b->insert(b->end(), payload.begin(), payload.end()); +} + +void PutFullBox(std::vector* b, const char* type, + const std::vector& payload) { + Put32(b, static_cast(12 + payload.size())); + PutType(b, type); + Put32(b, 0); // version and flags. + b->insert(b->end(), payload.begin(), payload.end()); +} + +struct SequenceConfig { + uint32_t samplesPerChunk; // Declared by the single 'stsc' entry. + uint32_t allSamplesSize; // Fixed sample size declared by 'stsz'. + uint32_t chunkOffset; // Offset of every 'stco' chunk. + uint32_t numChunks; // Number of 'stco' entries, all at chunkOffset. +}; + +// ftyp(avis) + moov(trak(tkhd, mdia(mdhd, hdlr(pict), minf(stbl(stsd(av01), +// stsc, stsz, stco))))) + mdat. The samples may be overlapping garbage since +// only parsing is exercised, not decoding. +std::vector CreateAvifSequenceFile(const SequenceConfig& config) { + std::vector av1C; + PutBox(&av1C, "av1C", {0x81, 0x04, 0x00, 0x00}); + + std::vector visualSampleEntry; + // reserved[6], data_reference_index, pre_defined, reserved, pre_defined[3]. + for (int i = 0; i < 6; ++i) visualSampleEntry.push_back(0); + Put16(&visualSampleEntry, 1); + Put16(&visualSampleEntry, 0); + Put16(&visualSampleEntry, 0); + for (int i = 0; i < 6; ++i) Put16(&visualSampleEntry, 0); + Put16(&visualSampleEntry, 64); // width + Put16(&visualSampleEntry, 64); // height + Put32(&visualSampleEntry, 0x00480000); // horizresolution + Put32(&visualSampleEntry, 0x00480000); // vertresolution + Put32(&visualSampleEntry, 0); // reserved + Put16(&visualSampleEntry, 1); // frame_count + for (int i = 0; i < 32; ++i) + visualSampleEntry.push_back(0); // compressorname + Put16(&visualSampleEntry, 0x0018); // depth + Put16(&visualSampleEntry, 0xffff); // pre_defined + visualSampleEntry.insert(visualSampleEntry.end(), av1C.begin(), av1C.end()); + + std::vector stsd; + Put32(&stsd, 1); // entry_count + PutBox(&stsd, "av01", visualSampleEntry); + + std::vector stsc; + Put32(&stsc, 1); // entry_count + Put32(&stsc, 1); // first_chunk + Put32(&stsc, config.samplesPerChunk); + Put32(&stsc, 1); // sample_description_index + + std::vector stsz; + Put32(&stsz, config.allSamplesSize); // sample_size (0 if per-sample table) + Put32(&stsz, 0); // sample_count when all samples have the same size + + std::vector stco; + Put32(&stco, config.numChunks); + for (uint32_t i = 0; i < config.numChunks; ++i) + Put32(&stco, config.chunkOffset); + + std::vector stbl; + PutFullBox(&stbl, "stsd", stsd); + PutFullBox(&stbl, "stsc", stsc); + PutFullBox(&stbl, "stsz", stsz); + PutFullBox(&stbl, "stco", stco); + + std::vector minf; + PutBox(&minf, "stbl", stbl); + + std::vector mdhd; + Put32(&mdhd, 0); // creation_time + Put32(&mdhd, 0); // modification_time + Put32(&mdhd, 1000); // timescale + Put32(&mdhd, 500); // duration + Put16(&mdhd, 0x55C4); // language 'und' + Put16(&mdhd, 0); // pre_defined + + std::vector hdlr; + Put32(&hdlr, 0); // pre_defined + PutType(&hdlr, "pict"); + for (int i = 0; i < 3; ++i) Put32(&hdlr, 0); // reserved + hdlr.push_back(0); // name + + std::vector mdia; + PutFullBox(&mdia, "mdhd", mdhd); + PutFullBox(&mdia, "hdlr", hdlr); + PutBox(&mdia, "minf", minf); + + std::vector tkhd; + Put32(&tkhd, 0); // creation_time + Put32(&tkhd, 0); // modification_time + Put32(&tkhd, 1); // track_ID + Put32(&tkhd, 0); // reserved + Put32(&tkhd, 500); // duration + for (int i = 0; i < 2; ++i) Put32(&tkhd, 0); // reserved + Put16(&tkhd, 0); // layer + Put16(&tkhd, 0); // alternate_group + Put16(&tkhd, 0x0100); // volume + Put16(&tkhd, 0); // reserved + for (int i = 0; i < 9; ++i) + Put32(&tkhd, (i == 0 || i == 4 || i == 8) ? 0x00010000u : 0u); // matrix + Put32(&tkhd, 64u << 16); // width + Put32(&tkhd, 64u << 16); // height + + std::vector trak; + PutFullBox(&trak, "tkhd", tkhd); + PutBox(&trak, "mdia", mdia); + + std::vector moov; + PutBox(&moov, "trak", trak); + + std::vector ftyp; + PutType(&ftyp, "avis"); + Put32(&ftyp, 0); + PutType(&ftyp, "avis"); + + std::vector file; + PutBox(&file, "ftyp", ftyp); + PutBox(&file, "moov", moov); + Put32(&file, 12); + PutType(&file, "mdat"); + Put32(&file, 0); + Put32(&file, 0); + Put32(&file, 0); + return file; +} + +avifResult ParseSequence(const std::vector& file, + uint32_t* imageCount) { + DecoderPtr decoder(avifDecoderCreate()); + if (decoder == nullptr) return AVIF_RESULT_UNKNOWN_ERROR; + if (imageCount != nullptr) *imageCount = 0; + const avifResult result = + avifDecoderSetIOMemory(decoder.get(), file.data(), file.size()); + if (result != AVIF_RESULT_OK) return result; + const avifResult parseResult = avifDecoderParse(decoder.get()); + if (imageCount != nullptr) *imageCount = decoder->imageCount; + return parseResult; +} + +//------------------------------------------------------------------------------ + +// Control: a well-formed sequence parses fine, and stays fine after the +// resource bounds below are added. +TEST(SampleTableTest, DisjointChunksParse) { + if (!testutil::Av1DecoderAvailable()) { + GTEST_SKIP() << "AV1 Codec unavailable, skip test."; + } + const std::vector file = + CreateAvifSequenceFile({.samplesPerChunk = 1, + .allSamplesSize = 1, + .chunkOffset = 0, + .numChunks = 2}); + uint32_t imageCount = 0; + EXPECT_EQ(ParseSequence(file, &imageCount), AVIF_RESULT_OK); + EXPECT_EQ(imageCount, 2u); +} + +// All chunks overlap at offset 0, so a 4 KiB file can declare the whole +// imageCountLimit budget of samples. The sum of the sample sizes must be +// rejected as exceeding what the file can hold, instead of materializing +// ~125 MiB of samples (about 2 592 000 of them with the default +// imageCountLimit) from a tiny crafted file. +TEST(SampleTableTest, OverlappingChunksExceedTotalSizeBudget) { + if (!testutil::Av1DecoderAvailable()) { + GTEST_SKIP() << "AV1 Codec unavailable, skip test."; + } + const std::vector file = + CreateAvifSequenceFile({.samplesPerChunk = 2592, + .allSamplesSize = 1, + .chunkOffset = 0, + .numChunks = 1000}); + EXPECT_LT(file.size(), size_t{16 * 1024}); + EXPECT_EQ(ParseSequence(file, nullptr), AVIF_RESULT_NOT_IMPLEMENTED); +} + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace avif