EPS: shading functions in shared arrays no longer hang the import

A shading's Function can be an array of functions. eval evaluated every
entry of an array and the arrays inside it, with only a depth limit. An
array of 100 entries that all refer to the same array one level down,
nested that way a few levels deep, is small in memory, but each
evaluation visited about 100^levels functions, and a gradient samples
such a function 33 times. vectorcraft-cli info on a file with four
levels of 100 shared entries had used 90 CPU seconds, with a 12 MiB
resident set, when "timeout 120" stopped it. eval also counted nothing
against the operation budget: a loop that paints an axial or radial
shading counted a few operations a pass, however many functions each
pass evaluated. On a file that paints a gradient of one Type 2 function
with N 2 in "{ sh shfill } loop", vectorcraft-cli info ran for
209 CPU seconds, and its resident set reached 4 GB before the program
ran out of the operation budget.

eval now counts the functions one evaluation visits, as linear_points
counts its nodes, and raises limitcheck past 257. The PostScript
Language Reference defines a shading's function as one function
dictionary or an array of one for each color component. The reader
allows 32 components, and the depth limit allows each entry a chain of
8 functions. An array with more entries than the visits left raises
limitcheck before it is copied. Each function visited counts 8
operations against the budget: one visit took as long as 6 to 10
operations, measured with loops of pop and of a name lookup.

Tests: a shading over four levels of 100 shared entries raises
limitcheck, and the square drawn before it stays; a shading over one
array of 256 entries is drawn as a gradient, and one over 257 entries
raises limitcheck; a program that paints 2000 shadings over the
256-entry array runs out of the operation budget. The test reads each
program but the last on a thread of its own and fails when no result
arrives within 20 seconds.
This commit is contained in:
dotCooCoo authored and bflatastic committed 2026-10-12 09:43:15 +07:00
1 parent 2c5262c5dd
commit 4a27d897ed
2 files changed
+61 -3

No files matched your search

