Security: bounded flattening, plug-in budgets and caches, loaded textures, Revolve paint copies, color book names and ICC names

CodeRabbit findings #4, #34, #46, #51, #77, #101, #170, #171, #188:

- `vectorcraft_geom::flatten`: kurbo's flatten with each curve's tolerance raised so it makes
  at most ~4096 segments (non-finite curves become a line); the canvas outline and
  variable-width strokes use it.
- Plug-ins: a heavy filter's budget is the host's `Limits::heavy_fuel`/`heavy_wall_time_ms`,
  never more; a start function runs on one fuel slice; the effect cache holds at most ~1M
  anchors of inputs and results, and doesn't keep a result past a quarter of that.
- Loaded textures are cached by their PNG (base64 spacing doesn't matter), and live textures
  hold at most 512 MB together.
- Revolve draws the source path when the faces' paint copies would pass 256 MB.
- Color books: names with prefix and suffix take at most 64 MB.
- ICC: localized-name (`mluc`) records over 1 MB together are refused before moxcms copies them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dylan M. Taylor
2026-10-24 17:19:54 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent a608838ba2
commit e2a870e602
10 changed files with 257 additions and 28 deletions
+14 -6
View File
@@ -46,6 +46,11 @@ pub struct Limits {
/// Wall-clock budget for one run, in milliseconds. Native builds only: the web build has no
/// monotonic clock in `std`, so the fuel budget alone bounds it there.
pub wall_time_ms: u64,
/// The most a heavy filter ([`crate::Budget::Heavy`]) is given instead of `fuel_base` and
/// `wall_time_ms`: what the host allows, which the module's manifest can ask for but never
/// raise. Set them to `fuel_base` and `wall_time_ms` to give heavy filters nothing more.
pub heavy_fuel: u64,
pub heavy_wall_time_ms: u64,
}
impl Default for Limits {
@@ -58,6 +63,8 @@ impl Default for Limits {
fuel_per_byte: 4_000,
max_recursion_depth: 1024,
wall_time_ms: 60_000,
heavy_fuel: HEAVY_FUEL,
heavy_wall_time_ms: HEAVY_WALL_TIME_MS,
}
}
}
@@ -170,11 +177,11 @@ impl Plugin {
e => e,
})?;
p.manifest = Manifest::parse(&text)?;
// A heavy filter's runs get the larger budget, which the host lets the user allow or
// refuse before installing it.
// A heavy filter's runs get the larger budget the host allows for one, which the host
// lets the user allow or refuse before installing it.
if p.manifest.budget == crate::Budget::Heavy {
p.limits.fuel_base = p.limits.fuel_base.max(HEAVY_FUEL);
p.limits.wall_time_ms = p.limits.wall_time_ms.max(HEAVY_WALL_TIME_MS);
p.limits.fuel_base = p.limits.fuel_base.max(p.limits.heavy_fuel);
p.limits.wall_time_ms = p.limits.wall_time_ms.max(p.limits.heavy_wall_time_ms);
}
// The other exports must exist with the right types.
live.func::<i32, i32>("vc_alloc")?;
@@ -217,8 +224,9 @@ impl Plugin {
StoreLimitsBuilder::new().memory_size(self.limits.max_memory_bytes).memories(1).tables(4).table_elements(100_000).instances(1).build();
let mut store = Store::new(&self.engine, limits);
store.limiter(|l| l);
// The start function (if any) runs on this fuel.
store.set_fuel(self.limits.fuel_base).map_err(wasm_err)?;
// The start function (if any) runs on one slice of fuel: it runs before `call` can check
// the deadline or a cancel between slices, so a heavy budget would let it run for minutes.
store.set_fuel(self.limits.fuel_base.min(FUEL_SLICE)).map_err(wasm_err)?;
let linker = Linker::<StoreLimits>::new(&self.engine);
let instance = linker.instantiate_and_start(&mut store, &self.module).map_err(wasm_err)?;
let memory =