mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
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:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
a608838ba2
commit
e2a870e602
@@ -29,6 +29,10 @@ pub const WALL_TIME_MS: u64 = 5_000;
|
||||
|
||||
/// Cached results kept before the cache starts over.
|
||||
const CACHE_SIZE: usize = 512;
|
||||
/// Anchors the cached inputs and results may hold together before the cache starts over (about
|
||||
/// 50 MB). A plug-in can return far more than it was given, so a count of results alone let a few
|
||||
/// hundred small inputs keep gigabytes. A result past a quarter of this isn't kept.
|
||||
const CACHE_ANCHORS: usize = 1 << 20;
|
||||
|
||||
/// The plug-in id of effect id `id` (`plugin.<id>`).
|
||||
pub fn plugin_id(effect_id: &str) -> Option<&str> {
|
||||
@@ -107,9 +111,18 @@ impl Key {
|
||||
}
|
||||
}
|
||||
|
||||
type Cache = HashMap<u64, (Key, Option<PathData>)>;
|
||||
#[derive(Default)]
|
||||
struct Cache {
|
||||
entries: HashMap<u64, (Key, Option<PathData>)>,
|
||||
/// The anchors of the entries' inputs and results ([`CACHE_ANCHORS`]).
|
||||
anchors: usize,
|
||||
}
|
||||
|
||||
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
||||
|
||||
fn anchors(p: &PathData) -> usize {
|
||||
p.subpaths.iter().map(|sp| sp.anchors.len()).sum()
|
||||
}
|
||||
static ERRORS: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
|
||||
|
||||
/// The last error of live-effect plug-in `id` (cleared when it next succeeds).
|
||||
@@ -142,7 +155,7 @@ pub fn apply(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: Fill
|
||||
path: path.clone(),
|
||||
};
|
||||
let h = key.hash64();
|
||||
if let Some((k, out)) = CACHE.lock().unwrap_or_else(|e| e.into_inner()).as_ref().and_then(|c| c.get(&h))
|
||||
if let Some((k, out)) = CACHE.lock().unwrap_or_else(|e| e.into_inner()).as_ref().and_then(|c| c.entries.get(&h))
|
||||
&& *k == key
|
||||
{
|
||||
return out.clone();
|
||||
@@ -155,12 +168,19 @@ pub fn apply(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: Fill
|
||||
}
|
||||
note(id, &r);
|
||||
let out = r.ok();
|
||||
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let cache = cache.get_or_insert_with(HashMap::new);
|
||||
if cache.len() >= CACHE_SIZE {
|
||||
cache.clear();
|
||||
let size = anchors(&key.path) + out.as_ref().map_or(0, anchors);
|
||||
if size > CACHE_ANCHORS / 4 {
|
||||
return out;
|
||||
}
|
||||
cache.insert(h, (key, out.clone()));
|
||||
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let cache = cache.get_or_insert_with(Cache::default);
|
||||
if cache.entries.len() >= CACHE_SIZE || cache.anchors + size > CACHE_ANCHORS {
|
||||
*cache = Cache::default();
|
||||
}
|
||||
if let Some((old, old_out)) = cache.entries.insert(h, (key, out.clone())) {
|
||||
cache.anchors = cache.anchors.saturating_sub(anchors(&old.path) + old_out.as_ref().map_or(0, anchors));
|
||||
}
|
||||
cache.anchors += size;
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user