diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index c986f1917..6c8737cb9 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -2897,6 +2897,20 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() { format!("q 10 0 0 10 0 0 cm BI /W 2 /H 2 /CS /G /BPC 8 /F [/AHx /Fl] /DP [null {dp}] ID {data}> EI Q") }; let form = |dict: &str, content: &str| pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed(&format!("{FORM} {dict}"), content)]); + // A calculator function with the deflated program `z`, to write inside another object. Its + // data is in hexadecimal digits with spaces between them, so the object around it still reads. + let calculator = |z: &[u8], dp: &str| { + let data: String = z.iter().map(|b| format!("{:X} {:X} ", b >> 4, b & 15)).collect(); + let dict = format!("/FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Filter [/ASCIIHexDecode /FlateDecode] {dp}"); + format!("<< {dict} /Length {} >>\nstream\n{data}\nendstream", data.len()) + }; + let held = |object: String| pdf_with("0 0 5 5 re f", "<< >>", &[object.into_bytes()]); + let shaded = |function: &str| { + let shading = format!("<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function {function} >>"); + pdf_with("/S sh", "<< /Shading << /S 5 0 R >> >>", &[shading.into_bytes()]) + }; + let spaces = calculator(&spaces_past_the_cap(), ""); + let program = deflated(b"{ dup 1 exch sub 0 }"); let files = [ ("a form with Columns 0", form(zero, "0 0 5 5 re f")), ("a form with a huge Columns", form(huge, "0 0 5 5 re f")), @@ -2905,6 +2919,10 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() { ("an inline image in a compressed form", form("", &inline("<< /Predictor 2 /Columns 0 >>"))), ("an object stream with Columns 0", pdf_with_object_stream("[0 0 100 100]", zero)), ("a form of spaces past the cap", pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed_as_is(FORM, &spaces_past_the_cap())])), + ("a function of spaces past the cap in an array", held(format!("[{spaces}]"))), + ("a function of spaces past the cap in a dictionary", held(format!("<< /K {spaces} >>"))), + ("a function of spaces past the cap in a shading", shaded(&spaces)), + ("a function with Columns 0 in an array", held(format!("[{}]", calculator(&program, "/DecodeParms [null << /Predictor 2 /Columns 0 >>]")))), ]; on_a_small_stack(move || { for (what, bytes) in &files { @@ -2916,7 +2934,8 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() { }); // Within range, the same files open. let fine = "/DecodeParms << /Predictor 1 /Columns 0 >>"; - for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>")] { + let function = calculator(&program, ""); + for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>"), held(format!("[{function}]")), shaded(&function)] { vectorcraft_engine::cmd::fileio::load("fine.pdf", &bytes).expect("opens"); } } diff --git a/crates/pdf/src/pages.rs b/crates/pdf/src/pages.rs index 90ab4002a..aef1331a2 100644 --- a/crates/pdf/src/pages.rs +++ b/crates/pdf/src/pages.rs @@ -117,37 +117,34 @@ fn procedures_too_deep(o: &Object<'_>) -> bool { /// The error of a file with a stream the reader would fail on ([`crate::streams`]). const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or its decode parameters can't be applied"; -/// The error of a file [`nests_without_end`]. +/// The error of a file with functions or color spaces built on one another past [`MAX_LINKS`] +/// (or on themselves), or a calculator function nested past [`MAX_PROCEDURES`]. const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end"; -/// Why `pdf` is refused, if it is: a stream the reader would fail on (its own checks come first: -/// the calculator functions are decoded below), or [`nests_without_end`]. +/// Why `pdf` is refused, if it is: a stream the reader would fail on ([`STREAM_NOTE`]), or +/// functions, color spaces or calculator procedures nested too deep ([`NESTED_NOTE`]). Every +/// object is looked at, and every array, dictionary and stream written inside one, wherever it +/// is used from: the interpreter builds functions and color spaces from resources, shadings, +/// images, patterns, masks and graphics states alike. fn hostile(pdf: &Pdf) -> Option<&'static str> { - if pdf.objects().into_iter().any(|o| matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s))) { - return Some(STREAM_NOTE); - } - nests_without_end(pdf).then_some(NESTED_NOTE) -} - -/// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on -/// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked -/// at, wherever it is used from: the interpreter builds them from resources, shadings, images, -/// patterns, masks and graphics states alike. -fn nests_without_end(pdf: &Pdf) -> bool { let xref = pdf.xref(); let mut known = HashMap::new(); // The objects of the file, then what each holds directly (64 deep at most: the reader's cap). let mut pending: Vec> = pdf.objects().into_iter().collect(); while let Some(o) = pending.pop() { + // A stream's own checks come first: a calculator function is decoded below. + if matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s)) { + return Some(STREAM_NOTE); + } if procedures_too_deep(&o) || built_on(MaybeRef::NotRef(o.clone()), xref, &mut known, MAX_LINKS).is_none() { - return true; + return Some(NESTED_NOTE); } match &o { Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)), _ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| values(d)).filter_map(direct)), } } - false + None } /// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions