AF.5: bound hostile Affinity files; fuzz the new import paths

- One budget for the whole read (layers, pixels, characters, table
  cells), shared with embedded documents and charged also when their
  reading fails; the first reading is dropped before the second.
- Tables: at most 65 536 cells; lines and cell formats that don't
  match the grid are left out instead of shifted.
- Paragraph formats matched to runs by binary search; font names
  longer than 256 bytes are dropped.
- Overlays: one work budget per import; encoded originals decoded
  within their layer's size; table cell outlines recoloured too.
- Engine: master page copies capped, each page's master found in
  linear time, line gaps looked up once per font, the working CMYK
  table built once, objects past MAX_COORD left out.
- Embedded content stays with its node; pixel masks under artistic
  text are read whole and include stroke reach; gradient overlays on
  hidden layers are kept.
- Render: contexts and raster effect images stay within
  MAX_RASTER_SIDE per side (vello overflowed near u16::MAX; found by
  the new fuzz target).
- Tests: affinity tests/import_limits.rs, feature samples in
  import_fuzz.rs, shared builders in engine/tests/support/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dwetscher
2026-10-22 18:24:57 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent fd50bd6f84
commit f73a3df974
16 changed files with 1330 additions and 524 deletions
+125 -58
View File
@@ -271,9 +271,11 @@ pub struct Table {
pub rows: Vec<f64>,
/// Row by row.
pub cells: Vec<TableCell>,
/// Vertical edges row by row, one more per row than the columns; `None` draws no line.
/// Vertical edges row by row, one more per row than the columns; `None` draws no line, and
/// the list is empty when the file's didn't match the grid.
pub vertical: Vec<Option<Stroke>>,
/// Horizontal edges boundary by boundary (one more than the rows), one per column.
/// Horizontal edges boundary by boundary (one more than the rows), one per column (empty
/// as `vertical` can be).
pub horizontal: Vec<Option<Stroke>>,
/// Node space to document pixels.
pub transform: Affine,
@@ -472,6 +474,29 @@ const MAX_LAYER_PIXELS: u64 = 512 << 20;
/// Embedded documents inside embedded documents are followed this deep.
const MAX_EMBED_DEPTH: usize = 4;
/// Characters of all stories together (a story may be shared by many text layers).
const MAX_DOCUMENT_CHARS: usize = 16 << 20;
/// Table cells of all tables together (each becomes a text object with its edge lines).
const MAX_DOCUMENT_CELLS: usize = 1 << 18;
/// What a whole read may still use. Shared objects can be linked from many layers, and embedded
/// documents are read with what their embedding document left, so linking or embedding the
/// same content many times can't multiply the work.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Budget {
/// Layer nodes ([`MAX_NODES`] in all).
pub(crate) nodes: usize,
/// Pixels the pixel layers may decode ([`MAX_LAYER_PIXELS`] in all).
pub(crate) pixels: u64,
/// Characters of stories ([`MAX_DOCUMENT_CHARS`] in all).
pub(crate) chars: usize,
/// Table cells ([`MAX_DOCUMENT_CELLS`] in all).
pub(crate) cells: usize,
}
impl Budget {
pub(crate) const FULL: Budget = Budget { nodes: MAX_NODES, pixels: MAX_LAYER_PIXELS, chars: MAX_DOCUMENT_CHARS, cells: MAX_DOCUMENT_CELLS };
}
/// Read a document: archive, `doc.dat` and the model.
pub fn read(bytes: &[u8], limits: Limits) -> Result<Document, Error> {
@@ -480,28 +505,35 @@ pub fn read(bytes: &[u8], limits: Limits) -> Result<Document, Error> {
/// [`read`], its pixel layers holding at most `pixels` pixels together.
pub(crate) fn read_within(bytes: &[u8], limits: Limits, pixels: u64) -> Result<Document, Error> {
read_at(bytes, limits, pixels, Affine::IDENTITY, 0).map(|r| r.document)
read_at(bytes, limits, &mut Budget { pixels, ..Budget::FULL }, Affine::IDENTITY, 0).document
}
/// A document read by [`read_at`], with what it used of its budgets.
/// A document read by [`read_at`] (or why it couldn't be), with the bytes its archive extracted
/// either way.
struct ReadAt {
document: Document,
/// Bytes its archive extracted.
document: Result<Document, Error>,
extracted: usize,
/// Pixels its pixel layers left of the allowance.
pixels_left: u64,
}
/// Read a document whose spreads are placed by `base` (for embedded documents, nested `depth`
/// deep), its pixel layers holding at most `pixels` pixels together.
fn read_at(bytes: &[u8], limits: Limits, pixels: u64, base: Affine, depth: usize) -> Result<ReadAt, Error> {
let mut archive = Archive::open(bytes, limits)?;
/// deep). What it uses is taken from `budget`, also when it fails part of the way.
fn read_at(bytes: &[u8], limits: Limits, budget: &mut Budget, base: Affine, depth: usize) -> ReadAt {
let mut archive = match Archive::open(bytes, limits) {
Ok(a) => a,
Err(e) => return ReadAt { document: Err(e), extracted: 0 },
};
let document = read_archive(&mut archive, budget, base, depth);
ReadAt { document, extracted: archive.extracted() }
}
fn read_archive(archive: &mut Archive, budget: &mut Budget, base: Affine, depth: usize) -> Result<Document, Error> {
let doc = archive.read("doc.dat")?;
let s = stream::parse(&doc)?;
let mut reader = Reader::new(&s, &mut archive, base, depth);
reader.pixels_left = pixels;
let (document, pixels_left) = reader.document()?;
Ok(ReadAt { document, extracted: archive.extracted(), pixels_left })
let mut reader = Reader::new(&s, archive, base, depth);
reader.budget = *budget;
let document = reader.document();
*budget = reader.budget;
document
}
pub(crate) struct Reader<'s, 'a, 'b> {
@@ -509,7 +541,6 @@ pub(crate) struct Reader<'s, 'a, 'b> {
pub(crate) archive: &'b mut Archive<'a>,
pub(crate) dpi: f64,
pub(crate) warnings: BTreeMap<String, usize>,
nodes: usize,
active: HashSet<ObjId>,
/// Where the spreads go: the identity, or an embedding document's placement.
base: Affine,
@@ -521,8 +552,8 @@ pub(crate) struct Reader<'s, 'a, 'b> {
pending: Option<Vec<Node>>,
/// The master spreads, in the order of [`Document::masters`].
master_ids: Vec<ObjId>,
/// Pixels the pixel layers still to come may decode ([`MAX_LAYER_PIXELS`] in all).
pub(crate) pixels_left: u64,
/// What the rest of the read may use.
pub(crate) budget: Budget,
}
impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
@@ -533,14 +564,13 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
archive,
dpi: 72.0,
warnings: BTreeMap::new(),
nodes: 0,
active: HashSet::new(),
base,
type_base: Affine([a, b, c, d, 0.0, 0.0]),
depth,
pending: None,
master_ids: Vec::new(),
pixels_left: MAX_LAYER_PIXELS,
budget: Budget::FULL,
}
}
@@ -548,8 +578,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
*self.warnings.entry(what.into()).or_default() += 1;
}
/// The document, and the pixels its pixel layers left of the allowance.
fn document(mut self) -> Result<(Document, u64), Error> {
/// The document.
fn document(&mut self) -> Result<Document, Error> {
let s = self.s;
let root = s.root;
self.dpi = s.obj(root, b"UVCn").and_then(|u| s.f64(u, b"UPPI")).filter(|d| *d > 0.0 && *d < 100_000.0).unwrap_or(72.0);
@@ -589,8 +619,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
.and_then(|p| s.bytes(p, b"Data"))
.filter(|b| b.len() <= MAX_PROFILE_BYTES && b.get(16..20) == Some(b"CMYK".as_slice()))
.map(<[u8]>::to_vec);
let warnings = self.warnings.into_iter().map(|(w, n)| if n > 1 { format!("{w} ({n}×)") } else { w }).collect();
Ok((Document { dpi: self.dpi, spreads, masters, saved_by, warnings, cmyk_profile }, self.pixels_left))
let warnings = std::mem::take(&mut self.warnings).into_iter().map(|(w, n)| if n > 1 { format!("{w} ({n}×)") } else { w }).collect();
Ok(Document { dpi: self.dpi, spreads, masters, saved_by, warnings, cmyk_profile })
}
/// A spread (`Sprd`) of the document `doc`, or of its master spreads.
@@ -650,10 +680,10 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
self.warn("layers nested deeper than 128 levels");
return Ok(None);
}
self.nodes += 1;
if self.nodes > MAX_NODES {
let Some(left) = self.budget.nodes.checked_sub(1) else {
return Err(Error::Limit("too many layers"));
}
};
self.budget.nodes = left;
// A node that contains itself (a shared object linked back into its own subtree).
if !self.active.insert(id) {
self.warn("a layer that contains itself");
@@ -680,17 +710,16 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
} else {
self.kind(id, class, world, parent)?
};
// This node's embedded content, before its children's `node` calls can take it.
let embedded = self.pending.take();
// A compound's children are its operands, already merged into its outline.
let (mut children, child_mask) = if class == Tag::of(b"Comp") { (Vec::new(), None) } else { self.children(id, world, depth)? };
if let Some(embedded) = self.pending.take() {
if let Some(embedded) = embedded {
children.extend(embedded);
}
// Pixel masks are read only over what they mask (they can span a whole spread at print
// resolution, a gigapixel).
let over = match (kind_bounds(&kind), nodes_bounds(&children)) {
(Some(a), Some(b)) => Some(union(a, b)),
(a, b) => a.or(b),
};
let over = mask_extent(&kind, &children);
let child_mask = match child_mask {
Some((c, at)) => crate::raster::mask(self, c, at, over)?,
None => None,
@@ -699,8 +728,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
let pixel_mask = attached_mask.or(child_mask);
self.active.remove(&id);
let mut node = Node { class, name, visible, locked, opacity, blend, kind, mask, pixel_mask, effects: Vec::new(), children };
// Gradient overlays are laid out over the node's bounds.
let bounds = nodes_bounds(std::slice::from_ref(&node));
// Gradient overlays are laid out over the node's bounds, also while it is hidden.
let bounds = tree_bounds(&node);
node.effects = self.effects(id, world, bounds);
Ok(Some(node))
}
@@ -925,20 +954,30 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
return None;
}
// A first reading finds its page and resolution, the second places it.
let probe = match read_at(inner, self.archive.remaining(), self.pixels_left, Affine::IDENTITY, self.depth + 1) {
Ok(r) => r,
// Both readings are charged to the budget and the archive's limits, also when they fail:
// each does the work.
let mut budget = self.budget;
let probe = read_at(inner, self.archive.remaining(), &mut budget, Affine::IDENTITY, self.depth + 1);
self.budget = budget;
if self.archive.charge(probe.extracted).is_err() {
self.warn("embedded documents beyond the size limit (left out)");
return None;
}
let probe = match probe.document {
Ok(d) => d,
Err(e) => {
self.warn(format!("embedded documents that could not be read ({e})"));
return None;
}
};
let used = probe.extracted;
let spread = probe.document.spreads.first()?;
let spread = probe.spreads.first()?;
// `PBBx` 2 places the content's own bounds (as with a cached picture); otherwise its page.
let content = s.obj(id, b"Bitm").and_then(|b| s.enumeration(b, b"PBBx")).is_some_and(|(k, _)| k == 2);
let first_board = spread.nodes.iter().find_map(|n| if let Kind::Artboard { rect, .. } = &n.kind { Some(*rect) } else { None });
let page = if content { None } else { spread.pages.first().copied().or(first_board) };
let page = page.or_else(|| nodes_bounds(&spread.nodes)).unwrap_or(spread.bounds);
// Its pixels aren't needed any more: never hold both readings' at once.
drop(probe);
// Its document pixels are the node's units, whatever either document's resolution.
let k = 1.0;
let (cx, cy) = ((page.x0 + page.x1) / 2.0, (page.y0 + page.y1) / 2.0);
@@ -947,18 +986,14 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
self.warn("embedded documents with an invalid placement");
return None;
}
if self.archive.charge(used).is_err() {
self.warn("embedded documents beyond the size limit (left out)");
return None;
}
let placed = read_at(inner, self.archive.remaining(), self.pixels_left, base, self.depth + 1).ok()?;
// The probe's pixels were only looked at; the placed content's stay.
self.pixels_left = placed.pixels_left;
let doc = placed.document;
let mut budget = self.budget;
let placed = read_at(inner, self.archive.remaining(), &mut budget, base, self.depth + 1);
self.budget = budget;
if self.archive.charge(placed.extracted).is_err() {
self.warn("embedded documents beyond the size limit (left out)");
return None;
}
let doc = placed.document.ok()?;
for w in doc.warnings {
self.warn(format!("{w} [in embedded documents]"));
}
@@ -1005,22 +1040,54 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
/// The bounds of what visible `nodes` draw (control points of outlines, corners of images, frames
/// and tables), in document pixels.
pub fn nodes_bounds(nodes: &[Node]) -> Option<Rect> {
fn walk(nodes: &[Node], depth: usize, out: &mut Option<Rect>) {
if depth > MAX_TREE_DEPTH {
return;
}
for n in nodes.iter().filter(|n| n.visible) {
kind_points(&n.kind, &mut |p| add_point(out, p));
walk(&n.children, depth + 1, out);
}
}
let mut out = None;
walk(nodes, 0, &mut out);
walk_bounds(nodes, true, 0, &mut out);
out
}
/// The bounds of what one node's own kind draws (not its children).
pub(crate) fn kind_bounds(kind: &Kind) -> Option<Rect> {
/// The bounds of `node` and everything in it, hidden or not.
fn tree_bounds(node: &Node) -> Option<Rect> {
let mut out = None;
walk_bounds(std::slice::from_ref(node), false, 0, &mut out);
out
}
fn walk_bounds(nodes: &[Node], visible_only: bool, depth: usize, out: &mut Option<Rect>) {
if depth > MAX_TREE_DEPTH {
return;
}
for n in nodes.iter().filter(|n| n.visible || !visible_only) {
kind_points(&n.kind, &mut |p| add_point(out, p));
walk_bounds(&n.children, visible_only, depth + 1, out);
}
}
/// What a pixel mask over a node of `kind` with `children` is read over: the bounds of what they
/// draw, strokes included, or `None` (all of the mask) when some of it has no bounds known here:
/// artistic text, whose glyphs only the layout places.
fn mask_extent(kind: &Kind, children: &[Node]) -> Option<Rect> {
fn visit(kind: &Kind, children: &[Node], depth: usize, out: &mut Option<Rect>) -> bool {
if depth > MAX_TREE_DEPTH || matches!(kind, Kind::Text(t) if t.frame.is_none()) {
return false;
}
let mut own = kind_bounds(kind);
if let (Some(r), Kind::Shape { strokes, .. } | Kind::Artboard { strokes, .. }) = (own, kind) {
// As far as a stroke can reach out of its path, miter joins included.
let reach = strokes.iter().map(|s| s.width * s.miter_limit.clamp(1.0, 10.0)).filter(|w| w.is_finite()).fold(0.0, f64::max);
own = Some(Rect { x0: r.x0 - reach, y0: r.y0 - reach, x1: r.x1 + reach, y1: r.y1 + reach });
}
if let Some(r) = own {
*out = Some(out.map_or(r, |o| union(o, r)));
}
children.iter().filter(|n| n.visible).all(|n| visit(&n.kind, &n.children, depth + 1, out))
}
let mut out = None;
if visit(kind, children, 0, &mut out) { out } else { None }
}
/// The bounds of what one node's own kind draws (not its children): its outline's points, its
/// image's or frame's corners, or artistic text's anchor.
pub fn kind_bounds(kind: &Kind) -> Option<Rect> {
let mut out = None;
kind_points(kind, &mut |p| add_point(&mut out, p));
out