Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for resource limits #2085

Merged
merged 28 commits into from
Jan 6, 2024
Merged
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a543a80
Add initial tests for adherence to resolution and resource limits; so…
Shnatsel Jan 4, 2024
4ea84ec
Note absence of tests for animation
Shnatsel Jan 4, 2024
dc72466
Add PNG limit tests
Shnatsel Jan 4, 2024
95ef3c7
Also check allocation limits in the default implementation of set_limits
Shnatsel Jan 4, 2024
fac37ba
Uncomment tests fixed by the allocation check in the blanket set_limi…
Shnatsel Jan 4, 2024
2892d88
Drop an unnecesary import
Shnatsel Jan 4, 2024
c1ae486
Silence deprecation warning to fix CI
Shnatsel Jan 4, 2024
ccdbb36
Improve comment
Shnatsel Jan 4, 2024
2347ebd
Refactor tests to use constants more
Shnatsel Jan 4, 2024
1dac0ce
Add tests verifying that the limits are not overly restrictive
Shnatsel Jan 4, 2024
91672f5
Revert blanket impl for limits - it interacts poorly with the reader,…
Shnatsel Jan 4, 2024
e6c006a
Comment out broken tests
Shnatsel Jan 4, 2024
94d9c12
Add JPEG limits test
Shnatsel Jan 4, 2024
ebd7fa3
Uncomment a test that's actually working
Shnatsel Jan 4, 2024
dc2377b
Reintroduce a comment about animation not being covered
Shnatsel Jan 4, 2024
d142f6e
Fix a rustfmt goof
Shnatsel Jan 4, 2024
38d2a9c
Add resource limit tests for WebP
Shnatsel Jan 4, 2024
30b6222
Move format-specific methods to the end to make them stand out more
Shnatsel Jan 4, 2024
ccc817b
Fix rustfmt goof again
Shnatsel Jan 4, 2024
98c0da3
More newlines for readability
Shnatsel Jan 4, 2024
11125a6
Add a comment on the PNG brokenness
Shnatsel Jan 4, 2024
d951a0a
Add a test for TIFF limits, which have their own interesting brokenness
Shnatsel Jan 4, 2024
0527089
Add a test for AVIF limits
Shnatsel Jan 4, 2024
53755fc
Add a test for BMP limits
Shnatsel Jan 4, 2024
2f63f1c
cargo fmt
Shnatsel Jan 4, 2024
fb6eb40
Cleaner integer type conversions
Shnatsel Jan 4, 2024
128158f
Add a note that the png with_limits may actually be intended behavior
Shnatsel Jan 4, 2024
5c375c8
Fix tests based on PR review feedback
Shnatsel Jan 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions tests/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
//! Test enforcement of size and memory limits for various decoding APIs.
//!
//! We create an image, encode it with a given format, and then decode it with various APIs.
//! We expect each decoding API to return an error because the limits are too low.//!
//! The ones that do not return an error are buggy.
//!
//! There are several such buggy APIs in the crate. The tests for them are written but commented out.
//! Pull requests fixing these APIs are very welcome.
//!
//! It is possible that a maliciously crafted file coud bypass these checks
//! and cause a large allocation inside the decoder despite these limits.
//! These tests cannot catch that, but fuzzing can.
//!
//! These tests also don't cover animation (yet). Adding tests for that would be very welcome too.

use std::io::Cursor;

use image::{
io::Limits, load_from_memory_with_format, ImageDecoder, ImageFormat, ImageOutputFormat,
RgbImage,
};

const WIDTH: u32 = 256;
const HEIGHT: u32 = 256;

fn test_image(format: ImageOutputFormat) -> Vec<u8> {
let image = RgbImage::new(WIDTH, HEIGHT);
let mut bytes: Vec<u8> = Vec::new();
image
.write_to(&mut Cursor::new(&mut bytes), format)
.unwrap();
bytes
}

/// Returns `Limits` with width/height smaller than the test image
fn width_height_limits() -> Limits {
let mut limits = Limits::no_limits();
limits.max_image_width = Some(WIDTH / 2);
limits.max_image_height = Some(HEIGHT / 2);
limits
}

/// Returns `Limits` with allocation limit smaller than the test image
fn allocation_limits() -> Limits {
let mut limits = Limits::no_limits();
limits.max_alloc = Some(WIDTH as u64 / 2 * HEIGHT as u64 / 2 * 3); // matches dimension limits for RGB images
limits
}

