Never crash: an oversize export size is an error, not a multiply overflow

render_export / export_job now refuse an output box past 65535 px per side or
2^30 pixels (lightcraft_pipeline::check_output_size) before anything is sized
from it, and the GPU render checks w*h (and its 12-byte buffers) with
checked_mul, failing with a Limit error instead of overflowing.

Sources: own design.

(cherry picked from commit e0b279ad8e)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrew Frahn
2026-10-26 11:22:28 +11:00
co-authored by Claude Opus 5.5
parent 387bed199f
commit 0b38101924
4 changed files with 31 additions and 1 deletions
+1
View File
@@ -1675,6 +1675,7 @@ impl crate::Session {
space: lightcraft_pipeline::OutputSpace,
depth: lightcraft_pipeline::OutputDepth,
) -> Result<RenderJob, String> {
lightcraft_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 renders the full image: never a thumbnail's source developed from a smaller copy of the raw
// image that the file keeps (Lightroom's merged DNGs)
+11
View File
@@ -1203,3 +1203,14 @@ fn same_folder_as_original_refuses_photos_without_a_folder() {
assert!(e.contains("folder is missing") && e.contains(&gone), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
/// An absurd export size is refused, not a multiply overflow (it panicked before the fix).
#[test]
fn huge_export_size_is_an_error() {
use lightcraft_pipeline::{OutputDepth, OutputSpace};
let mut s = Session::with_demo();
let id = s.active().unwrap();
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());
}
+4 -1
View File
@@ -599,7 +599,10 @@ pub fn render(
};
let plan = lightcraft_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,
+15
View File
@@ -173,6 +173,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 {