Load PDF document objects through the pinned craft-pdf parser and convert them into EffectCraft's existing drawing model with bounded traversal, recovery and retained data. Preserve generation numbers, stream limits, encrypted-document refusal and reported repairs.
Retain app-owned drawing, EPS, fonts and content/image decoding. Assert early excessive-nesting rejection without partial art, and cover tolerant arrays on native and WASM.
Validated with the22-stage local matrix and exact-head FreeBSD, Windows ARM64 and packaging checks.
Co-authored-by: Codex <noreply@openai.com>
inline_len computed (w * n * bpc).div_ceil(8) * h with Width/Height taken
straight from the content stream and no size validation — unlike decode()
and mask_image, which bound their geometry first. /W 5000000000000000000
overflows the multiply in debug and test builds; in release it wraps to a
small length and the caller slices the wrong bytes, misparsing the image
and shifting the EI resync.
Chain the arithmetic through checked_mul and return None, which the
caller already routes to the EI-scan fallback. Regression test:
inline_image_with_absurd_dimensions_falls_back_to_ei_scan — panics at
crates/pdf/src/image.rs:85 on main, passes with the chain.
mask_image read /BitsPerComponent from the attacker-controlled /SMask
dict with no validation and fed it straight into 1u32 << bpc — the main
image path rejects anything but 1|2|4|8|16 before its own identical
shift, the mask path did not. /BitsPerComponent 32 panics in debug and
test builds and wraps the shift in release, mistranslating every sample
into alpha.
Mirror decode()'s depth check: an unsupported mask depth means no usable
mask, so the image draws with the mask treated as absent. Regression
test: smask_with_absurd_bpc_is_ignored_not_fatal — panics at
crates/pdf/src/image.rs:184 on main, passes with the check.
Use checked division and remainder for EPS integer operators so i64::MIN with a divisor truncating to -1 cannot panic. Preserve the existing zero-divisor fallback and normal truncation behavior.
Add public EPS parse regressions covering extreme, fractional and ordinary operands.
PR #1223
The EPS interpreter counted one operation against its budget for each
name it ran, whatever the operator did, and paths had no length limit.
Parsing one arc at a radius of 10^300 passed 3 GiB of resident memory
within 43 seconds. A loop of arc at a radius of 2.2 x 10^18 was still
running after 60 seconds, and a loop of pathbbox on a path of 100,001
elements and { {} loop } loop did not finish within 20 seconds. PDF
content had no path limit either: parsing a page that repeats
0 0 1 1 re 4,000,000 times peaked at 2.3 GiB of resident memory.
A path now holds at most 1,048,576 elements (MAX_PATH, the limit of
VectorCraft's EPS interpreter). In EPS, moveto, lineto, curveto,
closepath, arc, charpath and the rect operators check it, and an
operator that would take a path past it stops the program with
"PostScript (path limit)". In PDF, m, l, c, v, y, h and re (each adds
at most five elements) stop the content stream with
"content (path limit)" when the path already holds more than 1,048,571.
Each path element an operator makes, reads or copies (arc, rectangle
arrays, charpath, pathbbox, clip, eoclip, gsave, save) now counts
against the EPS budget. Each pass of repeat, for, loop and forall
counts too, and so does each procedure item that is not a name. A
moveto right after a moveto replaces it, as the PostScript Language
Reference describes, so text placed with moveto and show leaves one
element.
Five tests in tests_import.rs cover the loops, the path limit in both
interpreters and runs of moveto and show.
arc added 2π to its sweep in a loop until the sweep was no longer
negative, and arcn subtracted 2π the same way. For an angle of 10^18
degrees the loop took about 3e15 passes, and from 2^56 radians (about
4.1e18 degrees) up, adding 2π left the sweep unchanged and the loop
never ended. A sweep that needed no loop, as in 0 10^18 arc, was passed
unchanged to kurbo, which divides it into more than 1e16 curves (about
four per turn at a radius of 10), and arc appended each curve to the
path. Importing an EPS file probes it with effectcraft_pdf::parse,
which has no time limit, so importing such a file did not finish.
As in storytold/vectorcraft#1062, every arc now starts at the first
angle's remainder after whole turns, and when the second angle is below
the first (arc) or above it (arcn), the sweep is worked out from the
two angles' remainders. A sweep of up to 1,024 turns is drawn whole.
For a longer one, the import records a skip note
("arc (too many turns)") and leaves out whole pairs of turns, so that
between 1,022 and 1,024 turns remain. The arc still ends at the second
angle, and an even-odd fill covers the same area as the full sweep.
Arcs of up to 1,024 turns whose angles are within about 10^6 degrees
are unchanged apart from rounding, except some whose angles are a whole
number of turns apart or within rounding of that. For larger angles,
the old conversion to radians lost precision: at 10^15 degrees, a
quarter-turn arc swept 1.5723 radians instead of 1.5708.
When raising (arc) or lowering (arcn) the second angle by whole turns
makes it equal to the first, as in 7 -353 arc or 2 362 arcn, rounding
sometimes made the old loop add a turn and draw a full circle. Those
arcs are now empty, as the PostScript Language Reference defines them.
When the raised or lowered angle falls just short of the first
instead, the reference defines a full circle, and each version leaves
some of these arcs empty: the old code drew nothing for
125.00000000000001 125 arc, which the new code draws as a full circle,
and the new code draws nothing for 1.0000000000000002 -719 arc, which
the old code drew as a full circle.
arcs_with_huge_angles_end_at_their_second_angle parses each program on
a thread of its own and fails when no result arrives within 20 seconds.
In EPS/PostScript, `0#7` asked i64::from_str_radix for radix 0, which panics (found by
fuzzing). Radixes outside 2..=36 are not numbers now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The s, b and b* operators closed the path even when there was no current point, which kurbo
asserts against (a panic in debug builds; found by fuzzing). Close only when a subpath is open.
CFF charstrings that draw before their first rmoveto now start the subpath at the current
point, as Type 1 charstrings already did. Denies the panic lints in the crate (it had no panic
sites).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>