Skip to content

AvifDecoder::new() fully decodes the picture via dav1d before Limits are ever applied — configured limits give zero protection #3076

Description

@scadastrangelove

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions