Skip to content

Reject item-location boxes that over-allocate memory - #3372

Open
matejsmycka wants to merge 1 commit into
AOMediaCodec:mainfrom
matejsmycka:fix-iloc-extent-count-bound
Open

matejsmycka wants to merge 1 commit into
AOMediaCodec:mainfrom
matejsmycka:fix-iloc-extent-count-bound

Conversation

@matejsmycka

Copy link
Copy Markdown

Problem

avifParseItemLocationBox() reads a 16-bit extent_count per item and pushes that many avifExtent records without checking the count against the bytes remaining in the box. The iloc size fields may be zero, and avifROStreamReadUX8() consumes no input for a zero size factor, so each ~6-byte item can allocate ~1 MiB. This runs inside avifDecoderParse() with no AV1 payload and no codec linked, and none of the decoder limits gate it. The cost is paid while the file is ultimately rejected.

Fix

Before the extent loop, compute the per-extent wire size, reject a non-zero extent_count whose extents would consume no input, and reject any extent_count whose total wire size exceeds the bytes left in the box.

Testing

Built parse-only from a clean checkout, patched vs unpatched, with a harness calling avifDecoderParse() and reporting peak RSS:

Input (12,373-byte iloc, 2048 items x extent_count 65535, zero size fields) Result Peak RSS
unpatched rejected late after allocating 2.11 GB
patched rejected early with a parse error 1.9 MB

Valid AVIFs, including files with genuine multi-byte extents, still parse unchanged.

This is availability-only (no memory corruption, no information disclosure).

Comment thread src/read.c Outdated
AVIF_CHECKERR(avifROStreamReadUX8(&s, &baseOffset, baseOffsetSize), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(base_offset_size*8) base_offset;
uint16_t extentCount;
AVIF_CHECKERR(avifROStreamReadU16(&s, &extentCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) extent_count;
const uint64_t bytesPerExtent = (uint64_t)(((version == 1 || version == 2) ? indexSize : 0)) + offsetSize + lengthSize;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since indexSize is initialized to 0, we can omit the (version == 1 || version == 2) check.

Comment thread src/read.c Outdated
AVIF_CHECKERR(avifROStreamReadUX8(&s, &baseOffset, baseOffsetSize), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(base_offset_size*8) base_offset;
uint16_t extentCount;
AVIF_CHECKERR(avifROStreamReadU16(&s, &extentCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) extent_count;
const uint64_t bytesPerExtent = (uint64_t)(((version == 1 || version == 2) ? indexSize : 0)) + offsetSize + lengthSize;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not necessary to use the uint64_t type for the sum of indexSize, offsetSize, and lengthSize. All three variables are <= 8, so their sum is at most 24.

Comment thread src/read.c
return AVIF_RESULT_BMFF_PARSE_FAILED;
}
#endif
extent->size = (size_t)extentLength;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that we don't handle a zero extentLength correctly. ISO BMFF says that if the value of exteng_length is 0, then the length of the extent is the length of the entire referenced container.

Comment thread src/read.c Outdated
uint16_t extentCount;
AVIF_CHECKERR(avifROStreamReadU16(&s, &extentCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) extent_count;
const uint64_t bytesPerExtent = (uint64_t)(((version == 1 || version == 2) ? indexSize : 0)) + offsetSize + lengthSize;
AVIF_CHECKERR(extentCount == 0 || bytesPerExtent > 0, AVIF_RESULT_BMFF_PARSE_FAILED);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we should also allow bytesPerExtent to be zero when extentCount == 1. Please see my comment at line 2194.

Comment thread src/read.c Outdated
AVIF_CHECKERR(avifROStreamReadU16(&s, &extentCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) extent_count;
const uint64_t bytesPerExtent = (uint64_t)(((version == 1 || version == 2) ? indexSize : 0)) + offsetSize + lengthSize;
AVIF_CHECKERR(extentCount == 0 || bytesPerExtent > 0, AVIF_RESULT_BMFF_PARSE_FAILED);
AVIF_CHECKERR((uint64_t)extentCount * bytesPerExtent <= avifROStreamRemainingBytes(&s), AVIF_RESULT_BMFF_PARSE_FAILED);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since bytesPerExtent is at most 24, it is not necessary to cast extentCount to uint64_t because the usual integer promotion to int is large enough for the product. Or we can cast to uint32_t or size_t (the return type of avifROStreamRemainingBytes().

Also, from a security point of view, this check is less important than the bytesPerExtent > 0 check, because it is the zero bytesPerExtent case that causes us to allocate a disproportionate amount of memory relative to the input size. So we could omit this check.

A crafted file can mint many storage extents from just a few bytes when
the extent size fields are all zero, forcing large allocations before any
image is decoded. Reject an item that declares more than one extent when
each extent consumes no input.
@matejsmycka
matejsmycka force-pushed the fix-iloc-extent-count-bound branch from 64d21c8 to 3aa171c Compare September 19, 2026 13:55
@matejsmycka

Copy link
Copy Markdown
Author

Thanks for the quick review Wan-Teh; feedback added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants