From 592b96ed2eae8cb06abeeb3a4dd87d459e79b176 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 03:19:12 +0700 Subject: [PATCH] =?UTF-8?q?Media:=20vector=20footage=20past=2016384=C3=971?= =?UTF-8?q?6384=20is=20an=20error,=20not=20an=20abort=20(#2136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An SVG or PDF declaring up to 30000×30000 was drawn into one 14 GB image whose failed allocation aborted the app. Decoding now refuses sizes past what Continuously Rasterize draws (16384 a side, shared as MAX_VECTOR_SIDE) with an error naming the size. Co-Authored-By: Claude Opus 5.5 --- crates/media/src/layered.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/media/src/layered.rs b/crates/media/src/layered.rs index 055c3feb0..b259c8b7f 100644 --- a/crates/media/src/layered.rs +++ b/crates/media/src/layered.rs @@ -113,6 +113,11 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) - if is_svg(path, bytes) || is_pdf_like(path, bytes) { let doc = vector_doc(path, bytes, footage.layer.as_ref(), footage.page).unwrap_or(Err(String::new())).map_err(MediaError::Decode)?; let (w, h) = doc.pixel_size(); + // A declared size up to 30000² would be a 14 GB allocation that aborts the app (#2136): + // past what Continuously Rasterize can draw, the footage reports an error instead. + if u64::from(w) * u64::from(h) > u64::from(MAX_VECTOR_SIDE) * u64::from(MAX_VECTOR_SIDE) { + return Err(MediaError::Decode(format!("{path}: {w}×{h} pixels is larger than vector footage can be drawn ({MAX_VECTOR_SIDE}×{MAX_VECTOR_SIDE})"))); + } let img = effectcraft_svg::rasterize(&doc, w, h, 1.0); // The rasteriser produces premultiplied pixels already. let _ = op; @@ -121,10 +126,32 @@ pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) - Ok(None) } +/// The largest side vector footage is drawn at, decoded or continuously rasterised. +const MAX_VECTOR_SIDE: u32 = 16384; + /// A vector file rasterised at `scale` × its pixel size (Continuously Rasterize). pub(crate) fn rasterize_vector(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32, scale: f64) -> Option { let doc = vector_doc(path, bytes, layer, page)?.ok()?; let (w, h) = doc.pixel_size(); - let (sw, sh) = (((w as f64 * scale).ceil() as u32).clamp(1, 16384), ((h as f64 * scale).ceil() as u32).clamp(1, 16384)); + let (sw, sh) = (((w as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE), ((h as f64 * scale).ceil() as u32).clamp(1, MAX_VECTOR_SIDE)); Some(effectcraft_svg::rasterize(&doc, sw, sh, scale)) } + +#[cfg(test)] +mod tests { + use super::*; + + /// #2136: an SVG that declares 30000×30000 asked for a 14 GB image and aborted the app; it + /// is now an error, and a drawable size still decodes. + #[test] + fn vector_footage_past_the_drawable_size_is_an_error_not_an_abort() { + let huge = br#""#; + let footage = probe("huge.svg", huge).unwrap().unwrap(); + let e = decode("huge.svg", huge, &footage, AlphaOp::new(footage.alpha, footage.premul_color)).err().map(|e| e.to_string()).unwrap_or_default(); + assert!(e.contains("30000×30000"), "{e}"); + let small = br#""#; + let footage = probe("small.svg", small).unwrap().unwrap(); + let img = decode("small.svg", small, &footage, AlphaOp::new(footage.alpha, footage.premul_color)).unwrap().unwrap(); + assert_eq!((img.width, img.height), (40, 20)); + } +}