/// Returns `Limits` that allow decoding this image without issues
fn permissive_limits() -> Limits {
let mut limits = Limits::no_limits();
limits.max_image_width = Some(WIDTH);
limits.max_image_height = Some(HEIGHT);
limits.max_alloc = Some(WIDTH as u64 * HEIGHT as u64 * 5); // `* 3`` would be an exact fit for RGB; `* 5`` allows some slack space
limits
}

fn load_through_reader(
input: &[u8],
format: ImageFormat,
limits: Limits,
) -> Result<image::DynamicImage, image::ImageError> {
let mut reader = image::io::Reader::new(Cursor::new(input));
reader.set_format(format);
reader.limits(limits);
reader.decode()
}

#[test]
#[cfg(feature = "gif")]
fn gif() {
use image::codecs::gif::GifDecoder;

let image = test_image(ImageOutputFormat::Gif);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Gif).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::Gif, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Gif, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Gif, allocation_limits()).is_err()); // BROKEN!

// GifDecoder
let mut decoder = GifDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = GifDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!

// Custom constructor on GifDecoder
#[allow(deprecated)]
{
assert!(GifDecoder::with_limits(Cursor::new(&image), width_height_limits()).is_err());
//assert!(GifDecoder::with_limits(Cursor::new(&image), allocation_limits()).is_err()); // BROKEN!
}
}

#[test]
#[cfg(feature = "png")]
fn png() {
use image::codecs::png::PngDecoder;

let image = test_image(ImageOutputFormat::Png);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Png).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::Png, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Png, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Png, allocation_limits()).is_err());

// PngDecoder
let mut decoder = PngDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = PngDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!
// The proper fix is known to require an API break: https://github.com/image-rs/image/issues/2084

// Custom constructor on PngDecoder
assert!(PngDecoder::with_limits(Cursor::new(&image), width_height_limits()).is_err());
//assert!(PngDecoder::with_limits(Cursor::new(&image), allocation_limits()).is_err()); // BROKEN!
Shnatsel marked this conversation as resolved.
Show resolved Hide resolved
}

#[test]
#[cfg(feature = "jpeg")]
fn jpeg() {
use image::codecs::jpeg::JpegDecoder;

let image = test_image(ImageOutputFormat::Jpeg(80));
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Jpeg).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::Jpeg, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Jpeg, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Jpeg, allocation_limits()).is_err());

// JpegDecoder
let mut decoder = JpegDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = JpegDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!
}

#[test]
#[cfg(feature = "webp")]
fn webp() {
use image::codecs::webp::WebPDecoder;

let image = test_image(ImageOutputFormat::WebP);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::WebP).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::WebP, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::WebP, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::WebP, allocation_limits()).is_err());

// WebPDecoder
let mut decoder = WebPDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = WebPDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!
}

#[test]
#[cfg(feature = "tiff")]
fn tiff() {
use image::codecs::tiff::TiffDecoder;

let image = test_image(ImageOutputFormat::Tiff);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Tiff).is_ok());
// check that the limits implementation is not overly restrictive
//load_through_reader(&image, ImageFormat::Tiff, permissive_limits()).unwrap(); // BROKEN!
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Tiff, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Tiff, allocation_limits()).is_err());

// TiffDecoder
let mut decoder = TiffDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = TiffDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!
}

#[test]
#[cfg(all(feature = "avif", feature = "avif-decoder"))]
fn avif() {
use image::codecs::avif::AvifDecoder;

let image = test_image(ImageOutputFormat::Avif);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Avif).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::Avif, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Avif, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Avif, allocation_limits()).is_err());

// AvifDecoder
let mut decoder = AvifDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// Not clear if this works or fails, I don't have a recent enough dav1d to run this
// let mut decoder = AvifDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err());
}

#[test]
#[cfg(feature = "bmp")]
fn bmp() {
use image::codecs::bmp::BmpDecoder;

let image = test_image(ImageOutputFormat::Bmp);
// sanity check that our image loads successfully without limits
assert!(load_from_memory_with_format(&image, ImageFormat::Bmp).is_ok());
// check that the limits implementation is not overly restrictive
assert!(load_through_reader(&image, ImageFormat::Bmp, permissive_limits()).is_ok());
// image::io::Reader
assert!(load_through_reader(&image, ImageFormat::Bmp, width_height_limits()).is_err());
assert!(load_through_reader(&image, ImageFormat::Bmp, allocation_limits()).is_err());

// BmpDecoder
let mut decoder = BmpDecoder::new(Cursor::new(&image)).unwrap();
assert!(decoder.set_limits(width_height_limits()).is_err());
// let mut decoder = BmpDecoder::new(Cursor::new(&image)).unwrap();
// assert!(decoder.set_limits(allocation_limits()).is_err()); // BROKEN!
}
Loading