diff --git a/crates/eps/src/import/data/filters.rs b/crates/eps/src/import/data/filters.rs index e3268b240..79dce67ea 100644 --- a/crates/eps/src/import/data/filters.rs +++ b/crates/eps/src/import/data/filters.rs @@ -131,6 +131,10 @@ fn lzw(raw: &[u8]) -> Res<(Vec, usize)> { } } +/// Most pixels a DCTDecode JPEG may have: twice the most an image is read with, so every image that +/// was read before is still read. +const MAX_DCT_PIXELS: u64 = 1 << 27; + /// A JPEG's pixels (gray, RGB or CMYK, 8 bits) with its channel count, and how many bytes of /// `raw` it took. A CMYK JPEG gives its samples as stored, for the image's Decode array to read /// (Adobe's CMYK JPEGs store them inverted, which a Decode array of `[1 0 1 0 1 0 1 0]` undoes). @@ -143,6 +147,12 @@ fn dct(raw: &[u8]) -> Res<(Vec, usize, u8)> { { return Ok((inks.data.into_iter().map(|v| 255 - v).collect(), end, 4)); } + // The header's size is checked before the decoder allocates for it (a progressive JPEG keeps its + // coefficients besides its output). + let size = image::ImageReader::with_format(std::io::Cursor::new(jpeg), image::ImageFormat::Jpeg).into_dimensions(); + if size.is_ok_and(|(w, h)| u64::from(w) * u64::from(h) > MAX_DCT_PIXELS) { + return Err(PsError::Limit("decoded data is too large")); + } let img = image::load_from_memory_with_format(jpeg, image::ImageFormat::Jpeg).map_err(|_| PsError::Ps("ioerror", "DCTDecode".into()))?; if img.color().channel_count() == 1 { Ok((img.to_luma8().into_raw(), end, 1)) } else { Ok((img.to_rgb8().into_raw(), end, 3)) } } diff --git a/crates/eps/src/import/data/ops.rs b/crates/eps/src/import/data/ops.rs index 33f394482..04e5431d3 100644 --- a/crates/eps/src/import/data/ops.rs +++ b/crates/eps/src/import/data/ops.rs @@ -7,6 +7,9 @@ use super::{ /// Most bytes one filter or image reads. pub(super) const MAX_DATA: usize = 1 << 28; + +/// Most data sources one image may name (a DeviceN space has up to 32 components). +const MAX_SOURCES: usize = 32; /// The longest chain of filters over one pending file (real programs stack two or three). const MAX_FILTER_CHAIN: usize = 64; @@ -389,6 +392,25 @@ impl Interp<'_> { /// Decode the filters a file waits to run over the data the program reads, from where it is /// now in the file they were made over: a file being run, or the program's own file. pub(in super::super) fn materialize(&mut self, f: &Rc>) -> Res { + let input = match &*f.borrow() { + Stream::Pending { from: Some(r), .. } => match &*r.borrow() { + Stream::Buf { data, pos, .. } => data.len().saturating_sub(*pos), + _ => 0, + }, + Stream::Pending { from: None, .. } => self.lex.data().len(), + _ => return Ok(()), + }; + let r = self.decode_pending(f); + // A decode that failed is over: the stream is empty from now on, and the work it did is + // charged, so a program that catches the error can't have it run again and again. + if r.is_err() && matches!(&*f.borrow(), Stream::Pending { .. }) { + *f.borrow_mut() = Stream::Buf { data: vec![], pos: 0, rgb: None }; + self.spend_bytes(input)?; + } + r + } + + fn decode_pending(&mut self, f: &Rc>) -> Res { let (chain, from) = match &*f.borrow() { Stream::Pending { chain, from } => (chain.clone(), from.clone()), _ => return Ok(()), @@ -663,6 +685,8 @@ impl Interp<'_> { let multi = get("MultipleDataSources").is_some_and(|o| matches!(o, Obj::Bool(true))); let sources = match (get("DataSource"), multi) { (None, _) => vec![], + // An image has a data source for each of its components, so many more is no image: refused before any copy. + (Some(Obj::Array { items, exec: false }), true) if items.len() > MAX_SOURCES => return ps_err("rangecheck", "DataSource"), (Some(Obj::Array { items, exec: false }), true) => items.borrow().iter().map(source).collect::>>()?, (Some(src), _) => vec![source(&src)?], }; diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 4920b1d66..992a8bbce 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -550,10 +550,30 @@ impl<'a> Interp<'a> { /// An object as the scanner gives it: with `immediate`, its `//name`s replaced by their /// values (down through procedures), as they are when read. - pub fn scanned(&self, o: Obj, immediate: bool) -> Res { + pub fn scanned(&mut self, o: Obj, immediate: bool) -> Res { + // Every scan builds a fresh string or procedure, so a loop that rescans one text is charged + // for each copy it keeps, not only for the text. + self.charge_scanned(&o, 0)?; if immediate { self.resolve(o, 0) } else { Ok(o) } } + /// Count the storage of the string or procedure the scanner just made against [`MAX_MEMORY`], + /// and the time to build it. + fn charge_scanned(&mut self, o: &Obj, depth: usize) -> Res { + match o { + Obj::Str(s) => { + self.alloc(s.len())?; + self.spend_bytes(s.len()) + } + Obj::Array { items, .. } if depth < MAX_DEPTH as usize => { + self.alloc(items.len().saturating_mul(std::mem::size_of::()))?; + self.spend_bytes(items.len())?; + items.borrow().iter().try_for_each(|i| self.charge_scanned(i, depth + 1)) + } + _ => Ok(()), + } + } + fn resolve(&self, o: Obj, depth: usize) -> Res { match o { Obj::Exec(n) if n.starts_with(IMMEDIATE) => { diff --git a/crates/eps/src/import/preview.rs b/crates/eps/src/import/preview.rs index 9a3f5b751..d4b4efb13 100644 --- a/crates/eps/src/import/preview.rs +++ b/crates/eps/src/import/preview.rs @@ -28,7 +28,14 @@ pub(super) fn preview(bytes: &[u8], ps: &[u8], frame: Rect) -> Option { epsi(ps).and_then(|img| image_doc(img, frame)).map(|document| Preview { document, what: IMAGE }) } +/// Most pixels a TIFF preview may have: its RGBA copy is four bytes each besides the decoded image. +const MAX_PREVIEW_PIXELS: u64 = 1 << 26; + fn tiff_rgba(tiff: &[u8]) -> Option { + let size = image::ImageReader::with_format(std::io::Cursor::new(tiff), image::ImageFormat::Tiff).into_dimensions(); + if size.is_ok_and(|(w, h)| u64::from(w) * u64::from(h) > MAX_PREVIEW_PIXELS) { + return None; + } match image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff) { Ok(img) => Some(img.to_rgba8()), Err(_) => crate::tiff::palette_rgba(tiff), diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index f4edc0ac8..6a1df810b 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1588,3 +1588,40 @@ fn abutting_blocks_of_distinct_colours_stay_apart() { let gaps: String = (0..10).map(|i| format!("{} setgray {} 10 2 80 rectfill ", f64::from(i) / 40.0, 10 + i * 3)).collect(); assert_eq!(objects(&read(&gaps).document).len(), 10); } + +/// A string a program scans again and again (`token`) makes a fresh copy each time, and the copies +/// count against the memory budget. +#[test] +fn rescanned_strings_are_charged() { + let body = "/s 1048576 string def s 0 40 put s 1048575 41 put\n1100 { s token pop pop pop } repeat\n"; + let e = import(&eps(body)).unwrap_err(); + assert!(e.contains("too much memory"), "{e}"); + // A few scans stay far within it. + let r = import(&eps("/s 1048576 string def s 0 40 put s 1048575 41 put\n20 { s token pop pop pop } repeat\n")).unwrap(); + assert!(!r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings); +} + +/// An image names no more data sources than a color space has components: thousands of references +/// to one string are refused before any is copied. +#[test] +fn an_image_with_thousands_of_data_sources_is_refused() { + let sources = "<80> ".repeat(2048); + let e = import(&eps(&format!( + "/DeviceCMYK setcolorspace << /ImageType 1 /Width 1 /Height 1 /BitsPerComponent 8 \ + /MultipleDataSources true /ImageMatrix [1 0 0 1 0 0] /DataSource [{sources}] >> image" + ))) + .unwrap_err(); + assert!(e.contains("rangecheck") && e.contains("DataSource"), "{e}"); +} + +/// A filter over the program's own file that fails to decode is spent: a program that catches the +/// error and reads again gets the end of the file, not the same decode over again. +#[test] +fn a_failed_filter_decode_is_not_tried_again() { + let body = "/f currentfile /FlateDecode filter def\n/n 0 def 3 { { f read { pop } if } stopped { /n n 1 add def } if } repeat\n\ + n 1 eq { 0 0 10 10 rectfill } if\n"; + let r = import(&eps(body)).unwrap(); + let mut paths = 0; + r.document.walk(|n| paths += usize::from(matches!(n.kind, NodeKind::Path { .. }))); + assert_eq!(paths, 1, "{:?}", r.warnings); +} diff --git a/crates/eps/src/import/tests_images.rs b/crates/eps/src/import/tests_images.rs index 8aad47fc7..193b81a23 100644 --- a/crates/eps/src/import/tests_images.rs +++ b/crates/eps/src/import/tests_images.rs @@ -548,3 +548,21 @@ fn the_data_of_an_image_too_large_to_read_is_skipped() { assert_eq!(super::tests::fill(&o[0]), Some(&vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0))), "{program}"); } } + +/// A JPEG whose header claims far more pixels than an image may have is refused before the decoder +/// allocates for them. +#[test] +fn a_jpeg_with_a_huge_header_is_refused_before_it_is_decoded() { + let mut jpeg = vec![]; + image::codecs::jpeg::JpegEncoder::new(&mut jpeg).encode(&[128; 64], 8, 8, image::ExtendedColorType::L8).unwrap(); + // The frame header (marker, length, precision, height, width) now says 65535 × 65535. + let sof = jpeg.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap(); + jpeg[sof + 5..sof + 9].fill(0xFF); + let jpeg: String = jpeg.iter().map(|b| format!("{b:02x}")).collect(); + let e = import(&eps(&format!( + "/DeviceGray setcolorspace << /ImageType 1 /Width 1 /Height 1 /BitsPerComponent 8 /ImageMatrix [1 0 0 1 0 0] \ + /DataSource currentfile /ASCIIHexDecode filter /DCTDecode filter >> image\n{jpeg}>" + ))) + .unwrap_err(); + assert!(e.contains("too large"), "{e}"); +}