Summary
AvifDecoder::<R>::new() doesn't just parse a header — it feeds the entire AV1 payload to dav1d::Decoder and drains it to completion (read_until_ready), fully decoding the picture (and alpha picture, if present) into YUV plane buffers sized from the bitstream's own header. Only after this full decode does dimensions() become available. Since AvifDecoder never overrides ImageDecoder::set_limits, and ImageReader::decode()'s dispatch (make_decoder) constructs the decoder before ever calling set_limits/reserve, a caller who does everything the API asks for (reader.limits(Limits{max_image_width: Some(N), ..}) then .decode()) gets zero protection against oversized AVIF frames — the memory/CPU cost is already spent by the time the limit check can even see the dimensions.
Details
Confirmed by direct source reading against image 0.25.10 (not a hypothesis — this is the exact current code):
src/codecs/avif/decoder.rs, AvifDecoder::new(): constructs dav1d::Decoder, calls send_data, and calls read_until_ready(&mut primary_decoder) — this blocks until the full picture is decoded — before returning Self { picture, alpha_picture, .. }. dimensions() only reads self.picture.width()/height() — post-decode data.
AvifDecoder's impl ImageDecoder block has no set_limits/total_bytes override, so it falls through to the default trait impl (src/io/decoder.rs), which calls check_dimensions against dimensions() — necessarily after the decode already happened.
src/io/image_reader_type.rs's decode(): make_decoder(...) (which for ImageFormat::Avif is literally avif::AvifDecoder::new(reader)?) is called at line 315, before limits.reserve(decoder.total_bytes()) (line 319) and decoder.set_limits(limits) (line 320).
- The crate's own doc comment on
make_decoder explicitly concedes this: "For all formats except PNG, the limits are ignored and can be set with ImageDecoder::set_limits after calling this function." PNG is special-cased (PngDecoder::with_limits) specifically because its constructor isn't cheap — but AVIF's constructor is by far the most expensive of all ~15 backends (it does the entire pixel decode), and wasn't given the same treatment.
Impact
A caller decoding untrusted .avif files who configures Limits exactly as the API instructs gets no protection against a crafted file with a small compressed payload but a large declared resolution (AV1 permits a single skip-coded/DC-predicted superblock covering an entire large frame) — the CPU cost of the AV1 decode and the memory for the full-resolution YUV planes are spent regardless of the configured limit.
Note: avif-native (the dav1d-backed decode path this affects) is not part of the crate's default feature set — it must be explicitly enabled. Any application decoding untrusted user-supplied .avif files necessarily enables it, at which point this is fully reachable via the crate's own documented, intended API usage.
Suggested direction (not a fix — reporting for maintainer judgment)
Mirror PNG's own pattern: either extract a cheap pre-decode dimension source if the AVIF/HEIF container exposes one (e.g. the ispe image-spatial-extents property box, if not already parsed), and validate against Limits before invoking dav1d; or give AvifDecoder a with_limits-style constructor (matching PngDecoder::with_limits's naming) so make_decoder can thread limits in before construction, updating the ImageFormat::Avif dispatch arm accordingly.
Not submitting a PR — this needs a maintainer's call on whether a cheap pre-decode dimension check exists in the container format, or whether a constructor signature change (with the accompanying non-breaking with_limits sibling, not altering new) is the right shape.
Discovered by the rust-in-peace security pipeline.
Summary
AvifDecoder::<R>::new()doesn't just parse a header — it feeds the entire AV1 payload todav1d::Decoderand drains it to completion (read_until_ready), fully decoding the picture (and alpha picture, if present) into YUV plane buffers sized from the bitstream's own header. Only after this full decode doesdimensions()become available. SinceAvifDecodernever overridesImageDecoder::set_limits, andImageReader::decode()'s dispatch (make_decoder) constructs the decoder before ever callingset_limits/reserve, a caller who does everything the API asks for (reader.limits(Limits{max_image_width: Some(N), ..})then.decode()) gets zero protection against oversized AVIF frames — the memory/CPU cost is already spent by the time the limit check can even see the dimensions.Details
Confirmed by direct source reading against
image0.25.10 (not a hypothesis — this is the exact current code):src/codecs/avif/decoder.rs,AvifDecoder::new(): constructsdav1d::Decoder, callssend_data, and callsread_until_ready(&mut primary_decoder)— this blocks until the full picture is decoded — before returningSelf { picture, alpha_picture, .. }.dimensions()only readsself.picture.width()/height()— post-decode data.AvifDecoder'simpl ImageDecoderblock has noset_limits/total_bytesoverride, so it falls through to the default trait impl (src/io/decoder.rs), which callscheck_dimensionsagainstdimensions()— necessarily after the decode already happened.src/io/image_reader_type.rs'sdecode():make_decoder(...)(which forImageFormat::Avifis literallyavif::AvifDecoder::new(reader)?) is called at line 315, beforelimits.reserve(decoder.total_bytes())(line 319) anddecoder.set_limits(limits)(line 320).make_decoderexplicitly concedes this: "For all formats except PNG, the limits are ignored and can be set withImageDecoder::set_limitsafter calling this function." PNG is special-cased (PngDecoder::with_limits) specifically because its constructor isn't cheap — but AVIF's constructor is by far the most expensive of all ~15 backends (it does the entire pixel decode), and wasn't given the same treatment.Impact
A caller decoding untrusted
.aviffiles who configuresLimitsexactly as the API instructs gets no protection against a crafted file with a small compressed payload but a large declared resolution (AV1 permits a single skip-coded/DC-predicted superblock covering an entire large frame) — the CPU cost of the AV1 decode and the memory for the full-resolution YUV planes are spent regardless of the configured limit.Note:
avif-native(the dav1d-backed decode path this affects) is not part of the crate'sdefaultfeature set — it must be explicitly enabled. Any application decoding untrusted user-supplied.aviffiles necessarily enables it, at which point this is fully reachable via the crate's own documented, intended API usage.Suggested direction (not a fix — reporting for maintainer judgment)
Mirror PNG's own pattern: either extract a cheap pre-decode dimension source if the AVIF/HEIF container exposes one (e.g. the
ispeimage-spatial-extents property box, if not already parsed), and validate againstLimitsbefore invokingdav1d; or giveAvifDecoderawith_limits-style constructor (matchingPngDecoder::with_limits's naming) somake_decodercan thread limits in before construction, updating theImageFormat::Avifdispatch arm accordingly.Not submitting a PR — this needs a maintainer's call on whether a cheap pre-decode dimension check exists in the container format, or whether a constructor signature change (with the accompanying non-breaking
with_limitssibling, not alteringnew) is the right shape.Discovered by the rust-in-peace security pipeline.