mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1358 from Sloptimum/fix/brep-p3-offset-turn-nurbs-hang
fix: topology.brep_p3 — the runner refuses the HANG-16 regime on the Rust side too (nightly fuzz00282 timeout)
This commit is contained in:
@@ -1624,9 +1624,11 @@ fn put_vertex_points(o: &mut Vec<f64>, b: &Brep, vertices: &[VertexId]) -> Resul
|
||||
fn run_rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
let mut k = Kernel::default();
|
||||
if standalone(&i.op) {
|
||||
let Some((_, args)) = op_args(&i.op, &[0; 10])? else {
|
||||
let Some((op, args)) = op_args(&i.op, &[0; 10])? else {
|
||||
return Ok(Output::err("no target"));
|
||||
};
|
||||
// an input the FFI refuses is a harness error on both sides
|
||||
ffi::validate(op, "", &args, &[0; 10], false).map_err(|e| ffi_err(&e))?;
|
||||
let mut b = Brep::new();
|
||||
return match run_rust_op(&mut k, &mut b, None, &i.op, &args) {
|
||||
Ok(out) => Ok(Output::new().values("out", out)),
|
||||
@@ -1638,9 +1640,24 @@ fn run_rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
Ok(x) => x,
|
||||
Err(o) => return Ok(o),
|
||||
};
|
||||
let Some((_, args)) = op_args(&i.op, &c)? else {
|
||||
let Some((op, args)) = op_args(&i.op, &c)? else {
|
||||
return Ok(Output::err("no target"));
|
||||
};
|
||||
// an input the FFI refuses is a harness error on both sides: the Rust port works
|
||||
// through the guarded hang regimes as upstream does (HANG-16: a `TurnNurbsOffset` of
|
||||
// 1e300 kept it busy for 25 s in the UV trim curve rebuild, nightly fuzz00282), so it
|
||||
// must not run them (fuzz mutations reach them with the C++ side refused). The
|
||||
// checks see the same input vector as the C++ side and this B-rep's own counts.
|
||||
let mut input = head(i)?;
|
||||
input.extend(&args);
|
||||
ffi::validate(
|
||||
op,
|
||||
&path_of(&i.brep)?,
|
||||
&input,
|
||||
&c,
|
||||
b.infinite_region().is_some(),
|
||||
)
|
||||
.map_err(|e| ffi_err(&e))?;
|
||||
let mut second = match i.op {
|
||||
OpIn::Merge { .. } | OpIn::CopyVertices { .. } => Some(own_brep(&mut k, i)?.map_err(
|
||||
|_| harness("the second build of the B-rep failed where the first did not"),
|
||||
@@ -1999,6 +2016,26 @@ mod tests {
|
||||
};
|
||||
let err = run_cpp(&case(1e300)).unwrap_err().to_string();
|
||||
assert!(err.contains("HANG-16"), "{err}");
|
||||
// the Rust side applies the same guard (`ffi::validate`): the port works through
|
||||
// the regime for tens of seconds (nightly fuzz00282, a 1e300 TurnNurbsOffset)
|
||||
let err = run_rust(&case(1e300)).unwrap_err().to_string();
|
||||
assert!(err.contains("HANG-16"), "{err}");
|
||||
assert!(run_cpp(&case(0.5)).is_ok());
|
||||
}
|
||||
|
||||
/// Nightly fuzz case `topology.brep_p3/fuzz00282` (`nurbs_off_box_with_large_fillet_-0.05`
|
||||
/// with `/offset = 1e+300`): the FFI refuses the C++ side (HANG-16) and the Rust side
|
||||
/// must be refused the same way instead of running the operation (it took 25 s).
|
||||
#[test]
|
||||
fn huge_offset_is_refused_on_both_sides() {
|
||||
let v = serde_json::json!({
|
||||
"brep": {"kind": "file", "file": "box_with_large_fillet.smb", "rebuild": false},
|
||||
"editing": -1, "op": "turn_nurbs_offset", "offset": 1e300
|
||||
});
|
||||
let i: Inputs = serde_json::from_value(v).unwrap();
|
||||
let cpp = run_cpp(&i).unwrap_err().to_string();
|
||||
let rust = run_rust(&i).unwrap_err().to_string();
|
||||
assert!(cpp.contains("HANG-16"), "{cpp}");
|
||||
assert_eq!(cpp, rust);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"unit": "topology.brep_p3",
|
||||
"cases": [
|
||||
{
|
||||
"name": "fuzz00282_turn_nurbs_offset_1e300",
|
||||
"origin": "nightly fuzz runs 40177686438 and 40298730423: topology.brep_p3/fuzz00282 (timeout: rust: no result after 20 s; base nurbs_off_box_with_large_fillet_-0.05, mutation /offset = 1e+300)",
|
||||
"issue": "HANG-16",
|
||||
"cpp": "harness: invalid argument: geometry value beyond 1e150 (HANG-16)",
|
||||
"rust": "harness: invalid argument: geometry value beyond 1e150 (HANG-16)",
|
||||
"inputs": {
|
||||
"brep": {
|
||||
"kind": "file",
|
||||
"file": "box_with_large_fillet.smb",
|
||||
"rebuild": false
|
||||
},
|
||||
"editing": -1,
|
||||
"op": "turn_nurbs_offset",
|
||||
"offset": 1e+300
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -221,26 +221,27 @@ fn raw(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs `op` on `input` (see the module docs); `path` is the `.smb` file of a file
|
||||
/// B-rep block (ignored otherwise).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed input; [`FfiError::Status`] for a shim
|
||||
/// failure.
|
||||
pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
/// The checks of [`eval`] that need no B-rep: the standalone ops' whole input (then
|
||||
/// `None`: nothing is left to check), else the B-rep block, the editing flag and the
|
||||
/// HANG-16 magnitude bound, returning the reader at the op's arguments and the head
|
||||
/// (the B-rep block and flags) for the export.
|
||||
fn check_head<'a>(
|
||||
op: Op,
|
||||
path: &str,
|
||||
input: &'a [f64],
|
||||
) -> Result<Option<(Reader<'a>, &'a [f64])>, FfiError> {
|
||||
let mut rd = Reader::new(input);
|
||||
match op {
|
||||
Op::Construct => {
|
||||
rd.finite()?;
|
||||
rd.finite()?;
|
||||
rd.done()?;
|
||||
return raw(op, path, input);
|
||||
return Ok(None);
|
||||
}
|
||||
Op::ApiCopyNull => {
|
||||
int(&mut rd, 0, 0)?;
|
||||
rd.done()?;
|
||||
return raw(op, path, input);
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -258,6 +259,59 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
{
|
||||
return Err(bad("geometry value beyond 1e150 (HANG-16)"));
|
||||
}
|
||||
Ok(Some((rd, head)))
|
||||
}
|
||||
|
||||
/// The op's arguments after the head, checked against the B-rep's entity counts
|
||||
/// `counts` (`topology_vertexuse::counts` order) and whether it has an infinite region,
|
||||
/// then nothing may be left.
|
||||
fn check_args(
|
||||
op: Op,
|
||||
rd: &mut Reader<'_>,
|
||||
counts: &[usize; 10],
|
||||
has_infinite: bool,
|
||||
) -> Result<(), FfiError> {
|
||||
args(op, rd, counts, has_infinite)?;
|
||||
rd.done()
|
||||
}
|
||||
|
||||
/// The checks [`eval`] makes before calling the shim, without running it or exporting
|
||||
/// the B-rep: `counts` and `has_infinite` describe the B-rep the caller built from the
|
||||
/// same input (the export's counts in `eval`; the runner's Rust side passes its own
|
||||
/// B-rep's). The runner applies them to the Rust side, so an input the FFI refuses is a
|
||||
/// harness error on both sides and the Rust port never runs the guarded hang regimes: a
|
||||
/// `TurnNurbsOffset` of `1e300` on `box_with_large_fillet.smb` (nightly fuzz case
|
||||
/// `fuzz00282`) is refused here for the C++ (HANG-16) while the port, faithful to
|
||||
/// upstream, spent 25 s rebuilding the face's UV trim curves on the approximated offset
|
||||
/// surface and exceeded the fuzz time limit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed input, as [`eval`].
|
||||
pub fn validate(
|
||||
op: Op,
|
||||
path: &str,
|
||||
input: &[f64],
|
||||
counts: &[usize; 10],
|
||||
has_infinite: bool,
|
||||
) -> Result<(), FfiError> {
|
||||
match check_head(op, path, input)? {
|
||||
None => Ok(()),
|
||||
Some((mut rd, _)) => check_args(op, &mut rd, counts, has_infinite),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `op` on `input` (see the module docs); `path` is the `.smb` file of a file
|
||||
/// B-rep block (ignored otherwise).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed input; [`FfiError::Status`] for a shim
|
||||
/// failure.
|
||||
pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
let Some((mut rd, head)) = check_head(op, path, input)? else {
|
||||
return raw(op, path, input);
|
||||
};
|
||||
let export = p2::eval(p2::Op::Export, path, head)?;
|
||||
let Some(c) = vu::counts(&export) else {
|
||||
// the B-rep could not be built: every op reports the same failure
|
||||
@@ -265,7 +319,6 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
};
|
||||
let extras = p2::eval(p2::Op::Extras, path, head)?;
|
||||
let has_infinite = extras.get(2).is_some_and(|&r| r >= 0.0);
|
||||
args(op, &mut rd, &c, has_infinite)?;
|
||||
rd.done()?;
|
||||
check_args(op, &mut rd, &c, has_infinite)?;
|
||||
raw(op, path, input)
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# topology.brep_p3: a 1e300 `TurnNurbsOffset` kept the Rust side busy for 25 s where the FFI refuses the C++ (nightly fuzz00282) (2026-10-27)
|
||||
|
||||
- **Unit:** `topology.brep_p3` (`SmBrep::TurnToNURBS`, the `SmOffsetSurface` arm). PR #1358.
|
||||
- **Inputs:** nightly fuzz case `topology.brep_p3/fuzz00282` of runs 40177686438 (2026-10-25)
|
||||
and 40298730423 (2026-10-26): fixture `nurbs_off_box_with_large_fillet_-0.05`
|
||||
(`box_with_large_fillet.smb`, `turn_nurbs_offset`, `editing = -1`) with the mutation
|
||||
`/offset = 1e+300`. Verdict both nights: `timeout`, `rust: no result after 20 s`.
|
||||
- **Both sides:**
|
||||
- C++: not run. The FFI refuses the input before calling in:
|
||||
`invalid argument: geometry value beyond 1e150 (HANG-16)` (`solidcore-ffi`
|
||||
`topology_brep_p3::eval`, `MAX_GEOMETRY`): geometry beyond 1e150 overflows the squared
|
||||
distances of `SmCurve::GlobalCoincidenceChecker`, reached by the UV trim curve rebuild
|
||||
of `TurnToNURBS`, and upstream never returns.
|
||||
- Rust (`--digest-dir`, Rust side alone, Apple M-series): returns after **25.4 s** with
|
||||
status `0` and a full output record (every face surface a B-spline, type code 14033,
|
||||
and their natural UV domains); so not an endless loop, but far past the fuzz harness's
|
||||
20 s limit, and the CI runner is slower.
|
||||
- **Where the time goes** (macOS `sample` of the running process, 6 s window, 3495
|
||||
samples on the main thread, all under `turn_to_nurbs_in`): `topology::brep::p2::
|
||||
replace_surface` -> `face::p3::create_uv_trim_curves_notified` -> `edgeuse::p3::
|
||||
create_uv_trim_curve` -> `intersect::surface_drop_lib::p1::drop_and_trim_curve` ->
|
||||
`drop_curve_trace` -> `surface_tracer::tracer_surface_cache` -> `TrimSrfCache::build_tree`
|
||||
-> `SurfaceCache::subdivide…` (`subdivide_node_split`, `patch_boxes`, `is_flat_cheap`,
|
||||
`calculate_bounding_box_of`, `net_constants` / `compute_net_constants`, `Surface::eval`,
|
||||
`basis_eval_into`). The approximated offset surface has control points of magnitude 1e300
|
||||
(the B-rep tolerance is 1e-6 scale), so the trimmed-surface cache that the curve drop
|
||||
builds for every edge of the face subdivides until its existing node and depth guards
|
||||
stop it. The offset approximation itself (`nurbs::srf_offset`, 24 refinements, 10 000
|
||||
samples) and the bounding box / extension steps are not where the time goes.
|
||||
- **Cause:** a port faithful to upstream in a regime the FFI keeps the C++ out of (HANG-16),
|
||||
the class of issue #504 (Rust library loops on inputs the FFI refuses). The fuzz harness
|
||||
runs the Rust side on a guard-refused input on purpose, to find exactly this.
|
||||
- **Fix (harness, PR #502's convention):** `topology_brep_p3::eval` is split into
|
||||
`validate` (every check it made before calling the shim) and the call; the runner's
|
||||
Rust side applies `validate` to the same input vector the C++ side gets, so the case is a
|
||||
harness error on both sides and the Rust port never runs it (`fuzz` verdict `rejected`).
|
||||
Regression: `crates/difftest/tests/fuzz_corpus/topology.brep_p3.json`
|
||||
(`fuzz00282_turn_nurbs_offset_1e300`, both sides `harness: invalid argument: geometry
|
||||
value beyond 1e150 (HANG-16)`) and the runner tests `huge_geometry_is_rejected` (Rust side
|
||||
added) and `huge_offset_is_refused_on_both_sides`. A gate fixture is not possible: a
|
||||
harness error always fails a case.
|
||||
- **Library follow-up (not done here, #504):** the cost is the trimmed-surface cache
|
||||
subdivision of `drop_and_trim_curve` on a surface whose control points dwarf the
|
||||
tolerance; the subdivision is bounded (it returned), only slow. A work bound belongs to
|
||||
the owners of `surface.surface_cache_p*` / `surface.trim_srf_cache` (draft #818 covers the
|
||||
surface-cache subdivision slice of #504); the reproducer is this corpus case run through
|
||||
`turn_to_nurbs` directly.
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user