port: surface.surface_point_solve_p1
130 KiB
Upstream SMLib issues
Defects and hazards in the upstream C++ (source/SMLib, source/SM_API) found while
porting it to Rust. Problems only: porting notes, harness details and Rust-side choices
stay in plan/findings.md. Behaviour that is odd but plausibly intended is left out.
- Upstream commit:
c9c5979(NVIDIA usd-brep). Paths are relative to the upstream root. Line numbers were checked against that commit. - Security. Entries marked [PSIRT] are crafted-file memory or stack exhaustion,
or out-of-bounds or uninitialised reads that file input can reach. Per upstream
SECURITY.md, report these to NVIDIA PSIRT (psirt@nvidia.com or the web form), not as public GitHub issues. The other out-of-bounds entries need a bad argument from the calling code. Whoever files them should still decide whether they count as security issues. - Fields.
- How found: "read during porting" means source reading only. The other values are "generated difftest case", "adversarial / edge-case fixture", or "crash in the C++ reference run" (the difftest harness linking the upstream build).
- Confirmed: yes only when a C++ run showed the behaviour: a crash, or a named difftest case or fixture that compares against it. Otherwise no. In most "no" cases the harness deliberately keeps that input away from the C++ side.
- Build. The reference build is the upstream release configuration (
-O2, noSM_DEBUG_CODE,SM_USE_OLDTOL) with-ffp-contract=off, on x86-64 Linux / LP64.
Severity order: crash/UB, hang, nondeterminism, wrong result, docs. Entries are grouped by area within each severity, so IDs are in order within each area but not across a whole severity.
Numbering. A new entry takes the next free number in its series (UB-, HANG-,
ND-, WR-, DOC-) at the moment its PR merges. If another PR took that number in the
meantime, whoever merges renumbers the new entry and every reference to it (here, in
plan/findings.md, plan/modules.toml and code comments). Numbers are not reused.
Crash / undefined behaviour
File reading
UB-1. SmTopologyData::ReadFromDB crashes on old ASCII .smb files [PSIRT]
- Location:
source/SMLib/src/SmBrepData.cpp:4307,SmTopologyData::ReadFromDB(def.:4261). Reached fromSmApiReadBrepFromFile(source/SM_API/src/SmApiImportExport.cpp:12) andSmBrep::ReadFromFile(SmBrep.cpp:31982). - What happens: Reading
DB version 30ASCII files writes through a NULLSmShellData(thelDBVersionNumber <= 31branch). Depending on heap state, the read either segfaults or fails with a non-success status. - Trigger:
SmApiReadBrepFromFileonTestFiles/pt_TestFiles/SmallBox.smborPlatform.smbread as ASCII. - How found: crash in the C++ reference run (geom.object, geom.tools fixtures).
- Confirmed: yes (intermittent; depends on heap layout).
- Severity: crash/UB.
- Cause: These files are part files (
ReadPartFromFile, version 32). Read as version 30 the ASCII extractions fail, the uninitialised section countlCountreachesSetSizeasSM_BIG_ULONGor more,SetSizerefuses it and leaves the arrayNULL, and the record loop callsReadFromDBonNULL + i(see UB-75). - Rust port:
topology.brep_datareads B-reps without this: unreadable counts are bounded by the input left and returnInvalidInput. Its difftest reads both files correctly as version 32 (fixturespart_SmallBox_db_v32,part_Platform_db_v32).
UB-2. Readers allocate whatever counts a file header declares [PSIRT]
- Location:
SmBSplineSurface.cpp:6367SmBSplineSurface::ReadFromDB,SmBSplineCurve.cpp:7237SmBSplineCurve::ReadFromDBand the otherReadFromDBreaders. AlsoSmCompositeCurve.cpp:4169SmCompositeCurve::ReadFromDB: an unreadable ASCII segment count leavesSM_UNDEF_ULONG(123456789), which is thencalloc'd and read with uninitialised locals. - What happens: Counts from the header go straight to
malloc/callocand are not checked against the data left.n + p + 1can overflow. ASCII readers keep going with stale values after a missing field. - Trigger: A
.smc/.smsrecord whose header declares e.g.n = 2^40control points followed by a few bytes. - How found: read during porting (commit security review of
nurbs.b_spline_surface_p1). - Confirmed: no.
- Severity: crash/UB (memory exhaustion, integer overflow).
- Rust port: Checks each count against the bytes left (
DatabaseReader::ensure_available) after upstream's own validation, and returnsInvalidInput.
UB-3. Polymorphic curve readers recurse without a depth limit [PSIRT]
- Location:
SmCurve.cpp:23709SmCurve::ReadFromDBdispatching toSmProjectedCurve.cpp:690SmProjectedCurve::ReadFromDB(and to the offset and iso curve readers), which read their base curve throughSmCurve::ReadFromDBagain. - What happens: Each nesting level costs a few bytes of input and one stack frame chain, so a crafted file overflows the stack.
- Trigger: A file of N nested projected-curve records with N large (thousands).
- How found: read during porting (commit security review of
curve.projected_curve). - Confirmed: no.
- Severity: crash/UB (stack exhaustion).
- Rust port: Limits nesting to 64 levels (
DatabaseReader::enter_nested) and returnsInvalidInputfor deeper input.
UB-4. Binary short reads return success with uninitialised values [PSIRT]
- Location:
SmDatabaseIO.cpp:566/:598SmDatabaseIO::ReadLong,:473SmDatabaseIO::ReadLongs(version >= 40path). - What happens: When the data ends, the reads land in uninitialised locals and the functions still return
SM_SUCCESS. Callers then use the garbage, for example as counts. - Trigger: A binary stream truncated inside a
longfield. - How found: read during porting.
- Confirmed: no. The difftest compares only the status there.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-5. ReadType overflows its 256-byte buffer unless libstdc++ bounds the read [PSIRT]
- Location:
SmDatabaseIO.cpp:400,SmDatabaseIO::ReadType(istream >> char*intosBuff[256]). - What happens: An ASCII word longer than 255 bytes overflows
sBuff. GCC 11+ libstdc++ only caps the extraction under__OPTIMIZE__, so-O0builds and other standard libraries overflow. - Trigger: An ASCII record whose type word is 300 non-space bytes, read by a
-O0build. - How found: read during porting.
- Confirmed: no. The
-O2reference build caps the read. - Severity: crash/UB.
- Rust port: Caps the word at 255 bytes, as the optimised build does.
UB-6. Other ASCII readers use uninitialised locals after a failed extraction [PSIRT]
- Location:
SmAttribute.cpp:210SmAttributeData::ReadIndexedAttributesFromDB(uninitialised count when the stream is already failed or at EOF; a count nearULONG_MAXwrapsSetSize);SmLine.cpp:2505SmLine::ReadFromDBandSmBSplineCurve::ReadFromDB(short header leavesn,puninitialised before the allocation);SmEllipse.cpp:1488SmPolarConversion::ReadFromDB;SmConic.cpp:905SmConic::ReadFromDB(uninitialisedlConicType);SmCrvInVolume.cpp:1404SmCrvInVolume::ReadFromDB(dereferences a NULL volume forSmVolume_TYPE). - What happens: Once
failbit/eofbitis set,>>stores nothing, and the readers go on using the unset locals. - Trigger: A record cut short in its header, or holding a non-numeric token where a count is expected.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInputfor unreadable headers and bounds every count.
UB-75. SmBrepData readers use uninitialised locals and unchecked SetSize [PSIRT]
- Location:
source/SMLib/src/SmBrepData.cpp:2933SmBrepData::ReadFromDB(section countlCountuninitialised at:2942; everySetSize(lCount)unchecked);:3245(lEdgeCount,lEdgeuninitialised in the composite edge loop);:5098SmEdgeData::ReadFromDB(dMin,dMaxuninitialised);:3613SmBrepData::ReadFromFile(lDBVersionNumberuninitialised when the ASCII header extraction stores nothing; the binaryReadLongstatus is ignored);:5393ReadCurveFromDBcomposite branch (lNumSegments,lDimension,lClosed,dGapStart,dGapEnd). - What happens: After a failed ASCII extraction (stream at its end, or a non-numeric token) these locals keep indeterminate values and drive loops and allocations.
SmTArray::SetSizerefuses sizes ofSM_BIG_ULONG(99999999) or more andSM_UNDEF_ULONGand leaves the array empty orNULL, but the record loops still index it up to the count; recordReadFromDBstatuses are ignored. - Trigger: A truncated ASCII
.smb; a section count of100000000with a few records after it. - How found: read during porting (
topology.brep_data); UB-1 is one instance. - Confirmed: no (the difftest guard keeps such streams away from the C++ side), except through UB-1.
- Severity: crash/UB.
- Rust port: Uses
0for the uninitialised locals, rejects counts the remaining input cannot hold (InvalidInput), appends records as they are read, and stops a record loop withInvalidInputonce the stream has failed or hit end of data before the declared count (otherwise each further record would consume no input and the count bound would not cap memory).
UB-76. ReadCurveFromDB indexes rAllAttributes past its end after a failed attribute line skip [PSIRT]
- Location:
source/SMLib/src/SmBrepData.cpp:5480SmBrepData::ReadCurveFromDBand:5736ReadSurfaceFromDB, afterSmAttribute.cpp:260(GOTO_NEXT_ATTLINEinSmAttributeData::ReadIndexedAttributesFromDB). - What happens: When an ASCII attribute line has more than 1000 characters (or a
0xFFbyte) after its indices,ReadIndexedAttributesFromDBreturnsSM_ERRwith the indices filled in but before it growsrAllAttributesand creates the placeholders. The callers ignore the status and readrAllAttributes[index]past the array's end (curves thenNERaNULLslot; surfaces skip it). - Trigger: A curve record of a version 31+
.smbread with attributes whose attribute line is1 7followed by 1001 blanks. - How found: read during porting (
topology.brep_data). - Confirmed: no for the geometry readers (guarded). The record readers' behaviour (indices kept, no placeholders) is compared by the fixture
region_attribute_line_too_long_v33. - Severity: crash/UB.
- Rust port: Keeps the indices as upstream (
read_indexed_attributes_from_db_partial), returnsInvalidInputfor an index past the table andSM_ERR_NULL_POINTERfor an empty slot of a curve.
UB-77. Old composite curves: a closed one without segments reads sGaps[ULONG_MAX]; nesting is unbounded [PSIRT]
- Location:
source/SMLib/src/SmBrepData.cpp:5456(new SmCompositeCurve(lDimension, sCurves, bClosed, &sSenses, &sGaps)) intoSmCompositeCurve.cpp:1090((*cpGaps)[m_lNumSegments-1]); recursion atSmBrepData.cpp:5445. - What happens: A version 32 or older composite curve record with segment count 0 and the closed flag set indexes the empty gap array at
ULONG_MAX. Each nested composite record recurses intoReadCurveFromDBwith no depth limit. - Trigger:
Curve Type 14032/0 3 1; or thousands of nested composite records. - How found: read during porting (
topology.brep_data). - Confirmed: no (guarded).
- Severity: crash/UB (out-of-bounds read, stack exhaustion).
- Rust port:
CompositeCurve::with_curvesreturnsInvalidInput; nesting is bounded byDatabaseReader::enter_nested(64).
NLib: knots and basis functions
UB-7. N_BasisEval uses an uninitialised span for an unknown side flag
- Location:
NL_BasisBasic.cpp:2899,N_BasisEval(def.:2838). - What happens:
N_BasisFindSpanreturnsNL_CAL_ERRfor a flag that is neitherNL_LEFTnorNL_RIGHT. The return value is ignored and the unset spaniis used to indexU. - Trigger:
N_BasisEval(knt, p, u, /*flg=*/0, N, &spn)withustrictly inside the domain. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Takes a
Sideenum, so the case cannot be expressed.
UB-8. Span and basis routines read outside short knot vectors
- Location:
NL_BasisBasic.cpp:1429N_BasisFindSpan,:2838N_BasisEval(stack arraysleft/right[NL_MAXDEG + 2]at:2846). - What happens: With
m < 2p+1the routines read outsideU. Withp > NL_MAXDEG + 1,N_BasisEvaloverflows its stack arrays. - Trigger:
p = 3with knots[0,0,0,1,1]; orp = 20. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-9. N_BasisDerivs overflows an int
- Location:
NL_BasisBasic.cpp:3154,N_BasisDerivs(r *= (p - k);). - What happens: The factor
p!/(p-k)!is anintand is computed one step past the last row it scales, so it overflows (signed overflow, UB). - Trigger:
p >= 13withder >= 10. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-10. N_BasisDerivsArray walks spans with no bound
- Location:
NL_BasisAdv.cpp:1122,N_BasisDerivsArray. - What happens: Only
u[0]andu[n]go throughN_BasisFindSpan. The inner spans are found by walking forward with no upper bound, so they read pastU[m]. The sameintfactor overflow as UB-9 applies. - Trigger:
NL_LEFTwith an inner parameter equal toU[m-p]of a clamped vector; or any inner parameter past the end. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-11. N_BasisInsertKnots reads one past (or before) X
- Location:
NL_BasisAdv.cpp:253,N_BasisInsertKnots. - What happens: After the last new knot is inserted, the merge step compares
X[mx+1]. An emptyXreadsX[-1]. - Trigger: Any call: one slot past the end is always read. Or an empty insert vector.
- How found: read during porting.
- Confirmed: no. The read never changes the output in practice.
- Severity: crash/UB.
- Rust port: Skips the comparison. Empty
XisInvalidInput.
UB-12. N_GetCompatibleKnotVectorToTol reads U[-1] and can overflow its buffer
- Location:
NL_BasisBasic.cpp:2483,N_GetCompatibleKnotVectorToTol. - What happens: The "move to average" test reads
U[-1]when a vector's first knot is above the current smallest knot. It writes into a buffer ofsum(m)+1slots per vector, which a vector with fewer thank-1knots overflows. - Trigger: Two vectors where the second starts above the first's first knot; or
kvectors one of which has fewer thank-1knots. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Skips the test for such vectors. The short-vector case is
InvalidInput.
UB-13. N_MakeKnotsCompatible / N_GetCompatibleKnotArrayMult read past U[m]
- Location:
NL_BasisBasic.cpp:3665N_MakeKnotsCompatible,NL_BasisAdv.cpp:119N_GetCompatibleKnotArrayMult. - What happens: Both read past
U[m]on several inputs. Their output buffers are caller-allocated and unbounded. - Trigger: Degree-0 vectors whose ends differ by less than
NL_PTOL; inner knots withinNL_PTOLof an end; unclamped vectors. - How found: read during porting.
- Confirmed: no. The shim pads with
+infso that upstream stays in bounds. - Severity: crash/UB.
- Rust port: Returns
InvalidInputfor the out-of-range reads.
UB-14. NL_PascalTri[18][18] is indexed past its end
- Location:
source/SMLib/inc/NL_Globals.h:67(the table); users includeNL_Bezier.cpp:105,:118(elevation / product matrices) and:5417N_BezCrvExtend(henceN_BezSrfExtend4D). - What happens: Degree sums above 17 index past the table.
- Trigger: Elevating a degree-10 Bezier by 10.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-15. N_BezReduceDegree with p = 1 reads unwritten / negative indices
- Location:
NL_Bezier.cpp:2540,N_BezReduceDegree. - What happens: With
p = 1it reads the never-writtenalf[0]andQw[-1]. - Trigger:
N_BezReduceDegreeon a degree-1 segment. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInputforp < 2.
UB-16. Bezier matrices returned in freed stack storage
- Location:
NL_Bezier.cpp:301N_BezElevateDegree(matrix built at:319on local stacks freed at:353). The same pattern appears inN_BezSrfElevateDegreeandN_BezFuncMultiplyBez*. - What happens: When the caller passes a NULL
NL_RMATRIX, the matrix is built on a localNL_STACKSthatN_EndNurbsfrees before returning. The caller is left holding dangling storage. - Trigger: Call
N_BezElevateDegreewith a NULLdm, then reusedm. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (use after free).
- Rust port: Builds a local matrix when the caller's is null. Callers wanting reuse build it first.
NLib: matrices and polynomials
UB-17. Banded and triangular matrix routines index outside rows
- Location:
NL_Math.cpp:5714N_RealMatrixMultiply,:6300N_RealMatrixTranspose(e.g.:6395),:6463N_RealMatrixMultiplyPtArray(:6538). - What happens: With an even bandwidth,
j - i + bw/2reachesbw, one column past the row. An in-place banded transpose writes past its new(n+1)-column rows whenbw > n + 1. Non-square triangular operands read outside their rows. - Trigger: A banded matrix with
bw = 2; an in-place transpose withbw > n + 1; a 2x3 triangular operand. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-18. N_SrfPowerBasisEvalDerivs overflows up[NL_MAXDEG+1]
- Location:
NL_SrfPoly.cpp:286(array),:315(vp = &up[p + 1]),:333(write), inN_SrfPowerBasisEvalDerivs(def.:276). Also reached from the power-basisSmBSplineSurfaceEvaluateSimple. - What happens: The v-power rows are stored after the
p+1u-powers in the same 18-entry stack array. The writes overrun it whenp + 1 + min(vdr,q)*(q+1) + q > 17. - Trigger: A bicubic surface with
vdr = 3. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (stack buffer overflow).
- Rust port: Returns
InvalidInput.
UB-19. N_SrfPowerBasisEvalDerivs rational branch tests a stale variable
- Location:
NL_SrfPoly.cpp:491,N_SrfPowerBasisEvalDerivs. - What happens: The final loop tests
k1 EQ k2.k1is a leftover loop variable ((k2+1)*(q+1)); the intended test isudr == vdr. Withudr > vdrthe loop reads pastSD/CwDers. - Trigger: A rational surface with
mfl = NL_TRUE,udr = 2,vdr = 1. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Keeps the test so results match. The out-of-range case is
InvalidInput.
UB-20. SmPolynomial::SolveQuarticEqn reads an uninitialised root
- Location:
SmPolynomial.cpp:365,SmPolynomial::SolveQuarticEqn(def.:279). - What happens:
z = adCSol[0]is taken from the resolvent cubic without checking the root count. WithdZeroTolerance > 1the cubic degrades to a quadratic that may have no roots. - Trigger: Any quartic with
r != 0and zero tolerance> 1. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Uses
z = 0.0.
UB-21. SmMatrix::Compute3x3EigenVectors returns uninitialised eigenvectors
- Location:
SmMatrix.cpp:1482,SmMatrix::Compute3x3EigenVectors. - What happens: It ignores the
EigenSystemJacobiSolverstatus. For a non-symmetric matrix it returnsSM_SUCCESSwith eigenvectors read from an uninitialisedSmMatrix. It is alsoconstbut mutatesthisthrough a cast. - Trigger: A non-symmetric 3x3 matrix.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Reports
SM_ERR.
UB-22. SmMatrix::LUDecompose uses a fixed stack buffer it does not check
- Location:
SmMatrix.cpp:662,SmMatrix::LUDecompose. - What happens: It scales with a stack
double[MAX_MATRIX_SIZE](1000). OnlySolveLinearSystem(:843) checks the size. - Trigger:
LUDecomposecalled directly on a 1001x1001 matrix. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Uses a
Vec. The harness limits sizes.
NLib: curves, fitting, surfaces
UB-23. N_CrvSumDiffCrv corrupts the heap for rational sums
- Location:
NL_CrvAdv.cpp:10058N_CrvSumDiffCrv; products at:10136/:10142, sums at:10155/:10160. - What happens:
N_CrvFuncMultiply*rescales the knot vector of an operand in place between the two products. Whentmpcomes out with more control points thannum, the loop overtmp'snwrites pastnum's control points. - Trigger: A rational sum of two curves over different parameter ranges, or with interior knots closer than
NL_PTOLbut not equal. - How found: crash in the C++ reference run (
double free or corruption,free(): invalid next size). - Confirmed: yes.
- Severity: crash/UB (heap overflow).
- Rust port: Returns
InvalidInput.
UB-24. ST_curxcrr in place frees an uninitialised curve
- Location:
NL_CrvAdv.cpp:4179-4182(EXIT: if( curP EQ curQ ) N_FreeCrv( &curA, SP ););curAis assigned only at:3724. FunctionST_curxcrr(def.:3540). - What happens: An in-place
NL_G2call that recurses, or fails before:3724, reachesEXITand frees stack garbage. - Trigger: In-place
N_CrvExtendByParamDistwithNL_G2and a parameter more than0.9213ranges outside the curve. - How found: crash in the C++ reference run (SIGSEGV).
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: Has no such free and completes the extension.
UB-25. N_CrvApproxSpiral frees uninitialised stacks on its own input errors
- Location:
NL_Spiral.cpp:108,:111,:120,:123(NL_ERRORchecks) run beforeN_InitNurbs(&SL)at:148. TheEXITpath callsN_EndNurbs(&SL). FunctionN_CrvApproxSpiral(def.:88). - What happens: Invalid input jumps to
EXIT, which frees uninitialised local stacks.SmBSplineCurve::CreateHelixSegment(SmBSplineCurveNLib.cpp:539) validates first, but its checks let some of these inputs through: height in[-SM_EFF_ZERO, NL_MTOL), both radii belowNL_MTOL, turns in[SM_EFF_ZERO, NL_MTOL). - Trigger:
height = 0,r0 = r1 = 2,turns = 5. - How found: crash in the C++ reference run (
free(): invalid pointer). - Confirmed: yes.
- Severity: crash/UB.
- Rust port: Returns
NlibCode::Input(helix:SM_ERR).
UB-26. N_CreateRevolvedSrf overflows wi[9]
- Location:
NL_SrfCommon.cpp:1408,N_CreateRevolvedSrf(def.:1398). - What happens: The circle weights from
N_CalcCircWeightsgo intoNL_REAL wi[9]. A quartic revolution of 241 degrees or more has 13 weights. - Trigger:
ctp = NL_QUARTIC,al = 270degrees. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (stack buffer overflow).
- Rust port: Keeps all the weights.
UB-27. N_FitCrvFirstDerivAndKnots writes M[2i+1][-1]
- Location:
NL_CrvFit.cpp:7923,N_FitCrvFirstDerivAndKnots. - What happens: When the span of
u[i]givesl == 0, the routine writes column-1of row2i+1. The banded matrix is one row-major block, so this overwrites the last entry of row2iwith a wrong value. - Trigger: Knots for which
u[i]falls in a span givingl == 0(not produced by its driverN_FitCalcKnotsDerivs). - How found: adversarial / edge-case fixture
first_derivs_row_underflow_p2. - Confirmed: yes. The C++ matches the port bit for bit through the bad write.
- Severity: crash/UB (out-of-row write; silently wrong matrix).
- Rust port: Writes exactly there so that results match.
UB-28. Curve fitting routines index outside their inputs
- Location:
NL_CrvFit.cpp:2458N_FitCalcKnotVectorEndDerivs(andN_FitCalcKnotVectorDeriv);:5461N_FitCrvApproxKnots;:2273N_FitCrvInterpGivenParams;:2716N_FitCrvDerivs/N_FitCubicSplineInterp. - What happens:
p == 1with one parameter readsu[1]/u[-1].N_FitCrvApproxKnotscopiesn+p+2knots before checking the count, writesPw[1]forn == 0, and indexesBcorrectly only for non-decreasing parameters.2 <= n < pgivesN_BasisEvaltoo few knots.- A zero end derivative is estimated from
P[0..=3]whatever the point count, which reads out of bounds below four points.
- Trigger:
N_FitCrvDerivswith 3 points and zeroDs. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-29. Curve tools trust their inputs
- Location:
NL_CrvTool.cpp:65N_CrvInsertKnot,:1369N_CrvRefine, plusN_CrvSplit,N_CrvDecomposeBez,N_CrvExtractCrvSeg,N_CrvRemoveKnotMaxErr;NL_SrfSymbol.cpp:4165N_CrossBoundDerivCrvNonRatSrf. - What happens: No validation. Unclamped knots, multiplicity above
p, out-of-rangeul/ur, or a too-small surface index outside the arrays. - Trigger:
N_CrvInsertKnoton a curve with an inner knot of multiplicityp+1. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-30. Reparametrisation writes at negative indices
- Location:
NL_CrvBasic.cpp:5005N_CrvReparamToInterval;NL_SrfBasic.cpp:5095N_SrfReparamToInterval. - What happens: They write
U[m-p..]form < p(andU[r-p..]forr < p). - Trigger: A curve record with
m < p. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput, leaving the curve unchanged.
UB-31. NLib curve helpers read values their callees did not write
- Location:
NL_CrvBasic.cpp:3256N_CrvGetNumAndDenom: allocates the numerator, then fails before writing it.NL_CrvBasic.cpp:5364N_CrvIsPlanar: readsangafter a failedN_VectorsAngle.NL_CrvAdv.cpp:12688N_LinearMultiplyCrv: ignores theN_CrvGetGrevilleAbscissaerror, then uses the parameter.NL_CrvCommon.cpp:75N_CreateQuadraticArcTol: readsN_IsectLineLine's unsetintsflag.NL_Geom.cpp:2868N_DistPtLineSegand:6068N_CPtsAreColinear: no distance written for NaN input, which is then compared.
- What happens: Results depend on uninitialised memory.
- Trigger: A degree-0 curve for
N_LinearMultiplyCrv; zero end tangents forN_CreateQuadraticArcTol; a zero weight forN_CPtsAreColinear. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Propagates the callee error (or returns NaN /
falsewhere documented).
UB-32. NLib surface routines read out of range or uninitialised data
- Location:
NL_SrfAdv.cpp:696ST_surxsdr: the degree-1 reflection fallback readsRw[ii][kby ± 2]outside its row. Also reached throughN_SrfExtendByDist(:661).:10401N_CreateSkinSpine: readspSpineParams[-1]for one profile with end derivatives.:17403N_ApproxCrossBoundaryDerivs: ignores theN_VectorsAnglestatus, then inserts an uninitialised knotUX[0].:13262N_CreateSkinSrfBoundaryContinuity: uses an uninitialised derivative curve withNL_UVDIR.
- What happens: Out-of-range or uninitialised reads. Upstream then "succeeds" with whatever it read.
- Trigger: G1/G2 extension in a degree-1 direction with a degenerate first reflection normal;
eps = 0with an exactly aligned end tangent. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInputor the angle error.
UB-33. sm_CrvDerivs / sm_CrvRationalBasisDerivs overrun fixed derivative arrays
- Location:
SmNurbsCrv.cpp:1281-1288(sm_CrvDerivs:BD_STACK[GW_MAX_DERIV], but the guard isder > GW_MAX_DERIV);SmNurbsCrv.cpp:1182sm_CrvRationalBasisDerivs, reached fromSmOffsetCurve::Evaluate(SmOffsetCurve.cpp:141). - What happens:
der == 5writes past the row arrays. A zero-distance offset of a rational B-spline evaluated with more thanNL_MAXDER(4) derivatives overflows a fixed stack array. - Trigger:
sm_CrvDerivs(..., der = 5, ...);SmOffsetCurve(distance 0, rational base)Evaluatewith 5 derivatives. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (stack buffer overflow).
- Rust port: Rejects
der >= 5. The harness limits offset evaluation to 4 derivatives.
Spatial structures and small geometry
UB-34. SmObjsInVoxels::IncMaxItemCount re-points buckets into the array it is about to free
- Location:
source/SMLib/inc/SmHashTable.h:1803(bucket items re-pointed at&m_sPointItems.GetAt(id), which is still the old array) then:1810(SetArrayfrees the old array). FunctionSmObjsInVoxels<TYPE>::IncMaxItemCount(def.:1747). - What happens: Once a model that still has items in buckets grows, every bucket item dangles, and
FindCoincidentPairsreads freed memory. Upstream comments atSmHashTable.h:2113andSmHealData.cpp:3126describe the hazard and reserve capacity up front. The growth path itself is unchanged. - Trigger:
SmObjsInVoxels::Addbeyond the reserved size (size + 7 > GetDataSize()) while items are in buckets, thenFindCoincidentPairs. - How found: read during porting.
- Confirmed: no. The shim refuses these programs.
- Severity: crash/UB (use after free).
- Rust port: Stores indices, not pointers.
UB-35. MapPointToVoxel converts negative doubles to ULONG
- Location:
SmHashTable.h:1957-1959,SmObjsInVoxels<TYPE>::MapPointToVoxel. - What happens:
(ULONG)((x - min) / len)is UB for negative values. On x86-64 g++ it wraps, so a point below the box lands in the top voxel after the clamp (the comment says the low end). NaN maps high;+infmaps to 0. - Trigger: Add a point with
x < box.min.x. - How found: read during porting; generated difftest cases.
- Confirmed: yes. The port emulates the x86-64 conversion and matches.
- Severity: crash/UB (wrong voxel in practice).
- Rust port: Emulates the x86-64 result (
ulong_from_f64).
UB-36. FindCoincidentPairs reads GetAt(0) of an empty bucket list
- Location:
SmHashTable.h:2360,SmObjsInVoxels<TYPE>::FindCoincidentPairs. - What happens: An empty
pOptBucketIndicesis read at index 0. - Trigger:
FindCoincidentPairswith an empty, non-NULL bucket index array. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Checks no bucket.
UB-37. SmPolarBox::Union loops over an unset count
- Location:
SmPolarBox.cpp:1031SmPolarBox::Union;:930GetBoundaryVectors. - What happens:
GetBoundaryVectorsreturnsSM_ERR_INVALID_INPUTwithout setting its count when one box isSM_PS_UNINITIALIZED(possible afterReSet, which keeps a stale basis).Unionthen loops over the unset count. - Trigger:
ReSetbox A, thenA.Union(B)with aligned stale bases. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-38. Point trees divide by zero in AddToSpatialTree
- Location:
SmTree.cpp:255SmTree::AddToSpatialTree(point-tree constructors at:110/:164). - What happens: A point tree never initialises its object-list memory manager, so adding an object divides by zero.
- Trigger:
SmTree(point, size, ...)thenAddToSpatialTree. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Refuses the operation.
UB-39. SmGrid indexing and conversion hazards
- Location:
SmGrid.cpp:881Step3DDDA(stepping after an exit indexes voxelm_lOut*);:1143GetVoxelAddress(NaN toULONGfor a zero-size axis);:369~SmGrid(readsm_aVoxels[0][0]with a zero voxel count);:525AddElement(..., SM_GE_HIERARCHY, ...)(dereferences a NULL element). - What happens: Out-of-bounds reads, UB conversions, NULL dereference.
- Trigger: Call
Step3DDDAagain afterrbExitedGrid = TRUE; build a grid with a zero extent on one axis. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput, or a saturating cast / constructor check.
UB-40. smgu_PolygonArea with no points reads sPoints[-1]
- Location:
SmGeomUtility.cpp:274(sLastPoint = sPoints[lNumPoints-1]),smgu_PolygonArea(def.:265). Reached fromSmPointSet3d::GetArea(source/SMLib/inc/SmPointSet.h:358). - What happens:
lNumPoints - 1wraps toULONG_MAX. - Trigger:
smgu_PolygonAreawith 0 points;GetAreaon an empty point sequence. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput(point set: area-1).
UB-41. SmPointSet3d::GetTightPBox on an empty planar set reads m_pData[0]
- Location:
source/SMLib/inc/SmPointSet.h:181GetTightPBox, viaSmPointSet3d::SetUpTightPseudoBox(SmPointSet.cpp:631). - What happens: An empty set built with a normal is classified planar, and the planar extra work reads the first point.
- Trigger: An empty
SmPointSet3dwith a normal, thenGetTightPBox(). - How found: crash in the C++ reference run (segfault).
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: Skips the extra work and uses
(0,0,0)for missing points.
UB-42. SmTransform::EvaluateBoundingBoxSimple dereferences a NULL pseudo box
- Location:
SmTransform.cpp:1224,SmTransform::EvaluateBoundingBoxSimple. - What happens: When an output box is requested,
AreAligned(*pOptParamPseudoBox, ...)runs before theNULLcheck. - Trigger: The default call (
pOptParamPseudoBox = NULL) with an output box requested. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-43. Small object-model NULL dereferences
- Location:
source/SMLib/inc/SmVector2d.h:541SmVector2d::operator[](non-const, index > 1);source/SMLib/inc/SmObject.h:331-332SmTemporaryChangeValue::UseOrigValue/UseTempValueon a default-constructed guard. - What happens: Both dereference NULL.
- Trigger:
v[2] = 1.0on anSmVector2d;SmTemporaryChangeValue<double> g; g.UseOrigValue();. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
SolidError::Upstream { code: 1001 }, or does nothing.
UB-44. Attribute lifetime bugs
- Location:
SmAttribute.cpp:1619SmAttribute::~SmAttribute: deletes a dependent attribute twice, throughRemoveAttribute->RemoveUser. A user that does not list the attribute leaves dangling lists.:816SmMergeRegionAttribute::Merge: dereferences NULL when both inputs have a merge-region attribute and the result does not, and casts anySM_AI_OTHER_REGION_IDattribute to the merge-region class.- The colour average casts whatever
SM_AI_COLORattribute it finds. :1038SmMergeRegionAttribute::SetRegions: writes past the "other" lists when they are shorter.SmAObject.cpp:88SmAObject::Notify(SM_NO_REG_PROPAGATION): with two or more other regions it indexes the refilled list with the previous count (stale or out-of-range), andSET_FROM_OTHERcan read an attribute it just deleted.
- What happens: Double delete, use after free, NULL dereference, bad casts, out-of-bounds writes.
- Trigger: Delete a dependent attribute that still has users; merge two merge-region-attributed objects into one without.
- How found: read during porting.
- Confirmed: no. The difftest skips these scripts.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-45. A tracker started twice leaves a dangling callback
- Location:
SmTrackTopologyChanges.cpp:434StartTracking,:512StopTracking. - What happens: The second
StartTrackingstores the tracker's own callback as "previous".StopTracking(and the destructor) then does nothing, and the context keeps a danglingSmNotifyCallback*. The nextNotifycalls into a destroyed object. - Trigger:
t.StartTracking(); t.StartTracking();destroyt, then any notify. - How found: read during porting.
- Confirmed: no. The shim skips destroying a tracker in that state.
- Severity: crash/UB (use after free).
- Rust port: Identifies callbacks by key, so the case cannot arise.
UB-85. SmTopologySolver::MayContainAnswer reads uninitialised ray/box results
- Location:
SmTopologySolver.cpp:2909,SmTopologySolver::MayContainAnswer(def.:2716),SM_SO_RAYFIREbranch. - What happens: The status of
SmExtent3d::IntersectLineis ignored. When it fails (SM_ERR_INVALID_INPUTfor a ray vector shorter thanSM_EFF_ZEROor an undefined box),lNumFound,dTEnteranddTExitare read uninitialised and decide whether the branch is culled. - Trigger: A ray-fire topology solve whose option vector is (near) zero.
- How found: read during porting (
intersect.topology_solver_p1). - Confirmed: no (the difftest guard keeps a ray vector shorter than
1e-6fromMayContainAnswer; zero and tiny option vectors elsewhere are defined upstream and are difftested byfixtures/intersect.topology_solver_p1/zero_vectors.json). - Severity: crash/UB (uninitialised read).
- Rust port: Returns
InvalidInput.
UB-86. Topology edge solvers dereference a curve's missing edge
- Location:
SmTopologySolver.cpp:557/:582SmTLSPointEdge::SolveIt,:1079/:1101SmTLSCurveEdge::SolveIt,:1359/:1380SmTLSEdgeEdge::SolveIt. - What happens:
pCurve->GetEdge()is only checked by anSM_ASSERT_MSG(compiled out); with no shape (m_pShape == NULL) a curve that defines no edge is dereferenced asNULL->GetInterval(). - Trigger: Any of these local solvers called on a free curve (no owning edge) outside a shape solve.
- How found: read during porting (
intersect.topology_solver_p1). - Confirmed: no (the difftest guard passes only curves that own an edge).
- Severity: crash/UB (bad argument from calling code).
- Rust port: Returns
InvalidInput.
Curves
UB-46. NURB-less curves are dereferenced
- Location:
SmBSplineCurve.cpp:43:SmBSplineCurve(lDim)has a NULL NURB, which nearly every query dereferences (GetOrCreateGwNurbPointerasserts in debug only).SmHyperbola.cpp:422MakeNurb(:441,:470) andSmParabola.cpp:419(:435): whensmgu_LineLineClosestPointfails,MakeNurbfails. The constructor ignores this, andGlobalPointSolveSTEPthen dereferences the missing NURB in the curve cache.
- What happens: NULL dereference.
- Trigger: A hyperbola or parabola with a degenerate interval (parallel end tangents) or zero focal distance, then
GlobalPointSolveSTEP. - How found: read during porting.
- Confirmed: no. The shim refuses these inputs.
- Severity: crash/UB.
- Rust port: Returns
SM_ERR.
UB-47. SmBSplineCurve helpers write or read past their arrays
- Location:
SmBSplineCurve.cpp:5109GetControlPointsExpert: writes two values per point for a dimension-1 curve.SmBSplineCurveNLib.cpp:2723InterpolatePoints: copiescpOptParams->GetSize()values into an array ofm + 1.SmBSplineCurveNLib.cpp:3038DeformToEndPointTargets: ignoresDegreeElevate's status. On a zero-length knot interval it then runs degree-3 indexing on the degree-1 curve.SmBSplineCurve.cpp:5922RemoveExtraKnots: on a borrowed NURB, swaps the copy in but keepsm_bNurbIsBorrowed = TRUE, and its temporary frees the borrowed curve.
- What happens: Out-of-bounds reads/writes and freeing memory the object does not own.
- Trigger:
DeformToEndPointTargetson a line whose NURB has knots[a,a,a,a];InterpolatePointswith 3 points and 4 parameters. - How found: read during porting. The
DeformToEndPointTargetscase ran in the C++ build without a visible failure. - Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-48. Copying a curve that owns a Hermite base
- Location:
SmProjectedCurve.cpp:76,SmOffsetCurve.cpp:53,SmCompositeCurve.cpp:1111,SmCrvInVolume.cpp:102(copy constructors). - What happens:
SmHermiteCurvehas noCopy(baseSmCurve::CopyreturnsSM_ERR), and the copy constructors ignore the status. The results:- a projected curve with a NULL base;
- an offset curve sharing the base pointer, then a double free;
- a composite curve with a NULL segment;
- an in-volume curve with both objects owning one curve, then a double delete.
- Trigger: Copy an
SmOffsetCurvethat owns anSmHermiteCurvebase. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (double free, NULL dereference).
- Rust port: Returns the
SM_ERR, or owns the base by value.
UB-49. SmCrvInVolume reads empty arrays
- Location:
SmCrvInVolume.cpp:911GenerateMults,:992GetNumberNaturalKnots,:623CalculateContinuities. - What happens:
GenerateMults' state check returns before sizing its output, soGetNumberNaturalKnotsreads past an empty array. With no breaks,CalculateContinuitiesreadsSetAt(0)of an empty array and loops onsize - 1underflow. - Trigger:
GetKnotswith an interval on a cached curve;CalculateContinuitieswith no breaks. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-50. SmCurve::FixChainCurvesList uses deleted curves
- Location:
SmCurve.cpp:946(also:968,:989,:1010),SmCurve::FixChainCurvesList(def.:854). - What happens: With
bDeleteDuplicates,delete pBSp1; pBSp1 = NULL;leaves the dangling pointer inr3DCurves[j]. The next scan dereferences it, and a reversed duplicate can be deleted twice. A later branch can pick the NULL local as the next link and dereference it. - Trigger: Two identical B-spline segments, one reversed, with
bDeleteDuplicates = TRUE. - How found: read during porting.
- Confirmed: no. The generator drops these cases.
- Severity: crash/UB (use after free, double free).
- Rust port: Removes the duplicate once. A link choice equal to the deleted curve is
InvalidInput.
UB-51. sm_UnitizeTangent reads rPVV[2] of a 2-element array
- Location:
SmCurve.cpp:4549sm_UnitizeTangent, called from:4620sm_FilterSolutionwithSmVector3d sPV1[2]/sPV2[2]. - What happens: When the first derivative is not longer than
SM_EFF_ZERO, the second derivative is read fromrPVV[2], past the stack array. - Trigger: A curve whose first and second derivatives both vanish at the solution.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Reads
Vec3::UNDEFthere.
UB-52. Curve solvers dereference optional pointers without checks
- Location:
SmCurve.cpp:14907SmCurvePropertyEFO::Evaluate:SM_CP_PROJECTED_POINT_*readsm_cpOptVectors;SM_CP_DISTANCE_TO_POINT/SM_CP_CYLINDER_INTERSECTIONreadm_cpdPropertyValue. Also,SM_CP_MAXIMIZE_RADIUS_OF_CURVATUREwith zero curvature falls through into a 0/0.:17170SmFindCCNormalENFO::Evaluate: readsm_cpOptVectors[0..1]forSM_SO_SIGNED_PIVOT_MINIMIZE.:4881SmCCIGlobalSolver::LocalSolve: casts each leaf'sm_pDatatoSmBezierSpan(the aux-type check is anSM_ASSERT), which reads past anSmBezierAux1don pruned trees.SmGlobalSolver.cpp:130SmGlobalSolver::SolveIt: initialisessStartBranch[i]only fori < m_lNumTrees, so a one-tree solve passes an uninitialisedapBranch[1].
- What happens: NULL dereference, out-of-object reads, uninitialised pointer use.
- Trigger: A property solve of
SM_CP_PROJECTED_POINT_Xwith no vectors. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-53. SmCurve::FindParameterAtArcLength reads sKnots[(ULONG)-1]
- Location:
SmCurve.cpp:3928,SmCurve::FindParameterAtArcLength. - What happens: Stepping backwards from a start within
dParamTolbelow the first knot leavesik = -1, which is then used as an index. - Trigger:
dArcLength < 0fromt = U[0] - 0.5 * dParamTol. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-54. CrvsFixGaps / CrvsCleanup cast every curve to SmBSplineCurve
- Location:
SmCurve.cpp:1184SmCurve::CrvsFixGaps,:1106SmCurve::CrvsCleanup. - What happens: The non-virtual
SetControlPoint(andTrimGap) are called through a bad cast for other curve kinds. - Trigger:
CrvsFixGapson a list containing anSmLineorSmEllipse. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Moves end points of B-splines only and returns
InvalidInputfor other kinds.
UB-55. SmCurve::CrvsCleanup crashes next to a closed curve
- Location:
SmCurve.cpp:1106,SmCurve::CrvsCleanup(TrimGapbetween the previous curve and a closed curve). - What happens: Removing a tiny curve whose neighbour is closed (equal end points) segfaults inside
TrimGap. - Trigger: A cubic segment, then a tiny segment, then a closed quadratic loop.
- How found: crash in the C++ reference run (SIGSEGV; reproduced with the trigger above).
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: Completes the clean-up.
UB-56. SmCurve::DeformToEndPointTargets uses a failed approximation
- Location:
SmCurve.cpp:21563,SmCurve::DeformToEndPointTargets. - What happens: It ignores
ApproximateCurve's status, then calls the B-spline deform through*ppOptNewCurve, which is NULL on failure. - Trigger: A curve kind whose
ApproximateCurvefails. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns the error.
UB-57. Conic and ellipse uninitialised reads
- Location:
SmConic.cpp:461SmConic::IntersectImplicitConic: solves a quartic of uninitialised coefficients forSM_CT_UNKNOWN.SmCircle.cppthree-point constructor: ignoressmgu_CircleFrom3Points' status, so collinear points leave the outputs unset.SmEllipse.cpp:1674SmEllipse::ConvertTo2D: readsadSols[1]with fewer than two roots.SmEllipse.cpp:467SmPolarConversion::ConvertToNURBSParameter/ConvertToPolarParameter/IsCurrent: index empty or short tables.SmEllipse::AssertValid(:4093) uses an uninitialiseddNurbTwhen no table entry is written.
- What happens: Uninitialised reads, out-of-bounds table reads.
- Trigger:
SmCircle(P0, P1, P2)with three collinear points. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-58. SmCompositeCurve::GetKnots reads past its array, and loops forever with an interval
- Location:
SmCompositeCurve.cpp:524(lLastIndex = rKnots.GetSize()),:569(for(ii=rKnots.GetSize()-1; ii>=0; ii--)on an unsignedULONG),SmCompositeCurve::GetKnots(def.:505). - What happens: For any composite of two or more segments, the first transition reads
rKnots.GetAt(GetSize())and writesSetAt(GetSize(), ...). WithpOptIvl, the trim loop never ends and readsrKnots[ULONG_MAX]. - Trigger:
GetKnotson a two-segment composite; again with an interval. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (also a hang).
- Rust port: Returns
InvalidInput.
UB-59. SmCompositeCurve NULL dereferences
- Location:
SmCompositeCurve.cpp:595ApproximateCurve(rpNewBSplineCurve->AssertValid()afterCreateByJoiningfails without making a curve);:392CalculateBoundingBox(*pBBoxis written unconditionally). - What happens: NULL dereference.
- Trigger:
ApproximateCurveon unconnected segments;CalculateBoundingBoxwithpBBox = NULL. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns the join error /
InvalidInput.
UB-60. sm_CreateCorner reads sSolutions[0] of an empty array
- Location:
SmCompositeCurve.cpp:2178,sm_CreateCorner(def.:1785). Reached fromCreateOffsetsOfManyCurvesandCreateTrimmedOffsetswithSM_OC_LINEAR_CHAMFER. - What happens: When the chamfer line misses an extension line, the empty intersection array is read at index 0.
- Trigger: Linear-chamfer offsets of random planar polygons hit it often.
- How found: crash in the C++ reference run (segfault in the offsetters).
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-61. sm_CheckBowTies indexes by the original curve count
- Location:
SmCompositeCurve.cpp:2352,sm_CheckBowTies. - What happens: Reads
crOffsets[0],crOffsets[lNumOrig-1]andcrOrigCurves[0]without checks.IntersectAndTrimOffsetsignores its status. - Trigger: One-sided offsetting that drops degenerate offsets, leaving fewer raw offsets than original curves.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInputand goes on without bow-tie points.
UB-62. SmCrvOnSurf::GetKnots reads rKnots[0] of an empty array
- Location:
SmCrvOnSurf.cpp:1220,SmCrvOnSurf::GetKnots(def.:1145). - What happens: On an uncached curve with
pOptIvl, an interval holding no UV knot leavesrKnotsempty, andrKnots[0]is read anyway. It also readsGetAt(lCnt - 1)when the classification has no interval. - Trigger:
GetKnotswith an interval strictly between two UV knots. - How found: crash in the C++ reference run.
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
Surfaces and volumes
UB-63. SmCurveBoundedSurface::operator== reads past the UV boundary array
- Location:
SmCurveBoundedSurface.cpp:122,SmCurveBoundedSurface::operator==(def.:83). - What happens: The UV loop runs to the number of 3D boundaries, so
m_vUVBoundaries[i]is read out of bounds when there are more 3D boundaries than UV boundaries. - Trigger: Two equal surfaces with 2 3D boundaries and 1 UV boundary.
- How found: crash in the C++ reference run (difftest case
drop_curve_eq_other_same). - Confirmed: yes.
- Severity: crash/UB.
- Rust port: Stops at the end of the UV array.
UB-64. sm_CreateNonRationalCurve crashes for a rational curve on a base surface
- Location:
SmBSplineSurfaceNLib.cpp:2906,sm_CreateNonRationalCurve(file static). - What happens: Segfaults, presumably in
SmSurface::DropCurveon a surface with no face or B-rep. The root cause was not verified. - Trigger:
DropCurveCurve.smcwith unit weights (rational) onDropCurveSurface.sms. - How found: crash in the C++ reference run.
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: The drop path is not ported yet (
Unported).
UB-65. CreateCornerBlend and patch builders dereference NULL or leave dangling inputs
- Location:
SmBSplineSurfaceNLib.cpp:3008CreateCornerBlend;:4296CreateCoonsPatch(alsoCreateNSidedPatch,JoinSurface). - What happens:
CreateCornerBlenddereferences NULL for a surface without a UV curve, for a rational 3D curve with a surface, and for n-sided bilinear blends with missing UV curves or surfaces. ItsNERcheck fires only where the null count first exceeds the limit.- The patch builders hand their inputs' NURBs to NLib, which reallocates them on the method's local stack. The caller's objects dangle after the call.
- Trigger:
CreateCornerBlendwith a surface but a NULL UV curve. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput, and stores the changed NURBs back.
UB-66. smsurf_ComputeSurfacePointType writes the v result to the u output
- Location:
SmSurface.cpp:30027,smsurf_ComputeSurfacePointType. - What happens:
if (pOptSurfSideDVV) *pOptSurfSideDUU = sSurfSideDVV. The v output is never written, and asking for the v side without the u side dereferences NULL. - Trigger:
pOptSurfSideDUU = NULL,pOptSurfSideDVV != NULL. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB (also a wrong result).
- Rust port: Ported as written. The v-without-u request is rejected.
UB-67. Surface helpers read empty arrays or unset counts
- Location:
SmSurface.cpp:24009sm_SetUpBreaks(readsrBreaks[0]of an empty array);:17963SmLSIGlobalSolver::BranchMayContainAnswers(uses an unset hit count whenSmExtent3d::IntersectLinefails on a zero line vector);SmSurfaceCache.cpp:1184SmBezierAux2d::BuildPolarBox(dereferences a NULLmBA_pSurfaceonBuildTreeTopheads). - What happens: Out-of-bounds or uninitialised reads, NULL dereference.
- Trigger: A line solve with a zero direction vector.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInputor the line error.
UB-68. SmPlane hazards
- Location:
SmPlane.cpp:1536SmPlane::Evaluate:lHighestVDeriv - kwraps as aULONGwithbOnlyUpperHalfand more u than v derivatives, and writes outside the array.SmPlane.cpp:83SmPlane(Origin, Normal): with|n|^2 < SM_EFF_ZERO_SQ,MakeUnitOrthoVectorsfails, the status is ignored and the axes stay uninitialised.SmBSplineSurfaceNLib.cpp:7223SmBSplineSurface::ReverseandSmPlane.cpp:4285SmPlane::RebuildSTEPFromNURBParameters: on a plane whose NURB cannot be made (degenerate or inverted domain), they pass a NULL NURB toN_SrfReverse/EvaluatePoint.
- What happens: Out-of-bounds write, uninitialised axes, NULL dereference.
- Trigger:
SmPlane(O, (0,0,1e-13));Reverseon a plane with an inverted domain. - How found: read during porting (review of
surface.plane_p1). - Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput/SM_ERR.
UB-69. SmSurfOfRevolution constructor reads an uninitialised bOnPlane
- Location:
SmSurfOfRevolution.cpp:147-149(constructor, def.:94);SmBSplineCurve.cpp:5433SmBSplineCurve::IsOnPlane;SmGeomUtility.cpp:3829smgu_PlanePointDistance. - What happens:
IsOnPlanefails before writingbOnPlanewhensmgu_PlanePointDistancerejects the normal (|y|^2 - 1 > SM_EFF_ZERO). The constructor onlySEs the status, then readsbOnPlane. - Trigger: A planar non-line generator with a placement Y axis of length 1.1.
- How found: read during porting (PR #15 review).
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Accepts only unit placement axes for the constructor.
UB-70. SmSurfOfRevolution constructor re-enters itself through the generator
- Location:
SmSurfOfRevolution.cpp:94(constructor);:202RebuildPolarConverter. - What happens: The constructor sets the generator's owner, then evaluates it. A fresh
SmLinegenerator with no NURB runsMakeNurb, which asks the owner forGetGenDirParamExtent. That makes the surface's NURB through the virtualMakeNurbfrom inside the constructor. Separately,RebuildPolarConverterdereferences a NULL iso curve whenCreateIsoParametricCurvefails (only asserted). - Trigger:
SmSurfOfRevolutionbuilt on anSmLinefrom its two-point constructor. - How found: read during porting.
- Confirmed: no. The shim makes the line's NURB first.
- Severity: crash/UB.
- Rust port: Evaluates the generator without the owner detour, and returns the error.
UB-71. Volume NULL dereferences
- Location:
SmVolume.cpp:634SmVolume::RefreshInvOrientMap(callsMakeMapSimplethrough NULL without an orient map);SmTwistVolume.cpp:1106SmTwistVolume::EvaluateBoundingBoxSimple(writes*pProjBoxunconditionally in the unbounded branch;(long)(theta / 2 PI)of NaN);SmBSplineVolume.cpp:2645SmBSplineVolume::InvEvaluateGuessPointSimple(indexes knots by control-point index; onlySM_ASSERT). - What happens: NULL dereference, UB conversion, out-of-bounds knot reads.
- Trigger:
RefreshInvOrientMapon a volume with no orient map. - How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns
InvalidInput.
UB-72. Curve-in-domain check of a default SmUnbendVolume crashes
- Location:
SmVolume.cpp:1538SmVolume::IsCurveInInSpaceDomain->SmUnbendVolume.cpp:2467SmUnbendVolume::IsCurveInParamDomainSimple. - What happens: Segfaults in the unbend volume's curve check. That check builds its discontinuity half plane with the interval
SmExtent1d(m_dSingularityTol, -SM_BIG_DOUBLE)(:2479), i.e. min above max. Whether this inverted plane domain is the cause was not verified. - Trigger: A default-constructed
SmUnbendVolumeused as an orient map, thenIsCurveInInSpaceDomainwith a line. - How found: crash in the C++ reference run.
- Confirmed: yes.
- Severity: crash/UB.
- Rust port: The unbend volume is not ported yet. The harness rejects these chains.
UB-73. SmBSplineVolume::CalculateContinuitiesSimple samples sKnots2[ii] past its end
- Location:
source/SMLib/src/SmBSplineVolumeNLib.cpp:602and:635(SmBSplineVolume::CalculateContinuitiesSimple, def.:456); also:667(rConts[kk]). - What happens: The inner sampling loop over
jjreadssKnots2[ii](the outer index) instead ofsKnots2[jj], so it samples one iso-curve periirepeatedly and reads past the end ofsKnots2(into its 256-entry stack buffer: stale or uninitialised values) wheneversKnots1has more samples thansKnots2.rConts[kk]likewise assumes every iso-curve has as many knots as the first one classified. - Trigger:
CalculateContinuitiesSimple(.., bCalcGeometric = TRUE)on a volume whose two other directions have different sample counts (e.g. 5 and 3), in a build whereEvaluateIsoParametricCurveSimplesucceeds (USE_VOLUMES). - How found: read during porting.
- Confirmed: no (the reference build has no
USE_VOLUMES, so the loop stops at the first sample; see WR-48). - Severity: crash/UB.
- Rust port: Returns
InvalidInputwhere upstream would read past the arrays.
UB-78. SmApiCreateSkinFromFaces with no faces crashes
- Location:
source/SM_API/src/SmApiPrimitives.cpp:1335SmApiCreateSkinFromFaces->source/SMLib/src/SmPrimitiveCreation.cpp:6711SmPrimitiveCreation::CreateSkinFromFaces->SmBSplineSurfaceNLib.cppSmBSplineSurface::CreateSkinnedSurface. - What happens: With an empty face array (and no curves, which the API always passes),
CreateSkinFromFacesstill runs one layer and callsCreateSkinnedSurfacewith zero cross sections, which has no check for that and segfaults. - Trigger:
SmApiCreateSkinFromFaceswith an emptyrFaces. - How found: crash in the C++ reference run (
sweep.api_surfacesprobe case). - Confirmed: yes.
- Severity: crash/UB.
- Rust port:
create_skin_from_facesis a stub untilprimitive.primitive_creation_p3lands;BSplineSurface::create_skinned_surfacereturnsInvalidInputfor no cross sections. The harness passes at least one face.
UB-80. SmSurfOfExtrusion::EvaluateSTEP lets order 5 overrun sm_CrvDerivs
- Location:
SmSurfOfExtrusion.cpp:920-922(EvaluateSTEP, def.:897:lMaxDeriv = 5; // 5: MAXDER in NLib),:959(m_pGenCurve->EvaluateSTEP(dU, lHighestUDeriv, ..));SmNurbsCrv.cpp:1281sm_CrvDerivs(see UB-33). - What happens: The guard allows order 5, but
NL_MAXDERis 4. A B-spline generator evaluates throughSmBSplineCurve::Evaluate->sm_CrvDerivs, which writes past its fixed derivative rows:*** stack smashing detected ***. Analytic generators (SmLine,SmCircle,SmEllipse) evaluate order 5 fine. - Trigger:
EvaluateSTEP(uv, 5, 5, ..)on an extrusion of anySmBSplineCurve(degrees 1 to 3 tried). - How found: crash in the C++ reference run (
surface.surf_of_extrusion_p1generated cases). - Confirmed: yes.
- Severity: crash/UB (stack buffer overflow).
- Rust port: The B-spline evaluation rejects order 5 and
evaluate_stepreturns the error. The harness keeps order 5 of a B-spline generator away from the C++.
UB-81. sm_IntersectGenerators reads unset offsets and a possibly NULL curve list
- Location:
SmSurfOfExtrusion.cpp:2978-2986(sm_IntersectGenerators, def.:2658; arrays declared:2725-:2726). - What happens: For a range solution,
pOpt3DCurves->GetSize()is read before thepOpt3DCurves != NULLtest, andpOffsetCurve[jj][k]/dOffsetLength[jj][k]are compared although they are only set when an end of the overlap interval equals one of four bounds exactly (==on doubles) and the end overlaps at all. - Trigger: A range solution of the projected generator intersection whose overlap ends match none of the four bounds, or a caller passing
NULLcurves. - How found: read during porting.
- Confirmed: no (its projected solve,
SmCurve::GlobalCurveSolve, is not ported yet, so no range case is compared). - Severity: crash/UB.
- Rust port: A missing curve list has size 0; unset offsets compare unequal (the case falls to "too complicated").
UB-82. SmSurfOfExtrusion constructors and copy constructor trust failed calls
- Location:
SmSurfOfExtrusion.cpp:104-109and:204-209(constructors:SE(IsOnPlane(..)), then readbOnPlane);:70,:162,:170(SEof the generator evaluations whose points are then used);:252-:259(copy constructor:SE(SM_ERR)for a NULL or non-B-spline generator copy, thenm_pGenCurve->SetOwner(this)). - What happens: A failed
IsOnPlaneleavesbOnPlaneuninitialised; a failed evaluation leaves the tolerance point or origin unset; copying a surface without generator dereferences NULL. - Trigger: Copying the empty I/O surface; a generator whose evaluation fails.
- How found: read during porting.
- Confirmed: no.
- Severity: crash/UB.
- Rust port: Returns the error (
SM_ERR_NULL_POINTERfor the copy).
UB-83. SmSurface::GetDomainWithContinuity dereferences a missing surface cache
- Location:
SmSurface.cpp:12056,SmSurface::GetDomainWithContinuity(base body). - What happens: When
smsurf_GetSurfaceCachereturnsNULL(neither a trimmed surface cache nor a surface cache can be built),SE(SM_ERR)only reports it and the body goes on to callGetUContinuitiesArray()through the null pointer. - Trigger:
GetDomainWithContinuityon a curve-bounded surface or plane whose cache build fails (generatedsurface.surface_p1dom_contcases). - How found: crash in the C++ reference run.
- Confirmed: yes (segfault; the
surface.surface_p1shim now checks the cache first and reportsSM_ERR). - Severity: crash/UB.
- Rust port: Returns the cache build error.
UB-84. SmSurface::CalculateBoundingBox of a polar box recurses without bound
- Location:
SmSurface.cpp:5397,SmSurface::CalculateBoundingBox(base body), withSmSurfaceCache.cpp:1184,SmBezierAux2d::BuildPolarBox. - What happens: For a polar box, the base body asks the top node of the surface's own cache for it (
SmBezierAux2d::GetPolarBox). Building that box calls the node surface'sCalculateBoundingBox, which for a curve-bounded or STEP surface is the same base body again. Nothing ends the cycle, so the stack overflows. - Trigger:
CalculateBoundingBoxwithpPolarBox != NULLon anSmCurveBoundedSurfaceorSmSTEPSurface(any surface kind that does not overrideCalculateBoundingBox). - How found: the maintainer's review of
surface.surface_p1(PR #43), by printing the polar box in the shim; gdb shows about 58k frames cycling through those three functions. - Confirmed: yes (stack overflow in the C++ reference build).
- Severity: crash/UB.
- Rust port:
Surface::base_calculate_bounding_boxand the surface caches'calculate_bounding_boxrefuse a polar box of those kinds withInvalidInput(bounded on wasm). Thesurface.surface_p1FFI never asks the C++ for it.
UB-87. SmCone::CreateIsoCircleFromPoint and GetHeight read uninitialised parameters
- Location:
SmCone.cpp:1679-1682SmCone::CreateIsoCircleFromPoint(def.:1620);SmCone.cpp:2265-2266SmCone::GetHeight(def.:2251). - What happens:
CreateIsoCircleFromPointignores the statuses ofSTEPInversionandConvertUVFromSTEPToNURBS; when the generator drop inSmSurfOfRevolution::STEPInversionfinds no solution the height ofsStepUVis never written and is used anyway.GetHeightignoresEvaluatePoint's status and subtracts uninitialised points when the conversion fails (no polar curve). - Trigger:
CreateIsoCircleFromPointwith a reference point just outside the height interval of a steep bounded cone and a tolerance below the in-plane gap the clamp leaves;CreateIsoCircleFromPointonSmCone::CreateCanonical(O, 1, 5e-11 deg, NULL)(nobSwapUV) at(-1, 1e-13, 0.7), where the polar conversion fails andsNurbUV.y(a defaultedSmVector2d) is used unset;GetHeightof the cone of UB-81. - How found: read during porting (
surface.cone_p1). - Confirmed: no. The shim replays the inversion and refuses those inputs.
- Severity: crash/UB (uninitialised read).
- Rust port: Returns the error.
UB-88. SmCone STEP-to-NURB conversion dereferences a missing polar curve
- Location:
source/SMLib/src/SmEllipse.cpp:489,SmPolarConversion::ConvertToNURBSParameter(def.:467), reached fromSmCone.cpp:1808SmCone::ConvertUVFromSTEPToNURBSand:1682SmCone::CreateIsoCircleFromPoint. - What happens:
m_cpCurve->GetSTEPInterval()is called without theNER(m_cpCurve)check thatConvertToPolarParameterhas. A cone whose polar converter could not be set up has no curve;SmSurfOfRevolution::MakeNurbWithSweepParamsignoresRebuildPolarConverter's status, soCreateCanonicalstill succeeds. - Trigger:
new SmCylinder(O, X, Y, -1.8, 0, 360, 4.3, FALSE, FALSE, FALSE)(a negative radius) andMakeNurb(), which succeeds without polar curve, thenConvertUVFromSTEPToNURBS. - How found: read during porting (
surface.cone_p1). - Confirmed: no. The shim refuses the op for a cone without polar curve.
- Severity: crash/UB (null dereference).
- Rust port: Returns
InvalidInput.
Intersection
UB-89. SmVolume::ReadFromDB fills NULL volumes and uses unread map types [PSIRT]
- Location:
SmVolume.cpp:8104,SmVolume::ReadFromDB(theSmVolume_TYPEbranch). - What happens: (1) A next map of type
SmVolume_TYPErecurses withrpNewVolume->m_pNextMap(NULL) as the volume to fill, and the branch writesrpNewVolume->m_pNextMapthrough it. A top-levelSmVolume_TYPEread does the same with the caller's NULL. (2)rDB.ReadType(lNextType)/ReadType(lOrientType)statuses are ignored; at end of data the type stays uninitialised and is switched on. (3) The orient map read is cast toSmTransform*whatever its type, and a failed read leaves it NULL;RefreshInvOrientMapthen copies, inverts and simplifies it (a NULL or mistyped object). The nested reads' statuses are ignored throughout. - Trigger: An ASCII transform volume whose base data says
1thenObject Type 22001(next map), or1thenObject Type 22011/22004(orient map), or ends right after a1flag. - How found: read during porting.
- Confirmed: no. The harness's strict text grammar rejects these inputs.
- Severity: crash/UB.
- Rust port: Returns
SolidError::Unsupported, which the derived readers do not ignore. Nested reads are bounded byDatabaseReader::enter_nested.
UB-90. Volume inverse maps reach UB-42 through MapPseudoBox and EvaluateBoundingBox
- Location:
SmVolume.cpp:3907InvMapPoint->:2058MapPseudoBox(trim branch:EvaluateBoundingBoxSimple(sParamBox, NULL, ...));:3411InvEvaluatePointand:4512GlobalPointSolve->:1692EvaluateBoundingBox(*pOptParamDomain, NULL, ...). - What happens: With a search domain and compounding, an unoriented
SmTransformmap with next maps (or, forEvaluateBoundingBox, any transform map, and the orient map for in-space boxes) receives a NULL parameter pseudo box, andSmTransform::EvaluateBoundingBoxSimpledereferences it (UB-42). - Trigger:
InvMapPoint(P, NULL, ok, pts, gaps, &domain, TRUE)onSmTransform->SmTwistVolume. - How found: crash in the C++ reference run (segfault in
SmPseudoBox::AreAlignedunderInvMapPoint->MapPseudoBox). - Confirmed: yes.
- Severity: crash/UB.
- Rust port: Returns
SolidError::Unsupported; the FFI guard ofsurface.volume_p2models these call paths and rejects them.
UB-74. PointBasedSurfaceIntersect writes past its 20-point array
- Location:
source/SMLib/src/SmSurface.cpp:30588SmSurface::PointBasedSurfaceIntersect(UV point version). - What happens: The points go into
SmTsectPnt saTsectPnts[20]; the size limit and the equal-length check of the two point arrays areSM_ASSERTs only, so 20 or more points write past the stack array, and a shorter second array is read past its end. - Trigger: 20 point pairs, or two UV point arrays of different lengths.
- How found: read during porting (
intersect.surface_intersector_p1). - Confirmed: no.
- Severity: crash/UB.
- Rust port:
Surface::point_based_surface_intersectreturnsInvalidInputfor those inputs.
UB-78. ComputeSingularityPoint can write past aSingDirections[8]
- Location:
source/SMLib/src/SmAdvSurfaceIntersector.cpp:164(SmVector3d aSingDirections[8]), filled bysm_AddSingDir(:54) from the Dupin indicatrix intersections at:616-:685;aSingUVSurf1/2[8]andlInsideIndex[8]at:775-:776. - What happens: Every intersection of the above (and below) indicatrix curves of the two surfaces adds a direction unless one within
acos(0.9)(about 26 degrees) is stored already. Up to 13 directions pass that test, but the array holds 8 andsm_AddSingDirdoes not check the count (upstream's own comment: "we have experienced examples with 8 vectors"). Saddle points generate four hyperbolas per surface, two of which are different conics (see WR-60), so the count is not bounded by the 4 + 4 of two central conics. - Trigger: Two non-planar surface points with parallel normals, no shared principal curvature, whose indicatrices meet in more than 8 directions more than 26 degrees apart.
- How found: read during porting.
- Confirmed: no (the difftest shim predicts the count and does not run such inputs; no generated case reached it).
- Severity: crash/UB.
- Rust port: the array keeps 8 slots; a ninth direction is
InvalidInput.
UB-79. SmPointClassification calls SmObject virtuals on an SmPoint3d*
- Location:
SmPointClass.cpp:668(ComputePointParameters:SmTol::GetZoneTol3d(m_pObject)),:435(GetGap3dwith a surface, throughGetObjZoneTol3d),:493(IsConnectedTo:GetType),:917(MergeIntoObject:GetType). - What happens:
SM_PC_POINTclassifications store anSmPoint3d*cast toSmObject*(SetClassObject), and these paths call virtualSmObjectmembers on it.SmPoint3dhas noSmObjectvtable, so the call is undefined behaviour. - Trigger:
SetClassObject(SM_PC_POINT, (SmObject*)&point)followed byComputePointParameters,GetGap3d(surface),IsConnectedToorMergeIntoObject. - How found: read during porting.
- Confirmed: no (the harness keeps these calls away from the C++).
- Severity: crash/UB.
- Rust port: Treats a point like any other non-topology object (system zone tolerance, not connected, not mergeable).
Hang
HANG-1. SmExtent1d::PeriodicWrap steps one period per iteration
- Location:
SmExtent1d.cpp:907SmExtent1d::PeriodicWrap. Reached fromSmExtent2d.cpp:1038ContainsPeriodicPoint2dand from theSmLocalSolveNdperiodic clamping (SmLocalSolveNd.cpp:90). - What happens: The run time is proportional to distance / period.
- Trigger: Period
1e-9, value1e15(~1e24 iterations); a diverging Newton step of1e8with period0.01(~1e10). - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Same loop (faithful). The generator avoids such ratios.
HANG-2. N_BasisFindGlobalMax loops when its step is below the knot resolution
- Location:
NL_BasisBasic.cpp:1751,N_BasisFindGlobalMax. - What happens: The bracketing step
(U[i+p+1]-U[i])/(10p)is below half an ulp of the knots, so the loop state never changes. - Trigger: Knots around
1e17with a span of1. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Returns
InvalidInputwhen the state repeats.
HANG-3. Knot merges loop forever on NaN or non-positive tolerance
- Location:
NL_BasisBasic.cpp:2483N_GetCompatibleKnotVectorToTol(tol <= 0, or NaN knots);:3665N_MakeKnotsCompatible;NL_BasisAdv.cpp:253N_BasisInsertKnots;NL_BasisAdv.cpp:119N_GetCompatibleKnotArrayMult;NL_SrfAdv.cpp:13051N_MergeKnotVectors(all with NaN knots). - What happens: No knot compares, so no iteration advances.
- Trigger: A knot vector containing NaN;
tol = 0. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Returns
InvalidInput.
HANG-4. Conic angle loops
- Location:
NL_CrvBasic.cpp:1863N_CrvGetConicData;NL_CrvCommon.cpp:1090N_CreateEllipticalArcAngTypeTol. - What happens: These loop forever for non-finite angles, or where
ae + 360 == ae. - Trigger:
ae = inf;ae = 1e17. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Returns
InvalidInputfor non-finite angles and after 2^20 turns.
HANG-5. N_ApproxCrvWithPolyline subdivides forever below floating-point resolution
- Location:
NL_CrvGeom.cpp:1531,N_ApproxCrvWithPolyline. - What happens: The degenerate-segment test
dd < tolsqnever succeeds, so coincident control points keep splitting. Withtol <= 0the last vertex is also never appended. - Trigger:
tol = 0; or a relative tolerance on a zero-length curve. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Stops after
POLYLINE_MAX_SPLITSwithNotConverged.
HANG-6. Zero-distance G2 curve and surface extensions
- Location:
NL_CrvAdv.cpp:640ST_curxcdr(NL_G2parameter search);NL_SrfAdv.cpp:661N_SrfExtendByDist/:696ST_surxsdr(G1/G2 extend the iso-curves withST_curxcdrNL_G2). - What happens: The step
du = range * ldst / d1is zero, or no longer changes the parameter, so the search never ends. A negative distance reads outside the knot range. - Trigger:
N_SrfExtendByDist(..., dist = 0, NL_G1, ...). - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Fails with
NotConvergedwhen a step changes nothing.
HANG-7. Periodic shifts in unbounded evaluation
- Location:
NL_CrvAdv.cpp:4257N_CrvEvalUnboundedPtsAndDerivs;NL_SrfAdv.cpp:5446N_SrfEvalPtDerivsUnbounded. - What happens:
whileloops shift the parameter byue - usuntil it is in range. They never end once the shift no longer changes the parameter, and take ~1e12 steps for far parameters. - Trigger: A G1-closed curve evaluated at
u = 1e20. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Returns
NotConvergedafter a bounded number of shifts.
HANG-8. SmBSplineCurve::DampenKnotSpacing cascades without end
- Location:
SmBSplineCurveNLib.cpp:1082,SmBSplineCurve::DampenKnotSpacing. - What happens: Each pass halves every span that is more than
dMaximumDeltaFactortimes longer than a neighbour. For factors below about 2 the halving cascades without end, and a factor of 1 or less never finishes. - Trigger: Unique knots
0 .3 .6 1, factor1.2. - How found: read during porting.
- Confirmed: no. The harness simulates the splitting and rejects such inputs.
- Severity: hang.
- Rust port: Stops after
DAMPEN_SPLIT_LIMITsplits withNotConverged.
HANG-9. CreateExtendedSurface with distance 0 never stops halving
- Location:
SmBSplineSurfaceNLib.cpp:4469SmBSplineSurface::CreateExtendedSurface; halving testsif (dExtDist < dDist/100.0)at:4574,:4636,:4703,:4764. It also runs forSmPlane. - What happens: After each failed extension the distance is halved until it drops below
dDist / 100, which never happens fordDist == 0(or NaN). - Trigger:
CreateExtendedSurfacewith a distance of0. - How found: read during porting.
- Confirmed: no.
- Severity: hang.
- Rust port: Stops after 1100 halvings with
NotConverged.
HANG-10. SmCurve::GlobalCoincidenceChecker recurses without a depth bound
- Location:
SmCurve.cpp:19950,SmCurve::GlobalCoincidenceChecker. - What happens: It recurses on closed and half-closed curves with no limit. The recursive calls also force
bSnapToEnds = TRUE. - Trigger: Two coincident closed curves.
- How found: read during porting.
- Confirmed: no. Unbounded recursion was not observed.
- Severity: hang (potential stack exhaustion).
- Rust port: Stops at 200 nested splits with
InvalidInput.
HANG-11. SmCone::AdjustSTEPUVDomain never returns for a reversed sweep interval
- Location:
SmCone.cpp:458SmCone::AdjustSTEPUVDomain->SmSurfOfRevolution.cpp:300SmSurfOfRevolution::AdjustSTEPUVDomain(Reparameterize, thenMakeNurb). - What happens: The C++ reference run did not finish within 7 minutes. The domain is stored and the NURB rebuilt without checking
start <= end. Which loop spins was not located. - Trigger:
SmCone::CreateCanonical(O, 1, 2, 3, cone), thenAdjustSTEPUVDomain(SmExtent2d(90, 0, 10, 1)). - How found: adversarial / edge-case fixture (
surface.cone_p1). - Confirmed: yes.
- Severity: hang.
- Rust port:
SmSurfOfRevolution::AdjustSTEPUVDomainis not ported yet (Unported); the FFI rejects a reversed sweep.
Nondeterminism
ND-1. srand(time(NULL)) in face sampling and shell classification
- Location:
SmFace.cpp:21183SmFace::GetPointsInFace(def.:21167);SmShell.cpp:888,:905(inSmShell::IsInnerShellOf, def.:827). - What happens: The C RNG is reseeded from the clock and used to dither sample parameters / ray directions. Results differ between runs, and the global
rand()state is clobbered for the whole process. - Trigger:
GetPointsInFacewithlPointCount > 1; inner-shell tests on wire shells. - How found: read during porting (static scan of the call-graph closure,
plan/closure.md). - Confirmed: no.
- Severity: nondeterminism.
- Rust port: Draws from the seeded
kernel.rng.
ND-2. Results that depend on pointer-keyed container order
- Location:
source/SMLib/inc/SmMapPtrToPtr.h:42,:115(std::unordered_mapkeyed by pointers);source/SMLib/inc/SmRelation.h:52SmRelation(GetSeconds,GetAllFirsts,GetAllSeconds);SmBrep.cpp:28891(SmBrep::Transform, range-for over an edge set);SmFilletExecutive.cpp:531;SmHCR.cpp:312;SmTopologyIntersector.cpp:945. - What happens: Iteration order is pointer-hash order, so it can change from run to run with heap layout. So can anything order-dependent downstream (created topology order, tie-breaks).
- Trigger: Any of these paths on the same input under different allocators or ASLR.
- How found: read during porting (static scan,
plan/closure.md). - Confirmed: no. The shims sort upstream results before comparing.
- Severity: nondeterminism.
- Rust port: Uses ordered maps / insertion order on arena ids.
ND-3. A function-local static fixed by the first call
- Location:
SmFilletSolver.cpp:3604,SmFilletSolver::ComputePointValues(def.:3544). - What happens:
static double sdStaticStep = (rTsectPnt.m_ePointType == SM_IP_TANGENT_POINT) ? 1.0e-3 : 1.0e-5;is initialised on the first call only. Every later call in the process uses the step chosen for the first point's type, so results depend on call history. (SmFilletExecutive.cpp:1503-1504also use function statics, but with constant initialisers.) - Trigger: Two fillet runs in one process whose first intersection points differ in type, run in either order.
- How found: read during porting (static scan,
plan/closure.md). - Confirmed: no. The fillet units are not ported yet.
- Severity: nondeterminism (order-dependent results).
- Rust port: Not ported yet. The plan is to compute the step per call.
Wrong result
Geometry and tolerances
WR-1. Two-context SmNewMarkAndLock advances and locks the wrong context
- Location:
source/SMLib/inc/SmContext.h:528-529(binary constructor). - What happens: With
p1 != p2it callsm_pContext1->NewMark(m_eMarkType1)(context 1 advanced twice) andLockMark(m_pContext2, m_eMarkType1)(wrong mark type). The destructor unlocksm_eMarkType2, so a stray lock stays on context 2. - Trigger:
SmNewMarkAndLock(c1, c2)where the free mark types ofc1andc2differ. - How found: adversarial / edge-case fixture
boolean_pair_two_contexts. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Reproduces it (
MarkLock::new_pair).
WR-2. Release builds do not enforce mark locks
- Location:
SmContext.cpp:331SmContext::NewMark(andLockMark/UnlockMark). - What happens: The lock checks are
SM_ASSERT*only, so in release builds locked marks can be advanced and double locks are silent. - Trigger:
NewMarkon a locked mark type. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result (release-only).
- Rust port: Drops the assertions (matches release).
WR-3. SmExtent3d::ContainsLineSeg3d accepts one end inside
- Location:
SmExtent3d.cpp:1235(bRtn |= ContainsPoint3d(rEndPoint, dTol)),SmExtent3d::ContainsLineSeg3d(def.:1227). - What happens: The comment at
:1233says the box contains the segment if it contains both points, but the code ORs the two tests. - Trigger: A segment from inside to far outside the box returns TRUE.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-4. SmExtent3d::ApproximateUnbounded computes max from the replaced min
- Location:
SmExtent3d.cpp:1760,SmExtent3d::ApproximateUnbounded(SM_BT_UNBOUNDED_MAXbranch). - What happens: Each axis's max is computed from the already replaced min. The optional expanded extent is left untouched when
SetMinMaxrejects it. - Trigger: An extent with infinite min and finite max on one axis.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is. The rejected second extent is
None.
WR-5. SmExtent3d::Circumscribe reads the axis map backwards
- Location:
SmExtent3d.cpp:129,SmExtent3d::Circumscribe(SmPseudoBox). - What happens:
IsAxisAlignedfillsalAxisMap[i]with the axis that basisiis parallel to, butCircumscribereads it as the basis parallel to axisi. Swaps and sign flips come out right; 3-cycles do not. - Trigger: Basis
(y, z, x)with intervals[1,2], [3,5], [-7,-6]: gives min(3, -7, 1)instead of(-7, 1, 3). - How found: adversarial / edge-case fixture
circumscribe_axis_cycle_quirk. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-6. SmPeriodicExtent1d::AddInterval on an empty extent gives the full period
- Location:
SmPeriodicExtent1d.cpp:262,SmPeriodicExtent1d::AddInterval. - What happens: The empty branch tests
(m_dPeriod - |m_dMax - m_dMin|) < ScaledZeroon the empty sentinels[1e20, -1e20]. This is true for any period below2e20, so the result is[0, period]full instead of[from, to]. - Trigger: Empty extent, period 1,
AddInterval(0.2, 0.3). - How found: adversarial / edge-case fixture
add_empty_quirk. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-7. SmPseudoBox::DistanceToPoint returns a status as a distance
- Location:
SmPseudoBox.cpp:772,SmPseudoBox::DistanceToPoint. - What happens: Returns
SM_ERR(1001.0) as the distance when the point is outside more than three planes. Whensmgu_IntersectTwoPlanes/ThreePlanesfail, it uses theSM_UNDEF_DOUBLEline or vertex. - Trigger: An inside-out box (
Init()state). - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-8. SmTransform::EvaluateBoundingBoxSimple aligned branch is not the image of the box
- Location:
SmTransform.cpp:1224,SmTransform::EvaluateBoundingBoxSimple. - What happens: It scales parameter intervals by
1/|axis|and shifts them byorigin·axis, which is not the transformed box (e.g. under a pure translation). - Trigger: A translation-only transform with an aligned parameter box.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-9. SmPolarBox::AddVector3d relabels a region as an arc without transposing
- Location:
SmPolarBox.cpp:696,SmPolarBox::AddVector3d(SM_PS_REGIONcase). - What happens: When the x range collapses, the basis is rotated and the state becomes
SM_PS_ARC, but the domain is re-set to its own bounds. The arc's span stays in y while arc code reads x. - Trigger: A region box whose x range collapses below
SM_EFF_ZERO_SQRT. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-10. smgu_BestFitLineToPoints takes a fixed eigenvector, not the best-fit direction
- Location:
SmGeomUtility.cpp:4223,smgu_BestFitLineToPoints.smgu_BestFitPlaneToPointsuses the same vector as a plane normal. - What happens: It takes
aEigenVectors[2]ofC = del*I - M. The comment calls this the largest eigenvalue's vector, but the least-squares direction minimisesv^T C v. In practice the result depends on the Jacobi solver and the sort. - Trigger: Axis-aligned collinear points give a direction perpendicular to them (classified
SM_PST_SCATTERED). Points along(1,1,1)give the right direction. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-11. smgu_CircleFindSilhouettePoints perspective case omits the centre
- Location:
SmGeomUtility.cpp:2185,smgu_CircleFindSilhouettePoints. - What happens: The perspective case returns rotated unit directions times the radius, without adding the circle centre. The parallel case adds it.
- Trigger: A circle centred at
(10,0,0)viewed in perspective. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-12. SmObjsInVoxels::FindCoincidentPairs misses pairs and changes the model
- Location:
source/SMLib/inc/SmHashTable.h:2360,SmObjsInVoxels<TYPE>::FindCoincidentPairs. - What happens:
pBegPointItem/rBegPointare bound once per bucket and never advanced, so only head-vs-other pairs are found.rEndPointis a reference, sorEndPoint = pEndPointItem->m_sTgtPoint3doverwrites stored points during the search. - Trigger: Three coincident points in one voxel. The pair not involving the head is never reported.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-13. SmHashTable::IncMaxItemCount zeroes bucket items after all were removed
- Location:
source/SMLib/inc/SmHashTable.h:804,SmHashTable<TYPE>::IncMaxItemCount. - What happens: Records are copied only while
GetUsedBucketItemCount() > 0. Otherwise they come back calloc-zeroed (bucket 0, NULL item, id 0), and a laterRemoveof such an item walks bucket 0 while the counters still increment. - Trigger: Add items, remove all, grow, then remove one.
- How found: generated difftest case.
- Confirmed: yes ("reproduced").
- Severity: wrong result.
- Rust port: Reproduced.
WR-14. SmPointSet3d::SetUpTightPseudoBox discards its projections
- Location:
SmPointSet.cpp:631,SmPointSet3d::SetUpTightPseudoBox. - What happens:
sBegPt0.ProjectPointToPlane(..);isconstand returns the projection, which is discarded. Angles are measured between unprojected points. - Trigger: A slightly non-planar point set: the basis differs in the last bits from a projecting implementation.
- How found: generated difftest case (a projecting port differed at ~1e-15).
- Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-15. SmPointSet3d::SetUpCentroid drops 50 points for sizes that are multiples of 50
- Location:
SmPointSet.cpp:410,SmPointSet3d::SetUpCentroid. - What happens: The first block holds
size % 50points and the loop runssize / 50 + (extras ? 1 : 0)times. For multiples of 50, the last 50 points are never added (50 points give(0,0,0)).m_bCentroidSetis never set, and afterGetNormalthe centroid comes from the best-fit plane instead, soGetCentroiddepends on call order. - Trigger:
GetCentroidon 50 points. - How found: adversarial / edge-case fixtures
centroid_blocks_*. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
Topology, attributes, solvers
WR-16. SmTopology::Notify merge uses the deleted object as the source
- Location:
SmTopology.cpp:220(and:239),SmTopology::Notify(def.:187). - What happens:
SM_NO_MERGE_IN_BREPtakespData2(the deleted object) as the source. Upstream marks this itself: "GWC:TODO try typo bug fix". Merges also OR the target flags into both targets but copy none from the source. - Trigger: Merge notify of two topology objects with different flags.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-17. StopTracking clears a foreign callback
- Location:
SmTrackTopologyChanges.cpp:512,SmTrackTopologyChanges::StopTracking. - What happens: If the slot was empty at start,
StopTrackingclears whatever callback is installed now, including one installed by someone else since. - Trigger: Start tracker A, install callback B, stop A: B is removed.
- How found: adversarial / edge-case fixture
track_stop_clears_foreign. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-18. Attribute copy, add and propagation inconsistencies
- Location:
SmAObject.cpp:540SmAObject::AddAttribute: skips only ifFindAttribute(id)returns that same attribute, so a second attribute with the same id is added, and re-adding duplicates the entry and the user.SmAttribute.cpp:1101SmColorAttribute::PropagateRegionsThroughBooleans: copies itself, not the colour it found.SmLongAttribute,SmVector3dAttributeandSmColorAttribute::MakeCopydrop the copy behaviour.SmAttribute.cpp:17GOTO_NEXT_ATTLINE: treats byte0xFFas EOF.
- What happens: Duplicated entries, wrong propagated colour, lost
SM_AB_STANDALONE_COPY, early end of input. - Trigger: Add two attributes with the same id; union two bodies where only one has a colour.
- How found: generated difftest case (attribute scripts).
- Confirmed: yes (all compared against C++).
- Severity: wrong result.
- Rust port: Ported as is.
WR-19. SmLocalSolve1d without an interval steps against Newton
- Location:
source/SMLib/inc/SmLocalSolve1d.h:290(constructor without interval);SmLocalSolve1d.cpp:751SmLocalSolve1d::SolveIt. - What happens: The interval stays
SmExtent1d()([1e20, -1e20]), so the step limitGetLength() * 0.08 + SM_EFF_ZEROis negative and every step becomes±1.584e19against the Newton direction. With no interval and a failed first evaluation,bFoundAnsweris read uninitialised. - Trigger:
SmLocalSolve1d(evaluator)thenSolveIt. - How found: adversarial / edge-case fixtures
no_interval_*. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is (
bFoundAnswerstartsfalse).
WR-20. SmLocalSolveNd::SolveIt reports a failed first evaluation as found
- Location:
SmLocalSolveNd.cpp:90,SmLocalSolveNd::SolveIt. - What happens: If the first
Evaluatefails, it returnsSM_SUCCESSwithrbFoundSolution = TRUE(the guess) andSM_TR_UNABLE_TO_CONVERGE.bFoundAnsweris read even when the evaluator never writes it, and the constructor does not setm_lCurrentIteration. - Trigger: An evaluator that fails on the first call.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is (state initialised).
WR-21. SmGlobalSolver::IdenticalSolutions prefers the smaller value when maximising
- Location:
SmGlobalSolver.cpp:1865,SmGlobalSolver::IdenticalSolutions. - What happens: An
SM_SO_MAXIMIZEsolve can replace an interior maximum by a nearby span-boundary point with a smaller value. - Trigger: Maximum distance on a closed Hermite curve:
0.42572...replaced by0.42578125. - How found: generated difftest case.
- Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-54. Old composite curve records get their segment gaps shifted by one
- Location:
source/SMLib/src/SmBrepData.cpp:5438(sGapsholds the first segment's start gap, then every end gap) passed toSmCompositeCurve.cpp:1050(which reads(*cpGaps)[i]as segmenti's end gap). - What happens: For a version 32 or older composite curve, segment
igets the stored end gap of segmenti - 1as its end gap (segment 0 gets its own start gap); the last end gap is never used. The branch is marked "untested code" upstream. - Trigger: A closed two-segment composite record with gaps
0.5 0.25and0 3: segment 0 gets gaps0.25 / 0.5instead of0.5 / 0.25. - How found: read during porting (
topology.brep_data). - Confirmed: no (gaps are not in the difftest dump).
- Severity: wrong result.
- Rust port: Faithful to the code.
WR-62. SmTLSPointVertex::SolveIt ray firing falls through into the 3D signed case
- Location:
SmTopologySolver.cpp:206-262,SmTLSPointVertex::SolveIt(def.:116). - What happens: The
SM_SO_RAYFIREcase has nobreak, so theSM_SO_3D_SIGNED_DIRECTED_MINIMIZEcase runs after it: it recomputes the ray parameter without the rayfire snap to the ray start (dParam < 0.0 -> 0.0), overwritesdDistSq, and sets the sign from the unsnapped parameter (negated again when the object order is swapped). A vertex just behind the ray start (withinSM_EFF_ZEROof it) gets a small negative ray distance instead of0, and a swapped call returns negated ray distances. - Trigger:
SM_SO_RAYFIREpoint/vertex solve from(1, 0, 0)along(1, 0, 0)with a vertex at(1 - 1e-13, 0, 0)(fixturepoint_vertex_rayfire_behind_start). - How found: read during porting (
intersect.topology_solver_p1). - Confirmed: no (the C++ runs the fixture, but the Rust side defers it: the solution comparison needs vertex zone tolerances). The review's single-call fixtures
adv_rayfire_behind_start_single_*are deferred the same way (add_sorted_solutionreaches theSmTol::GetZoneTol3dstub oftopology.brep_p1for vertex objects). - Severity: wrong result.
- Rust port: Ported as is (the fall-through kept).
NLib
WR-22. ST_surxsdr returns a stale error number past its recursion limit
- Location:
NL_SrfAdv.cpp:696,ST_surxsdr(depth > 10). - What happens: It returns
1withoutN_ErrSet, soNL_ERRORholds whatever was recorded last. That can be an ignored iso-curve extension error,NL_GEO_ERR, or0after a callee'sN_ErrClear.ST_BezCalcArcLength(NL_Bezier.cpp:4713) andST_curxcdr/ST_curxcrralso return bare flags. - Trigger:
DropCurveSurface.smsextended 900 units in v:eno = 17at one end,0at the other. - How found: adversarial / edge-case fixture.
- Confirmed: yes.
- Severity: wrong result (error reporting).
- Rust port: Returns
NotConverged.
WR-23. sm_CrvTrim returns an error flag that callers read as success
- Location:
SmNurbsCrv.cpp:729,sm_CrvTrim. - What happens: It returns the flag value
NL_INP_ERR(14), notNL_YES, forur <= ul, withoutN_ErrSet. Callers comparing withNL_YEStreat it as success. - Trigger:
sm_CrvTrimwithur == ul. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Returns
InvalidInput.
WR-24. N_KnotVectorIsValid accepts degenerate vectors
- Location:
NL_BasisBasic.cpp:172,N_KnotVectorIsValid. - What happens: Accepts
p+1equal knots (e.g.[2,2,2,2]withp = 3), and a single knot withp = 0. Later routines index such vectors as if they had a span. - Trigger:
[2,2,2,2],p = 3. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result (validation gap).
- Rust port: Accepts them too (faithful).
Curves
WR-25. SmBSplineCurve and SmCurve declare different default bNonZeroTangents
- Location:
source/SMLib/inc/SmBSplineCurve.h:481(bNonZeroTangents=FALSE) vssource/SMLib/inc/SmCurve.h:396(TRUE); used inSmBSplineCurve.cpp:2997SmBSplineCurve::Evaluate. - What happens: Default arguments bind by static type. The recursion inside
SmBSplineCurve::Evaluatefor a zero first derivative with one derivative asked runs withFALSEand returns the zero derivative, so the non-zero-tangent fix only works when 2+ derivatives are asked. - Trigger: A B-spline with coincident first two control points:
Evaluate(t0, 1, ...)through anSmCurve&. - How found: adversarial / edge-case fixture
zero_tangent_start_nd1. - Confirmed: yes.
- Severity: wrong result.
- Rust port: Passes
falsethere (matches).
WR-26. SmHermiteCurve::Evaluate leaves the last slot unset
- Location:
SmHermiteCurve.cpp:299(for (ULONG i=4; i<lNumDerivatives; i++)),SmHermiteCurve::Evaluate(def.:283). - What happens: The zeroing loop stops one short, so
aPointAndDerivatives[n]keeps the caller's value (SM_UNDEF_DOUBLEfor a fresh array). - Trigger:
EvaluatewithlNumDerivatives = 5. - How found: read during porting.
- Confirmed: no. The shim pre-fills zeros.
- Severity: wrong result.
- Rust port: Returns zero there.
WR-27. SmConic::Evaluate zero-tangent fix reads a slot before writing it
- Location:
SmConic.cpp:125,SmConic::Evaluate. - What happens: With
bNonZeroTangentsand two derivatives, the fix readsaPointAndDerivatives[2]before the second derivative is written there, i.e. the caller's leftover value. - Trigger: A conic with a tiny first derivative, evaluated into a reused array.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Reads
Vec3::UNDEF(matches fresh arrays).
WR-28. SmProjectedCurve::MakeOrthoNormal is inverted
- Location:
SmProjectedCurve.cpp:219-221(if(bProjInit && bProjZero) { m_sProjVec.Unitize() ; } else { return( SM_ERR ) ; }), and the same form_sAuxData;SmProjectedCurve::MakeOrthoNormal(def.:215). - What happens: It unitizes only a zero vector (giving
(0,0,1)) and returnsSM_ERRfor every non-zero vector, leaving it un-normalised. A non-unit view normal gives a skewed projection. - Trigger: A projected curve with view vector
(0, 0, 2). - How found: generated difftest case (both sides agree on the skew).
- Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-29. SmProjectedCurve::Transform only works for m_bOwnsCurve == UNSURE
- Location:
SmProjectedCurve.cpp:434(if (m_bOwnsCurve < 2)),SmProjectedCurve::Transform(def.:417). - What happens: Curves that own (
TRUE, e.g. everything fromReadFromDB) or borrow (FALSE) their base fail withSM_ERR, after a pre-edit notify. - Trigger:
Transformon a projected curve read from a file. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-30. SmIsoCurve::CreateUVIsoLine tests SM_SP_U in both branches
- Location:
SmIsoCurve.cpp:34,:38,SmIsoCurve::CreateUVIsoLine(def.:24). - What happens: A constant-v iso curve gets
SM_ERR_INVALID_INPUT. The constant-u branch builds the UV line over the domain's u interval although the line runs along v. - Trigger:
CreateUVIsoLineforSM_SP_V. - How found: generated difftest case (
*_uvline). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-31. SmEllipse::ConvertTo2D uses dThetaXRad for both axes
- Location:
SmEllipse.cpp:1740,SmEllipse::ConvertTo2D(def.:1674). - What happens:
sNewYcopies thesNewXformula withdThetaXRad. The projected ellipse gets equal axes, andSetCanonicalof two parallel vectors runs with its status ignored.dThetaYRadis computed and unused. - Trigger: An ellipse whose projected axes are not perpendicular.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-32. SmCircle::CreateSimpleOffset never shrinks for the anti-parallel normal
- Location:
SmCircle.cpp:339-340,SmCircle::CreateSimpleOffset(def.:321). - What happens:
IsParallelTo(+Z)also accepts anti-parallel vectors, so the-Zbranch (radius minus distance) is dead. - Trigger: Offset with normal
-Z: the radius grows. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-33. SmCrvInVolume::GenerateMults indexes continuities by knot index
- Location:
SmCrvInVolume.cpp:968,SmCrvInVolume::GenerateMults(def.:911). - What happens:
m_sConts[ii]uses the knot indexii, not the matched break index, soGetKnots' cached path reports the wrong breaks' multiplicities. The cached and computed paths also disagree (4 vs 3 at the ends). - Trigger:
GetKnotswith an interval on a cached curve. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-34. SmCompositeCurve::CalculateBoundingBox shifts intervals from 0
- Location:
SmCompositeCurve.cpp:392,SmCompositeCurve::CalculateBoundingBox. - What happens: Segment intervals are shifted from
0, not from the first segment's start (asGetNaturalIntervaldoes), and the shifted interval is passed to each segment. - Trigger: A composite whose first segment starts at
t = 5. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-35. SmCurvePropertyGS::BranchMayContainAnswers corner and angle bugs
- Location:
SmCurve.cpp:12219(double dZV = 1.0 * j;),SmCurvePropertyGS::BranchMayContainAnswers(def.:12141). - What happens: Branches are pruned or kept incorrectly:
- The "corner" points use
jfor z, so only 4 distinct box points are tried, each twice. - The
SM_CP_PERPENDICULAR_TO_VECTOR/SM_CP_MAXIMA_TO_PLANEtest adds degrees to radians before comparing withPI/2. SmCurvePropertyGSalso declares its ownm_cpOptVectors, which shadowsSmGlobalSolver's (source/SMLib/inc/SmGlobalSolver.h:98), so the property vectors never reach the base checks.
- The "corner" points use
- Trigger: A perpendicular-to-vector property solve.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-36. SmCurve::CurveMaxDistanceBetween with one sample divides 0/0
- Location:
SmCurve.cpp:2507SmCurve::CurveMaxDistanceBetween;SmCrvOnSurf.cpp:1663SmCrvOnSurf::SurfaceCurveMaxDistanceBetween. - What happens:
ii / (lNumSamples - 1)is0.0 / 0.0for one sample, so both curves are evaluated at NaN.SurfaceCurveMaxDistanceBetweenalso setsbDoPrecise = FALSEunconditionally, so its precise refinement is dead code. - Trigger:
lNumSamples = 1. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Computes the same NaN. The harness rejects one sample.
WR-37. SmBSplineCurve::CreateSimpleOffset passes the match flag in the wrong slot
- Location:
SmBSplineCurve.cpp:2475,SmBSplineCurve::CreateSimpleOffset. - What happens: It calls
ApproximateCurve(..., bOptMatchParameterization, FALSE). The caller's match flag lands inbOptCreateAnalytics, so parameterisation matching is always off. - Trigger:
CreateSimpleOffset(..., bOptMatchParameterization = TRUE). - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
Surfaces and volumes
WR-38. SmBSplineSurface data constructor zeroes the v knots when s > r
- Location:
SmBSplineSurface.cpp:87(smos_MemCpy(pNewSur->knv->U, V, (s+1) doubles, (r+1) doubles)), constructor at:63. - What happens: The v knots are copied with the u knot count as the destination size, so the copy is refused (status only
SE'd) whens > r. The v knots stay 0 from calloc, andsm_FixupKnotVectorthen runs on zeros. - Trigger:
SmBSplineSurface(n, m, p, q, r, s, Pw, U, V)with more v knots than u knots. - How found: generated difftest case (every such
datacase). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Reproduces it.
WR-39. SmBSplineSurface::IsParallelToVector returns a status as a boolean and uses the wrong overload
- Location:
SmBSplineSurface.cpp:3146,SmBSplineSurface::IsParallelToVector. - What happens:
- It returns
SER(sTestVec.Unitize())'sSM_ERR_INVALID_INPUTas anSmBoolean, so a zero vector reads as TRUE. smgu_LinePointDistance(LinePnt, LineEnd, Pnt, d)binds the (point, direction) overload, so the second net point is used as the direction.
- It returns
- Trigger: A zero test vector.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-40. SmBSplineSurface::TrimWithDomain v snapping uses idxMin for both boundaries
- Location:
SmBSplineSurfaceNLib.cpp:7455,SmBSplineSurface::TrimWithDomain. - What happens: The v loop's upper-boundary branch writes
idxMin, soidxMaxstays-1andsm_CheckTrimBoundariesnever widensvr. The u loop's "slightly above the knot" branch does not setidxMax. Only the u boundaries are checked for being too close. - Trigger: A v trim boundary just above an interior knot.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as is.
WR-41. SmPlane::Reparameterize does not move the origin in release builds
- Location:
SmPlane.cpp:3065SmPlane::Reparameterize. The new originsOriginMax(:3086) is used only by theSM_DEBUG_CODEcheck (:3090-3099).m_vPositionis never updated; only the domain and scales change (:3115-3116). - What happens: The analytic plane no longer matches its NURB unless the new domain starts where the old one did.
- Trigger: Reparameterize a plane from
[0,1]^2to[2,3]^2. - How found: adversarial / edge-case fixture
edge_reparameterize_keeps_origin. - Confirmed: yes.
- Severity: wrong result (release-only).
- Rust port: Ported as is.
WR-42. SmSurfaceCache::GetSurfaceContinuity skips the last internal v knot
- Location:
SmSurfaceCache.cpp:1711(iv+1<rVConts.GetSize()-1),SmSurfaceCache::GetSurfaceContinuity(def.:1672). - What happens: The v loop stops one earlier than the u loop, so the continuity at the last internal v knot is never checked.
- Trigger: A surface whose only C0 v knot is the last internal one.
- How found: generated difftest case.
- Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-43. SmCurveBoundedSurface::ReadFromDB reads UV boundaries with the 3D type
- Location:
SmCurveBoundedSurface.cpp:709,SmCurveBoundedSurface::ReadFromDB. - What happens: The UV boundaries are read with the type of the last 3D boundary (
l3DType,0when there is none, so every UV boundary then fails withSM_ERR). The implicit-outer flag that was read is dropped. - Trigger: A record with UV boundaries but no 3D boundaries.
- How found: generated difftest case (
WriteToDB-layout ASCII records). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Ported as is.
WR-44. Small SmSurface helper bugs
- Location:
SmSurface.cpp:11574sm_MergeDoubleArrays: stops when either array is exhausted, dropping the other's tail.:3303SmDerivSurfDefinition::CreateCrossSectionCurves: takes the third-derivative Taylor coefficient from the second derivatives.SmCrvOnSurf.cpp:811EvaluateSurface1stDerivDerivs: the zero fillfor (ii = 2; ii > lNumDers; ii++)never runs.
- What happens: Silently wrong merged arrays, cross sections and derivative slots.
- Trigger:
sm_MergeDoubleArrays([0,1,2], [0.5]). - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as written.
WR-45. smvol_LiftThirdDerivative uses the wrong terms
- Location:
SmVolume.cpp:9851(surface form),:10126(volume form),smvol_LiftThirdDerivative. - What happens:
crDxxzis used whereDxzzbelongs inrDuuv,rDuvv,rDuuw,rDuww,rDvvw,rDvww.rDuwwis built fromFuv/Fvinstead ofFuw/Fw, andrDvwwfromFvv/Fvinstead ofFvw/Fw. - Trigger: Third mixed derivatives of any compound volume (and of
SmSrfInVolume). - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Keeps the formulas so results match.
WR-46. SmTwistVolume sign, equality and constructor bugs
- Location:
SmTwistVolume.cpp:1045EvaluateSimple:Duwis(-per sin a, -per cos a, 0), but the header andEvaluateuse(-per sin a, per cos a, 0).:210-211operator==: the minimum comparison overwrites the period comparison.:38/:132constructors: compareUnitize'sSM_ERR_INVALID_INPUTwithSM_ERR, so a zero axis skips the intended repair. A too-small period becomes|dPeriodMinDist|.
- What happens: Wrong mixed derivatives; twists that differ only in period compare equal.
- Trigger: Two twist volumes differing only in period:
==is TRUE. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Ported as written.
WR-47. SmDatabaseIO byte-swapping asymmetries
- Location:
SmDatabaseIO.cpp:632ReadBoolean(never byte-swaps),:547ReadBooleans(does).ByteSwap32(unsigned short)always returns 0. - What happens: Booleans read singly from a foreign-endian file come out wrong.
- Trigger: A big-endian binary file read on x86-64.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Kept as is.
WR-48. SmBSplineVolume::HasDiscontinuitiesSimple never reports geometric discontinuities
- Location:
source/SMLib/src/SmBSplineVolumeNLib.cpp:242(HasDiscontinuitiesSimple, def.:226) ->:606(CalculateContinuitiesSimple) ->source/SMLib/src/SmBSplineVolume.cpp:2364(EvaluateIsoParametricCurveSimple,SM_ERRin its#elsebranch). - What happens: With the default
bCalcGeometric = TRUE,CalculateContinuitiesSimplefails at its first iso-curve (noUSE_VOLUMES), leaving the knots inrParams,rContsempty and the minimumSM_CT_CINFINITY.HasDiscontinuitiesSimpleignores the status and returnsFALSEwith empty continuity lists, even for a volume withC0knot planes. - Trigger:
HasDiscontinuitiesSimple(&d)on anySmBSplineVolume, e.g. one with a cubicUknot vector holding a triple interior knot. - How found: adversarial / edge-case fixture.
- Confirmed: yes (fixtures
hasdisc_*_geom,conts_*_e3_geomofsurface.b_spline_volume_n_lib). - Severity: wrong result.
- Rust port: Ported as written.
WR-49. Representational volume continuities leave the last knot SM_CT_UNDEFINED
- Location:
source/SMLib/src/SmBSplineVolumeNLib.cpp:505-:518(SmBSplineVolume::CalculateContinuitiesSimple,bCalcGeometric == FALSE). - What happens:
rContsis sized to the knot count but the loop fills only the firstcount - 1entries, so the last knot's continuity is the zero fillSM_CT_UNDEFINED, while the first (an end knot) gets a degree-minus-multiplicity value (SM_CT_C0when clamped) instead of the documentedSM_CT_DISCONTINUOUS. The minimum therefore includes the start knot but not the end knot, andHasDiscontinuitiesSimple(.., FALSE)isTRUEfor every clamped volume. - Trigger:
CalculateContinuitiesSimple(SM_VP_U, .., FALSE)on a trilinear unit cube volume: conts[C0, UNDEFINED]. - How found: adversarial / edge-case fixture.
- Confirmed: yes (fixtures
conts_*_repofsurface.b_spline_volume_n_lib). - Severity: wrong result.
- Rust port: Ported as written.
WR-59. SmBendVolume::EvaluateBoundingBoxSimple pseudo box does not contain the image
- Location:
source/SMLib/src/SmBendVolume.cpp:1555(pseudo boxdXMin),:1480(axis boxdYMax, unbounded-ubranch). - What happens: The pseudo box along the mid-angle direction takes its
xminimum assin(thetaEnd) * uMin(thecosvariable on the line above is commented out). Once the half angle exceedspi/2the image reaches negativex(down tocos(thetaEnd) * uMax), but the box starts at a positive value, so the "bounding" pseudo box misses part of the mapped box. Separately, withureachingSM_INFINITE_PARAMETERand both end angles at most0, the axis box'symaximum uses the cosines of the end angles instead of the sines (too large, still containing). - Trigger: Bend with neutral distance
1, parameter boxuin[0.5, 1.5],vin[-2, 2]: pseudoxinterval starts atsin(2) * 0.5 = 0.45, while the corner atu = 1.5, v = 2lies atx = cos(2) * 1.5 = -0.62. - How found: read during porting.
- Confirmed: yes (fixtures
b_box_unit_both_poles,b_box_unit_inf_u_downofsurface.bend_volumecompare the values). - Severity: wrong result.
- Rust port: Ported as written.
WR-60. SmBendVolume::TrimParamBoundingBoxSimple keeps stale output for boxes crossing the period end
- Location:
source/SMLib/src/SmBendVolume.cpp:2180-:2184. - What happens: Each
vend is clamped to the primary period separately withSM_CLAMP_TO_PERIOD, so a box crossingv = pi dgetsvMin > vMax. The finalSetMinMaxthen fails, its status is ignored, and the function returnsSM_SUCCESSwith the caller's output box unchanged (whatever it held before). - Trigger: Bend with neutral distance
1, parameter boxvin[2, 4.5]. - How found: adversarial / edge-case fixture.
- Confirmed: yes (fixture
trim_unit_across_periodofsurface.bend_volume). - Severity: wrong result.
- Rust port: Ported as written.
WR-61. SmSurfOfExtrusion::EvaluateSTEP / EvaluateSTEPPoint clamp a copy they never use
- Location:
SmSurfOfExtrusion.cpp:941-955(EvaluateSTEP: clampssUV, evaluatescrUV.x,crUV.y);:1015-1024(EvaluateSTEPPoint, same). - What happens: "Per SMLib practice clamp input point to surface domain" clamps a local copy, but both evaluate the caller's unclamped parameters. With out-of-bounds evaluation disabled, a v outside the STEP domain still extrapolates along the extrusion, and an analytic generator is evaluated outside its domain.
- Trigger:
EvaluateSTEPPoint((u, vmax + 1))with out-of-bounds evaluation off. - How found: read during porting; generated difftest cases evaluate outside the domain on both sides.
- Confirmed: yes (
surface.surf_of_extrusion_p1generatedeval_stepcases outside the domain agree with the unclamped port). - Severity: wrong result.
- Rust port: Faithful (evaluates the unclamped parameters).
WR-63. SmCone::CreateCanonical overloads misuse their arguments
- Location:
SmCone.cpp:341-348SmCone::CreateCanonical(Origin, BotRadius, SemiAngleDeg, pOptHeight, bSwapUV, bInsideOut)(def.:308);SmCone.cpp:392-400SmCone::CreateCanonical(Origin, BotRadius, TopRadius, Height, ...)(def.:375). - What happens: With a height, the first overload passes
FALSEas the bounded constructor'sbInsideOutand the caller'sbInsideOutasbMakeNurbGenCurve, so an inside-out request is dropped. ForHeight <= 0the second overload builds the infinite cone withTopRadiusas the semi-angle in degrees. - Trigger:
CreateCanonical(O, 1.0, 30.0, &h, cone, FALSE, TRUE)givesGetInsideOut() == FALSE;CreateCanonical(O, 1.0, 30.0, 0.0, cone)gives a 30 degree cone. - How found: read during porting; compared by fixtures
canonical_semi_with_height_inside_is_mngc,canonical_height_zero_top_is_semi(surface.cone_p1). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Reproduces both.
WR-64. sm_TotalCoincidenceChecker misses reversed coincident lines and returns FALSE as a status
- Location:
SmCone.cpp:2501and:2508,sm_TotalCoincidenceChecker(file static, def.:2454). - What happens: For lines, the reversed test computes
dDist_E_SfromsPt01andsPt11(the end of this line and the start of the other, instead ofsPt10), so reversed coincident lines are never found. The function then returnsFALSE(0), which is notSM_SUCCESS; callers'SERturn it into an error return. - Trigger: Lines
(0,0,0)-(1,2,3)and(1,2,3)-(0,0,0). - How found: read during porting; compared by fixture
coinc_reversed_line(surface.cone_p1, a verbatim copy of the static in the shim). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Reproduces it (
SmStatus0 error).
WR-63. SmSurfPtSolveStepper::DropToIsoCurve passes the same variable as guess and output of DropPoint
- Location:
SmSurfacePointSolve.cpp:4733(pIsoCurve->DropPoint(.., &dGuessParam, bGotThisDrop, dGuessParam, dDist)),SmCurve::DropPointatSmCurve.cpp:3290. - What happens:
DropPointzeroes its output parameter before it reads the guess, so the guess (the crease parameter clamped to the section) is always0. - Trigger: Any call (the method is not used upstream).
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result (a poorer guess for the local solve).
- Rust port: Passes
Some(0.0), as upstream's code ends up doing.
WR-64. The directed-maximise SmSurfacePointSolver ignores a failed CalculateBoundingBox
- Location:
SmSurfacePointSolve.cpp:463(m_pSrf->CalculateBoundingBox(m_sDomain, &sBBox, NULL, NULL)), constructor at:406. - What happens: The status is not checked. When the box cannot be built (for example a curve-bounded surface without boundary curves, whose surface cache fails on the base
SmSurface::GetKnots), the box stays in its inside-outInit()state and the test point becomes a corner of it, near±SM_BIG_DOUBLE. - Trigger:
SM_SO_3D_SIGNED_DIRECTED_MAXIMIZEon a surface whose cache cannot be built. - How found: generated difftest case (
surface.surface_point_solve_p1, curve-bounded surfaces). - Confirmed: yes.
- Severity: wrong result.
- Rust port: Reproduces it (the box stays
Extent3d::new());SolidError::Unportedstill propagates.
Intersection
WR-65. SmVolume "simple" box samplers and inverse solvers mix simple and compound maps
- Location:
SmVolume.cpp:5865EvaluateBoundingBoxSimpleand:6057EvaluatePseudoBoxSimple(sampleEvaluatePointandEvaluate1stDerivativeswith compounding);:919IsPointInParamDomain(passes the last out-space image to the next map's in-space check);:6857LocalPointSolveSimple(solution value is the distance to the compound image);:6665InvEvaluatePointSimple(a failed local solve forcesrbSuccess = FALSEafter a successful global solve);:3411InvEvaluatePoint,:3907InvMapPoint(always report success);:4512GlobalPointSolve(found flag of the last point only). - What happens: The projection-space boxes of a compound volume are boxes of its last out-space; domain checks and solution values use the wrong space; success flags are lost or always set.
- Trigger:
EvaluatePseudoBoxSimpleof a volume with a next map;IsPointInParamDomainof a chain with a plainSmVolumesecond map. - How found: read during porting.
- Confirmed: yes (surface.volume_p2 difftest: the Rust port reproduces these and matches the C++ on generated cases and the
twist_then_base_*fixtures).
WR-50. AddStartPoint "replaces" a bounded start point by assigning a local pointer
- Location:
source/SMLib/src/SmSurfaceIntersector.cpp:3901SmSurfaceIntersector::AddStartPoint. - What happens: For a bounded/exact start point pair whose stored point is the bounded one, the "replace pTSP with rStartTSP" branch only sets the loop variable
pTSP = &rStartTSPand reports a match. The exact candidate is dropped and the bounded point stays inm_vStartPoints, the opposite of what the comment (and the function header) describe. - Trigger: A bounded start point already in the list, then an exact one within
2 * m_dThisApproxTol3dwith a matching curve tangent. - How found: read during porting.
- Confirmed: no (the branch needs
smsurf_EvaluateNormalSection, which the Rust side does not have yet, so those difftest cases are deferred). - Severity: wrong result.
- Rust port: Faithful (the candidate is not added).
WR-51. IsPointOnStartPoint compares squared distances with a length tolerance
- Location:
source/SMLib/src/SmSurfaceIntersector.cpp:7515SmSurfaceIntersector::IsPointOnStartPoint. - What happens:
DistanceBetweenSquared(...) < m_dThisApproxTol3d: the effective tolerance issqrt(tol), far looser than intended for small tolerances (and tighter above 1). - Trigger: Any call; e.g.
tol = 1e-6accepts points 1e-3 apart. - How found: read during porting.
- Confirmed: yes (
intersect.surface_intersector_p1difftest,edge_start_pointsand generatedon_startops agree with the squared test). - Severity: wrong result.
- Rust port: Faithful.
WR-52. SER inside SmBoolean functions reports a failure as TRUE
- Location:
source/SMLib/src/SmSurfaceIntersector.cpp:4195sm_IsPointOnCurveand:7376,:7391SmSurfaceIntersector::IsPointOnCurve. - What happens:
SER(status)returns the non-zeroSmStatusfrom a function returningSmBoolean, so a failing point solve, curve evaluation or curve drop makes the point count as "on the curve". - Trigger: A curve whose
EvaluatePointorDropPointfails, or a failing Hermite point solve. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Faithful (
Ok(true)on such an error).
WR-53. TestSpanAccuracy compares a tangent length with an angle tolerance
- Location:
source/SMLib/src/SmSurfaceIntersector.cpp:3448SmSurfaceIntersector::TestSpanAccuracy(upstream's ownGWC_NEEDS_WORKnote). - What happens:
rNextTSP.CrvDeriv().Length() < m_dSteppingParallelTolRadians(SM_EFF_ZERO): a length is tested against a radian tolerance, so the check only catches (near) zero tangents. - Trigger: A next point with a tangent shorter than
1e-12. - How found: read during porting.
- Confirmed: yes (
intersect.surface_intersector_p1difftest,edge_span_angle). - Severity: wrong result (unit mix-up).
- Rust port: Faithful.
WR-55. SmSurface::GetFlatCorners destroys its return value while filling the corner points
- Location:
SmSurface.cpp:12691,SmSurface::GetFlatCorners(def.:12655). - What happens: The optional-output block tests
if(lRtn &= SM_FC_UMIN_VMIN)(and so on) with&=, so asking for the points masks the flags down to at most one bit (usually none), and later corners are evaluated only if all earlier masks kept a bit. - Trigger:
GetFlatCorners(&points)on a surface with two or more flat corners. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result.
- Rust port: Reproduces it (
Surface::flat_corners;IsFlatCorneris not ported yet).
WR-56. sm_GoodDropResult swaps the guess and result distances
- Location:
SmSurface.cpp:10349,sm_GoodDropResult(file static, def.:10256). - What happens:
dDsqGuessholds the distance to the result point anddDsqResultthe distance to the guess point, so the final "result farther than the guess" test rejects drops that improved on the guess. The caller then runs the expensive global solve. - Trigger: A local drop whose result is closer to the point than the guess, with a small residual that passes the earlier tests.
- How found: read during porting.
- Confirmed: no.
- Severity: wrong result (performance: an unneeded global solve).
- Rust port: Reproduces it (
good_drop_result).
WR-57. SmSurface::FindDegenParamForDirection compares a sample fraction with a parameter
- Location:
SmSurface.cpp:6948,SmSurface::FindDegenParamForDirection(def.:6757). - What happens: The "nearly as good and closer to the input" tie-break compares
dThisParam(a fraction in[0, 1]of the sample domain) withdS(the actual parameter), so it only means "closer" when the singular parameter runs over[0, 1]. - Trigger: A degenerate boundary whose singular parameter interval is not
[0, 1], with two nearly equally good samples. - How found: read during porting.
- Confirmed: no.
- Severity: wrong result (start guess of the Newton solve).
- Rust port: Reproduces it.
WR-58. SmPointClassification::GetGap3d returns SM_ERR as a distance
- Location:
SmPointClass.cpp:464,SmPointClassification::GetGap3d(def.:418). - What happens: When the object point has no single drop point on the given surface (even at 100x the zone tolerance), the function, which returns a
doublegap, returnsSM_ERR(1001.0), which callers read as a large gap. - Trigger:
GetGap3d(pSurface)for an object point far frompSurface. - How found: read during porting.
- Confirmed: no (the surface point solve is not ported yet, so these cases defer).
- Severity: wrong result.
- Rust port: Returns
SM_ERRas the gap value too.
WR-60. MakeDupin's bottom hyperbola uses k2 for both axes
- Location:
source/SMLib/src/SmAdvSurfaceIntersector.cpp:1120-1122SmAdvSurfaceIntersector::MakeDupin. - What happens: The four hyperbolas of a saddle point should be one indicatrix branch pair
(1/sqrt|k1|, 1/sqrt|k2|)and its conjugate pair(1/sqrt|k2|, 1/sqrt|k1|). The top hyperbola uses the latter, the bottom one(1/sqrt|k2|, 1/sqrt|k2|), so the bottom curve is a different conic and the singular directions found from it are wrong unless|k1| == |k2|. - Trigger:
MakeDupin(orComputeSingularityPoint) at a hyperbolic point with|k1| != |k2|. - How found: read during porting.
- Confirmed: yes (
intersect.adv_surface_intersectordifftest: generateddupinops andedge_dupin_kindscompare the conics). - Severity: wrong result.
- Rust port: Faithful.
WR-61. SmAdvSurfaceIntersector::SolveBranch never tests Gauss map separation
- Location:
source/SMLib/src/SmAdvSurfaceIntersector.cpp:1383and:1401SmAdvSurfaceIntersector::SolveBranch. - What happens: The method sets
m_bSeperateGaussMaps = TRUEon entry and then callsBranchIsGaussSeparateonlyif (!m_bSeperateGaussMaps), so the polar box test and the whole "separable" branch (one start point per separable sub-tree pair, the documented purpose of the class) are dead code: every intersecting leaf pair gives its own candidate. - Trigger: Any
SolveIt(SM_SO_INTERSECT, ..)on anSmAdvSurfaceIntersector. - How found: read during porting.
- Confirmed: yes (
intersect.adv_surface_intersectordifftest:solveandsolve_branchops agree on one solution per intersecting leaf pair). - Severity: wrong result (performance and duplicate start points).
- Rust port: Faithful.
Docs
DOC-1. NL_LEFT / NL_RIGHT documentation is swapped
- Location:
NL_BasisBasic.cpp:1433-1434(parameter comment ofN_BasisFindSpan),:1506,:1522(case comments). - What happens: The comments say
NL_LEFTmeansU[spn] < u <= U[spn+1]. The code (while( u LT U[mid] OR u GE U[mid + 1] )) does the opposite:NL_LEFTgivesU[spn] <= u < U[spn+1].NL_RIGHTis likewise swapped. - Trigger:
uequal to an interior knot. - How found: read during porting.
- Confirmed: no.
- Severity: docs.
- Rust port:
Sidedocuments what the code does.
DOC-2. N_GetCompatibleKnotVector doc says multiplicities are not carried over
- Location:
NL_BasisBasic.cpp:2339(N_GetCompatibleKnotVectordoc comment); behaviour in:2483N_GetCompatibleKnotVectorToTol. - What happens: Each distinct knot gets the highest multiplicity it has in any vector, unlike the doc says.
- Trigger: Vectors
[0,0,.5,1,1]and[0,0,.5,.5,1,1]. - How found: read during porting.
- Confirmed: no.
- Severity: docs.
- Rust port: Faithful to the code.
DOC-3. "Small topology" tolerances are 1000x larger, not tighter
- Location:
SmTol.cpp:661(also:806,:892),SmTol::GetZoneTol3d. - What happens:
GetZoneTol3d(SM_ST_SMALL_SIZE)returnss_LargeSmallSizeRatio * s_ZoneTol3d(default 1000x), despite comments that promise tighter tolerances. Theconst SmContext*overloads ignore the small flag. - Trigger:
GetZoneTol3d(SM_ST_SMALL_SIZE). - How found: read during porting.
- Confirmed: no.
- Severity: docs.
- Rust port: Faithful to the code.