diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 95a81a4b2..d5c8046bd 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -1256,6 +1256,7 @@ impl crate::Session { space: dac_pipeline::OutputSpace, depth: dac_pipeline::OutputDepth, ) -> Result { + dac_pipeline::check_output_size(max_w, max_h)?; let mut job = self.render_job(id, max_w, max_h, false, true).ok_or("no such photo")?; // An export is made from the denoised picture its Denoise amount asks for, made when the job runs if the // background queue has not got to it. (A source kept in memory may lack the picture, so the job reads the file.) diff --git a/crates/engine/src/tests_previews.rs b/crates/engine/src/tests_previews.rs index cbc86ef34..7130077dd 100644 --- a/crates/engine/src/tests_previews.rs +++ b/crates/engine/src/tests_previews.rs @@ -183,3 +183,13 @@ fn pan_at_one_to_one_on_45_megapixels() { ); } } + +/// An absurd export size is refused, not a multiply overflow (it panicked before the fix). +#[test] +fn huge_export_size_is_an_error() { + let mut s = Session::with_demo(); + let id = s.catalog.photos().next().unwrap().id; + let huge = usize::MAX >> 8; + assert!(s.render_export(id, huge, huge, OutputSpace::Srgb, OutputDepth::U8).is_err()); + assert!(s.render_export(id, 70_000, 10, OutputSpace::Srgb, OutputDepth::U8).is_err()); +} diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index ff6e23a30..3e601ae04 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -564,7 +564,10 @@ pub fn render( }; let plan = dac_pipeline::plan(src, info, s, req); let (w, h) = (plan.w, plan.h); - let n = w * h; + let Some(n) = w.checked_mul(h).filter(|n| n.checked_mul(12).is_some()) else { + fail(FailKind::Limit, format!("{w}×{h} output is too large")); + return None; + }; let _limit = match fault { Some(crate::Fault::Limit(bytes)) => Some(crate::ctx::LimitOverride::new(bytes)), _ => None, diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 00b2ccae2..3b9f9be77 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -156,6 +156,21 @@ impl PixelWindow { } } +/// The longest output side a render may be asked for (TIFF/PNG/JPEG all fit within it). +pub const MAX_OUTPUT_SIDE: usize = 65_535; +/// The most output pixels a render may be asked for (the box it fits into, ≈ 1.07 Gpx). +pub const MAX_OUTPUT_PIXELS: usize = 1 << 30; + +/// Rejects an output box past [`MAX_OUTPUT_SIDE`] or [`MAX_OUTPUT_PIXELS`] before anything is +/// sized from it. +pub fn check_output_size(max_w: usize, max_h: usize) -> Result<(), String> { + let pixels = max_w.checked_mul(max_h); + if max_w > MAX_OUTPUT_SIDE || max_h > MAX_OUTPUT_SIDE || pixels.is_none_or(|n| n > MAX_OUTPUT_PIXELS) { + return Err(format!("output size {max_w}×{max_h} is too large (at most {MAX_OUTPUT_SIDE} px per side and {MAX_OUTPUT_PIXELS} pixels)")); + } + Ok(()) +} + impl RenderRequest { pub fn fit(max_w: usize, max_h: usize) -> Self { Self {