mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #2103 from khanahmad4527/fix-ub286-bez-segments-null-write
fix: UB-286 — sm_CreateBezSegments writes through a null pointer for a one-span curve with an unclamped end (fuzz guard)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -53,7 +53,7 @@
|
||||
//! and surface blocks; conic axes whose squared length does not overflow, else the conic
|
||||
//! has no NURB to evaluate, and conic domains within [`MAX_CONIC_ANGLE`], HANG-4); the run-time state checks are the shim's.
|
||||
|
||||
use crate::validate::{bad, NOW};
|
||||
use crate::validate::{bad, bez_segments_write_null, NOW};
|
||||
use crate::{call_with_buffer, FfiError};
|
||||
|
||||
/// Largest conic domain end (radians) accepted (HANG-4).
|
||||
@@ -494,7 +494,8 @@ impl Reader<'_> {
|
||||
self.int(0, 1).map(|_| ())
|
||||
}
|
||||
/// `n p m cpts(x y z w) knots` with `n + p + 1 == m`, positive weights, nondecreasing
|
||||
/// knots.
|
||||
/// knots, and not one span with an end that is not clamped (UB-286: the curve's
|
||||
/// cache writes through a null pointer in `sm_CreateBezSegments`).
|
||||
fn nurb(&mut self) -> Result<(), FfiError> {
|
||||
let n = self.int(1, MAX_POINTS)?;
|
||||
let p = self.int(1, MAX_DEGREE)?;
|
||||
@@ -509,6 +510,7 @@ impl Reader<'_> {
|
||||
return Err(bad("weight"));
|
||||
}
|
||||
}
|
||||
let mut knots = Vec::new();
|
||||
let mut prev = f64::NEG_INFINITY;
|
||||
for _ in 0..=m {
|
||||
let u = self.finite()?;
|
||||
@@ -516,6 +518,11 @@ impl Reader<'_> {
|
||||
return Err(bad("knots"));
|
||||
}
|
||||
prev = u;
|
||||
knots.push(u);
|
||||
}
|
||||
let p = usize::try_from(p).map_err(|_| bad("nurb counts"))?;
|
||||
if bez_segments_write_null(p, &knots) {
|
||||
return Err(bad("knots: one span with an unclamped end (UB-286)"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,6 +26,35 @@ pub(crate) fn euclid([x, y, z, w]: [f64; 4]) -> [f64; 3] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `sm_CreateBezSegments` (`SmNurbsCrv.cpp`) writes through its null
|
||||
/// next-segment pointer `NQw` for a B-spline curve of degree `p` with `knots` (UB-286).
|
||||
/// It does when the curve has exactly one non-zero span (`N_BasisGetSpanCount`: the
|
||||
/// `i` in `p..m-p` with `U[i] != U[i+1]`), so `NQw` is never set, and a knot after
|
||||
/// that span's end run differs from it (an end that is not clamped), so the segment
|
||||
/// loop ends with `ie < m` and copies control points through `NQw`. The curve cache
|
||||
/// every B-spline curve gets (`SmCurveCache`) then segfaults. The indices are
|
||||
/// upstream's: the skip over a first knot's over-multiplicity, then the end run.
|
||||
pub(crate) fn bez_segments_write_null(p: usize, knots: &[f64]) -> bool {
|
||||
let Some(m) = knots.len().checked_sub(1) else {
|
||||
return false;
|
||||
};
|
||||
let u = |i: usize| knots.get(i).copied();
|
||||
let nsp = (p..m.saturating_sub(p))
|
||||
.filter(|&i| u(i) != u(i + 1))
|
||||
.count();
|
||||
if nsp != 1 {
|
||||
return false;
|
||||
}
|
||||
let mut ie = p + 1;
|
||||
while ie < m && u(ie - 1) == u(ie) {
|
||||
ie += 1;
|
||||
}
|
||||
while ie < m && u(ie) == u(ie + 1) {
|
||||
ie += 1;
|
||||
}
|
||||
ie < m
|
||||
}
|
||||
|
||||
/// Checks a clamped knot vector of degree `p`; returns its range.
|
||||
pub(crate) fn check_knots(knots: &[f64], p: usize, what: &str) -> Result<(f64, f64), FfiError> {
|
||||
let m = knots
|
||||
@@ -65,3 +94,36 @@ pub(crate) fn check_knots(knots: &[f64], p: usize, what: &str) -> Result<(f64, f
|
||||
}
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// UB-286: one span and an end that is not clamped (the curve of
|
||||
/// `topology.proto_topology_p2` fuzz00353), and the curves around it that
|
||||
/// `sm_CreateBezSegments` handles.
|
||||
#[test]
|
||||
fn bez_segments_write_null_only_for_one_unclamped_span() {
|
||||
let (a, b) = (-4.774869788871685, -3.774869788871685);
|
||||
let fuzz00353 = [a, a, a, a, a, b, b, b, b, -0.0];
|
||||
assert!(bez_segments_write_null(4, &fuzz00353));
|
||||
let clamped = [a, a, a, a, a, b, b, b, b, b];
|
||||
assert!(!bez_segments_write_null(4, &clamped));
|
||||
// degree 2, one span [0, 1], then a knot past it
|
||||
assert!(bez_segments_write_null(2, &[0.0, 0.0, 0.0, 1.0, 1.0, 2.0]));
|
||||
// the domain u2..u3 is one span here too, and 2, 3 run on past it
|
||||
assert!(bez_segments_write_null(2, &[0.0, 0.0, 0.0, 1.0, 2.0, 3.0]));
|
||||
// two spans: the first segment sets NQw (unclamped and clamped ends)
|
||||
assert!(!bez_segments_write_null(
|
||||
2,
|
||||
&[0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 3.0]
|
||||
));
|
||||
assert!(!bez_segments_write_null(
|
||||
2,
|
||||
&[0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0]
|
||||
));
|
||||
// no span (the caller refuses it with SM_ERR first), and no knots
|
||||
assert!(!bez_segments_write_null(1, &[0.0, 0.0, 0.0, 0.0]));
|
||||
assert!(!bez_segments_write_null(1, &[]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#### UB-286. `sm_CreateBezSegments` writes through a null next-segment pointer for a one-span curve whose end is not clamped
|
||||
- **Severity:** Crash / undefined behaviour
|
||||
- **Area:** Curves
|
||||
- **Location:** `source/SMLib/src/SmNurbsCrv.cpp`, `sm_CreateBezSegments` (def. `:478`), called by the B-spline branch of `SmCurveCache`'s Bezier span build (`source/SMLib/src/SmCurveCache.cpp:1299`). The function keeps `NQw = NULL` and sets it to the next segment's control points only `if( iq LT nsp-1 )`. After each segment's end knot run it copies control points through `NQw` whenever `ie LT m`: `InitCP(Qw[p],&NQw[save])` in the knot insertion branch and `InitCP(Pw[ie-p+i],&NQw[i])` after it.
|
||||
- **What happens:** for a curve with exactly one non-zero span (`N_BasisGetSpanCount`: the `i` in `p..m-p` with `U[i] != U[i+1]`), `NQw` is never set. If the knots after that span's end run on to a different value (the end is not clamped), the loop leaves the run with `ie < m` and writes through the null `NQw`: SIGSEGV, a write to address 0. With two or more spans, `NQw` still points at the last segment's own control points, and the same writes overwrite them without crashing. The caller already refuses a curve without a span (`NonZeroSpanCount < 1` is `SM_ERR`).
|
||||
- **Trigger:** any curve cache of such a B-spline curve. Nightly fuzz case `topology.proto_topology_p2` fuzz00353 (`--fuzz 600 --seeds 40`; base `gen0683`, mutation `/objects/2/curve/bspline/u/9 = -0.0`) gives a degree-4 curve the knots `-4.77 ×5, -3.77 ×4, -0.0`: the last knot no longer closes the end run, so `ie` stops at 8 of `m` 9. The knot vector is still non-decreasing; #1967's sweep listed the case as "non-monotone knots", which it is not.
|
||||
- **How found:** #1967's fuzz sweep left it open as an FFI guard gap. Reproduced on main with the nightly command. The kernel log names the fault: `segfault at 0 ... error 6` (a write) at file offset `0xbfd3d2f` of the difftest binary. That is virtual address `0xbfd4d2f`, which `addr2line`/`nm` resolve to `sm_CreateBezSegments(curve*, SmTArray<curve*>&) + 0x54f`. Reading the function gives the condition above.
|
||||
- **Confirmed:** yes, crash in the C++ reference run (prebuilt release harness at the pin, x86-64 Linux, 2026-11-01).
|
||||
- **Rust port:** not affected. `nurbs::nurbs_crv::create_bez_segments` holds the next segment as an `Option` and returns an index error when it is unset, where upstream writes through `NULL`. The fuzz case returns without a panic.
|
||||
- **Guard:** `solidcore_ffi::validate::bez_segments_write_null` replays `sm_CreateBezSegments`' indices: the span count, the skip over a first knot's over-multiplicity, and the first segment's end run. The proto script's curve validator (`topology.proto_topology_p1`, shared by p2 and p3) refuses exactly those curves, and the fuzz case is in `crates/difftest/tests/fuzz_corpus/topology.proto_topology_p2.json`. Other units that send B-spline curves to a C++ curve cache can use the same check.
|
||||
Reference in New Issue
Block a user