From 96b8cee8fef5893e49dc1c79a654b962fcbe2be8 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 6 Jan 2024 21:35:26 +0000 Subject: [PATCH] Add tests for resource limits (#2085) --- tests/limits.rs | 229 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 tests/limits.rs diff --git a/tests/limits.rs b/tests/limits.rs new file mode 100644 index 0000000000..924ecab57b --- /dev/null +++ b/tests/limits.rs @@ -0,0 +1,229 @@ +//! 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 { + let image = RgbImage::new(WIDTH, HEIGHT); + let mut bytes: Vec = 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 / 2) * (HEIGHT / 2) * 3).into()); // 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 * HEIGHT * 5).into()); // `* 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 { + 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()); + // no tests for allocation limits because the caller is responsible for allocating the buffer in this case + + // Custom constructor on GifDecoder + #[allow(deprecated)] + { + assert!(GifDecoder::with_limits(Cursor::new(&image), width_height_limits()).is_err()); + // no tests for allocation limits because the caller is responsible for allocating the buffer in this case + } +} + +#[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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. + // Unlike many others, the `png` crate does natively support memory limits for auxiliary buffers, + // but they are not passed down from `set_limits` - only from the `with_limits` constructor. + // 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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +} + +#[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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +} + +#[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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +} + +#[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 + // The TIFF decoder is special - the `image` crate API does not play well with `tiff` crate API, + // so there is a copy from the buffer allocated by `tiff` to a buffer allocated by `image`. + // This results in memory usage overhead the size of the output buffer. + let mut tiff_permissive_limits = permissive_limits(); + tiff_permissive_limits.max_alloc = Some((WIDTH * HEIGHT * 8).into()); // `* 6` would be exactly two output buffers, `* 8`` has some slack space + load_through_reader(&image, ImageFormat::Tiff, tiff_permissive_limits).unwrap(); + + // 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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +} + +#[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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +} + +#[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()); + // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. +}