+31 -3
View File
@@ -37,6 +37,16 @@ const SHADING_SAMPLES: usize = 32;
/// Most bounds of a stitched linear function that become a gradient's stops: past it, the
/// function is sampled like any other (each stop evaluates the whole function).
const MAX_STITCH_STOPS: usize = 1024;
/// Most functions one evaluation of a shading's function visits: the function itself, each entry
/// of an array of functions and, in each stitching function, the piece that applies to the input.
/// For every shading type, the `Function` entry is one function dictionary or an array of one for
/// each color component (PLRM 3rd ed., Tables 4.12 to 4.17; a type 7 shading has the entries of
/// type 6). The reader allows at most [`MAX_INKS`] components, and the depth limit allows each
/// entry a chain of at most 8 functions.
const MAX_FUNCTION_VISITS: usize = 1 + MAX_INKS * 8;
/// Work one function costs against the operation budget each time it is evaluated: about as much
/// as 8 operations (measured).
const FUNCTION_OPS: u64 = 8;
pub(super) const FAR_AWAY: &str = "objects far outside the page were left out";
pub(super) const TOO_MUCH: &str = "the file draws more than VectorCraft reads: the rest was left out";
@@ -1064,16 +1074,34 @@ impl Interp<'_> {
/// A function (types 0, 2 and 3; an array of them, one per component) at `x` (types 2 and 3
/// take one input).
pub fn eval(&mut self, f: &Obj, x: &[f64], depth: u32) -> Res<Vec<f64>> {
if depth > 8 {
self.eval_inner(f, x, depth, &mut 0)
}
/// [`Self::eval`], with `visits` (shared by the whole recursion, incremented on every call)
/// counting the functions visited against [`MAX_FUNCTION_VISITS`], as [`linear_points`] counts
/// its nodes. The depth limit alone stops a function that refers to itself, but not one that
/// fans out through shared arrays: when four levels of arrays each have 100 entries that all
/// refer to the same array of the level below (at the last level, the same function), one
/// evaluation would visit 101,010,101 functions. Each function visited also counts
/// [`FUNCTION_OPS`] against the operation budget.
fn eval_inner(&mut self, f: &Obj, x: &[f64], depth: u32, visits: &mut usize) -> Res<Vec<f64>> {
*visits += 1;
if depth > 8 || *visits > MAX_FUNCTION_VISITS {
return ps_err("limitcheck", "Function");
}
self.spend(FUNCTION_OPS)?;
let d = match f {
Obj::Dict(d) => d.clone(),
Obj::Array { items, .. } => {
// Each entry is at least one more function: an array with more entries than the
// visits left raises `limitcheck` before it is copied.
if items.len() > MAX_FUNCTION_VISITS - *visits {
return ps_err("limitcheck", "Function");
}
let fs = items.to_vec();
let mut out = vec![];
for g in &fs {
out.extend(self.eval(g, x, depth + 1)?.first().copied());
out.extend(self.eval_inner(g, x, depth + 1, visits)?.first().copied());
}
return Ok(out);
}
@@ -1115,7 +1143,7 @@ impl Interp<'_> {
let (e0, e1) = (encode.get(2 * k).copied().unwrap_or(0.0), encode.get(2 * k + 1).copied().unwrap_or(1.0));
let u = if b != a { e0 + (t - a) / (b - a) * (e1 - e0) } else { e0 };
let g = fs.and_then(|fs| fs.get(k)).ok_or(PsError::Ps("rangecheck", "Functions".into()))?;
self.eval(&g, &[u], depth + 1)
self.eval_inner(&g, &[u], depth + 1, visits)
}
_ => {
self.out.warn(UNKNOWN_FUNCTIONS);
+30
View File
@@ -696,6 +696,36 @@ fn arcs_with_huge_angles_end_at_their_second_angle() {
}
}
/// An axial shading's function is one function dictionary or an array of one for each color
/// component (PLRM 3rd ed., Table 4.13). One evaluation visits at most 257 functions, and past
/// that the shading raises `limitcheck`: when four levels of arrays each have 100 entries that all
/// refer to the same array of the level below (at the last level, the same function), one
/// evaluation would visit 101,010,101 functions. Each function visited also counts against the
/// operation budget.
#[test]
fn shading_functions_in_shared_arrays_stop_at_the_limits() {
let axial = |function: &str| format!("<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 100 0] /Function {function} >>");
let f = "/f << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> def";
// `a`: an array of `n` references to the function `f`.
let array = |n: usize| format!("{f} /a {n} array def 0 1 {} {{ a exch f put }} for", n - 1);
// Each level is an array of 100 references to the level below.
let levels = "/l2 100 array def 0 1 99 { l2 exch a put } for /l3 100 array def 0 1 99 { l3 exch l2 put } for \
/l4 100 array def 0 1 99 { l4 exch l3 put } for";
let limitcheck = |r: &crate::Imported| r.warnings.iter().any(|w| w.contains("limitcheck in `shfill`: Function"));
let r = read_soon(&format!("0 0 10 10 rectfill {} {levels} {} shfill", array(100), axial("l4")));
assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings);
// 256 references are 257 functions for each sample, the most allowed; one more raises `limitcheck`.
let r = read_soon(&format!("{} {} shfill", array(256), axial("a")));
let o = objects(&r.document);
assert!(r.warnings.is_empty() && matches!(fill(&o[0]), Some(Paint::Gradient(_))), "{:?}", r.warnings);
let r = read_soon(&format!("0 0 10 10 rectfill {} {} shfill", array(257), axial("a")));
assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings);
// Each of these shadings visits 33 * 257 functions (33 samples), and 2000 of them count for
// more than the operation budget.
let r = read(&format!("{} /sh {} def 2000 {{ sh shfill }} repeat", array(256), axial("a")));
assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{:?}", r.warnings);
}
#[test]
fn cshow_runs_its_procedure_for_each_character() {
check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq");