-
Notifications
You must be signed in to change notification settings - Fork 623
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for resource limits (#2085)
- Loading branch information
Showing
1 changed file
with
229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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 / 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<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()); | ||
// 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. | ||
} |