fix: bound four unbounded expansions in STEP and IGES import

Each of these asks the allocator for tens of gigabytes, or runs for hours, from
a file of a few kilobytes to a few megabytes. An allocation failure is an abort,
so no panic guard can report it and the user's unsaved work is gone.

- IGES composite curves (102) name their children by pointer, and a tree whose
  every node names the same child twice fans out to 2^30 leaf visits under the
  depth cap. Count the entities visited and the segments produced against one
  budget, since a node that yields no segment trips no bound on the output, and
  refuse a composite curve that contains itself.
- IGES directory entries could all name the same parameter range, each
  materialising it as its own string and token list. Keep the total text
  proportional to the parameter section the file carries.
- STEP knot multiplicities were expanded into one value each before the knot
  count was compared with the control points: 20 million knots of multiplicity
  64 built a 10 GB vector only to be rejected. Check the sum first.
- One STEP face could list any number of boundary loops, each cloning its edge
  handles into a new wire that the face extent then samples. Cap the loops and
  the edges they hold; 100,000 bounds went from 2.1 GB to an immediate error.

Also guard the IGES translation, which was the one importer stage outside every
panic guard, keep the vertex-sewing bucket key clear of the overflow its
neighbour lookup caused for coordinates near f64's range, and count how often a
loop passes each vertex once instead of once per edge.

Fixes #146

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit c32792b71c76d74348ad5002d8d2da870937e58e)
This commit is contained in:
Matt committed 2026-10-11 08:22:07 -07:00
1 parent a094fa46b5
commit b5fb86f245
5 files changed
+199 -12

No files matched your search

+10 -3
View File
@@ -146,7 +146,11 @@ pub(crate) fn axis1(cx: &Ctx, id: u64) -> R<(mt::Point3, mt::Vector3)> {
Ok((o, d))
}
fn knot_vec(mults: Option<&Param>, knots: Option<&Param>) -> R<mt::KnotVec> {
/// `expected` is the knot count the control points and degree imply. Multiplicities are
/// expanded into one value each, so the sum has to be checked before the expansion: 20 million
/// knots of multiplicity 64 would build a 10 GB vector that the length check then rejects, and
/// an allocation failure aborts the process where an error does not.
fn knot_vec(mults: Option<&Param>, knots: Option<&Param>, expected: usize) -> R<mt::KnotVec> {
let m: Vec<usize> = mults
.and_then(Param::as_list)
.ok_or("B-spline without knot multiplicities")?
@@ -157,6 +161,9 @@ fn knot_vec(mults: Option<&Param>, knots: Option<&Param>) -> R<mt::KnotVec> {
if m.len() != k.len() || k.is_empty() || k.windows(2).any(|w| w.first() > w.get(1)) {
return Err("inconsistent knot vector".into());
}
if m.len() > expected || m.iter().try_fold(0usize, |a, v| a.checked_add(*v)) != Some(expected) {
return Err("knot multiplicities do not match the control points".into());
}
mt::KnotVec::from_single_multi(k, m).map_err(|e| format!("knot vector: {e}"))
}
@@ -238,7 +245,7 @@ fn bspline_curve(cx: &Ctx, e: &Entity) -> R<CurveGeo> {
}
let ctrl: Vec<mt::Point3> = ctrl_ids.iter().map(|c| point(cx, c.as_ref_id().ok_or("bad control point")?)).collect::<R<_>>()?;
let kv = match knots {
Some((m, k)) => knot_vec(m, k)?,
Some((m, k)) => knot_vec(m, k, ctrl.len() + degree + 1)?,
None => generated_knots(kind, ctrl.len(), degree)?,
};
if kv.len() != ctrl.len() + degree + 1 {
@@ -737,7 +744,7 @@ pub(crate) fn bspline_surface(cx: &Ctx, e: &Entity) -> R<mt::Surface> {
return Err("ragged B-spline control net".into());
}
let (ku, kv) = match knots {
Some([mu, mv, ku, kv]) => (knot_vec(mu, ku)?, knot_vec(mv, kv)?),
Some([mu, mv, ku, kv]) => (knot_vec(mu, ku, nu + du + 1)?, knot_vec(mv, kv, nv + dv + 1)?),
None => (generated_knots(kind, nu, du)?, generated_knots(kind, nv, dv)?),
};
if ku.len() != nu + du + 1 || kv.len() != nv + dv + 1 {