fix(#705): reject a repeated edge pair in chamfer2D instead of crashing - #706
Conversation
Shape.chamfer2D(edgePairs:distances:) SIGSEGVs, uncatchably, when the same edge pair is named twice. Cluster B's census (#665, #704) recorded the crash rather than running it live, since an in-process OS signal would kill the census itself. Root cause is an upstream OCCT defect in BRepFilletAPI_MakeFillet2d::AddChamfer, not this bridge: the two-edge overload looks up the pair's shared vertex via ChFi2d::FindConnectedEdges and dereferences the two edges it returns without checking the returned status first. That lookup leaves both edges null on every failure path, and a pair's second call fails it, because the shared vertex was already consumed chamfering the pair the first time. The sibling overload, AddChamfer(edge, vertex, distance, angle), checks the identical status correctly. A kernel patch is being prepared in a follow-up PR; this bridge guard is what protects callers until it ships. OCCTFace2DChamfer (OCCTBridge_Modeling.mm) now rejects the whole call, in either order, when a pair repeats, matching fillet2D's own contract for a duplicated vertex on this same builder (#568). Reusing one edge across two different pairs, e.g. chamfering adjacent corners of a polygon, is unaffected and stays non-nil. Verified with a standalone probe process (an in-process crash would kill the test runner): before, exit 139 on a duplicate pair, in either order, and on a triple repeat; unaffected on adjacent pairs sharing one edge and on chamfering every corner of a rectangle. After, exit 0 and nil on every duplicate case, and both unaffected cases still non-nil. Recorded in docs/SEMVER.md as a behaviour change: a call that used to crash now returns nil. Cluster B's census (ClusterB.swift and its README) is updated to measure the duplicate-pair case live rather than noting it as unsafe to run. Closes #705
Independent verificationReproduced the fixed behaviour in a separate process, since the crash cannot be asserted Row C is the one that makes the guard correct rather than merely safe. Keying on the individual Verified: full suite 5,348 tests, 0 failures, censuses still 45 and 16 rows, four gate scripts On the test file I flagged. The root-cause correction is the part worth keepingThe report notes the issue's stale-handle hypothesis was a hedged guess and root-caused it properly The reachability check came back positive too: OCCT's own DRAW |
secondmouseAU-bot
left a comment
There was a problem hiding this comment.
Review
Repo: SecondMouseAU/OCCTSwift · fix/705-chamfer2d-duplicate-pair-crash → refactor/381-pass1b · +205/−21 across 8 files · CI green (build + test macOS, iOS sim smoke, gate-scripts)
Overview
Shape.chamfer2D(edgePairs:distances:) SIGSEGVs uncatchably when an edge pair is named twice (order-independent). Root cause is upstream: BRepFilletAPI_MakeFillet2d::AddChamfer dereferences the edges returned by ChFi2d::FindConnectedEdges without checking its status, and the pair's second call fails that lookup because the shared vertex was already consumed. The fix adds an O(n²) prior-pair scan in OCCTFace2DChamfer (OCCTBridge_Modeling.mm) that rejects the whole call (nullptr) the moment any two entries name the same pair in either order, before AddChamfer ever touches the second one. Docs, the Cluster B census, and two new tests are updated in the same PR.
Code correctness
- The guard is correct: it compares raw input indices (not resolved shapes), canonicalizes order via
sameOrder/swappedOrder, and runs beforeAddChamferbut aftere1/e2are resolved from the original (unmutated)edgeMap— consistent with the stated root cause. - Legitimate reuse of one edge across two different pairs (e.g.
(0,1)then(1,2)) is correctly left untouched — the check keys on the pair, not either index alone. - The nested
j < iloop is O(n²) inedgePairs.count, but that's bounded by face edge count in realistic geometry, so it's a non-issue in practice.
Minor issue — stale/forward-referencing comment
The new comment in OCCTBridge_Modeling.mm says:
A kernel patch is carried separately (
Scripts/patches/0022-*); this guard is what protects callers until it ships.
Scripts/patches/ only goes up to 0021-... as of this diff — no 0022-* file exists yet, and the PR description itself frames the kernel patch as future work ("a follow-up kernel patch PR will cite upstream"). Worth softening to "tracked in a follow-up patch, not yet filed" so a future reader doesn't go looking for a file that isn't there.
Test coverage
chamfer2DRejectsDuplicatePaircovers exact-duplicate, reversed-duplicate, and triple-duplicate.chamfer2DAcceptsSharedEdgeAcrossDifferentPairsis the important negative-space test — pins that keying on the pair (not either index) doesn't break the ordinary multi-corner case; per the other review comment it was proven to catch an overly-broad "reject on repeated single index" fix by going red first.- Crash verification is necessarily out-of-process (SIGSEGV can't be asserted in-process), which is reasonable and precedented in this repo.
Style / convention
- Matches the established "bridge mitigation now, kernel patch tracked separately" pattern (#298/#341/#344/#349), and explicitly avoids re-litigating #633 (first-wins vs last-wins) — good scoping discipline.
- Doc updates (Swift
///,Shape-Measurement.md,CHANGELOG.md,SEMVER.md) are thorough; the SEMVER anchor link resolves correctly against the new heading. - Correcting the earlier (now-known-wrong) "incremental rebuild / stale handle" hypothesis in
ClusterB.swiftand its README, rather than leaving the superseded theory in place, is good hygiene.
Security / risk
- This closes a real crash/DoS vector: any path that constructs
edgePairsfrom user input, a selection, or a loop (e.g. a NL→CAD pipeline) could previously be handed an uncatchable process kill from a duplicated pair. Converting that to a cleannil, recorded as a SemVer exception, is a meaningful robustness fix. - No new unsafe memory access or external input parsing — pure integer comparison over already-validated indices.
Verdict: Solid, well-scoped crash fix with correct root-cause analysis, adequate test coverage (including a proven-to-catch-the-wrong-fix negative test), and thorough doc/changelog hygiene. The only loose end is the Scripts/patches/0022-* comment reference, which is cosmetic.
🤖 Generated with Claude Code
…t add Review finding. The guard's comment pointed at Scripts/patches/0022-*, which this PR does not create: the kernel patch lands in its own PR, so anyone reading this commit alone goes looking for a file that is not there. The review suggested softening it to "not yet filed". That would be wrong in the other direction, since it is filed: OCCT#1431 has the reproducer and OCCT#1432 the fix. The comment now cites those, which are real and stable regardless of merge order, and says the patch is inert until the pinned xcframework is rebuilt so the reason this guard exists is on the record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed, though not quite the way you suggested. The comment did point at Your suggested wording was "tracked in a follow-up patch, not yet filed". That would be wrong in the other direction: it is filed. OCCT#1431 carries the reproducer and OCCT#1432 the fix, both opened before this review. The comment now cites those instead, which are real and stable regardless of merge order, and states that the patch is inert until the pinned xcframework is rebuilt so the reason this guard exists stays on the record. Also worth correcting one thing in the overview: the O(n²) scan is bounded by 13 tests in the suite pass, four gate scripts green, zero em-dashes. |
Summary
Shape.chamfer2D(edgePairs:distances:)SIGSEGVs, uncatchably, when the same edge pair is namedtwice. Found by Cluster B's census (#665, PR #704), which records the crash rather than running it
live, since an in-process OS signal would kill the census itself.
Root cause is an upstream OCCT defect, not this bridge's own:
BRepFilletAPI_MakeFillet2d::AddChamfer(edge1, edge2, ...)callsChFi2d::FindConnectedEdgestolook up the pair's shared vertex, then dereferences the two edges it returns without checking the
returned status first. That lookup leaves both edges null on every failure path, and a pair's
second call fails it, because the shared vertex was already consumed chamfering the pair the first
time. The sibling overload,
AddChamfer(edge, vertex, distance, angle), checks the identicalstatus correctly, which is the precedent a follow-up kernel patch PR will cite upstream. This PR
carries the immediate bridge-side guard; the kernel patch is tracked separately (matches this
repo's established pattern: bridge mitigation now, kernel patch after, e.g. #298, #341, #344, #349).
Fix:
OCCTFace2DChamfer(OCCTBridge_Modeling.mm) now checks each pair against every priorpair in the same call before invoking
AddChamfer, and rejects the whole request (nil) on amatch in either order. Reusing one edge across two different pairs, e.g. chamfering adjacent
corners of a polygon, is unaffected and stays non-nil, confirmed by a positive test that chamfers
every corner of a rectangle.
Contract chosen
Reject the whole call, matching
fillet2D(vertexIndices:radii:)'s own contract for a duplicatedvertex on this same
BRepFilletAPI_MakeFillet2dbuilder (#568).fillet2Drejects incidentally,because calling
AddFillettwice on the same vertex failsBuild()/IsDone()on its own;chamfer2Dcannot fail the same way, since its duplicate crashes inside OCCT beforeBuild()everruns, so an explicit guard was required either way. Given that, reject is the answer already
established one call away, and it sidesteps the "which distance wins" ambiguity a
first-wins/last-wins choice would introduce with no signal to the caller.
Not a vote on #633. #633 is open on the wider fillet/chamfer family's duplicate-index
direction (fillet is last-wins, chamfer is first-wins, per Cluster B's census). This fix removes a
crash, not a contract redesign, and is recorded as a data point for that issue rather than an
attempt to settle it.
Verification (separate-process, before/after)
An in-process crash kills the test runner, so the crash itself is proven in a standalone process
(temporarily repointing
Sources/OCCTTest/main.swift, restored afterward, no diff left behind):[(0, 1), (0, 1)]nil, exit 0[(0, 1), (1, 0)](reversed, same pair)nil, exit 0[(0, 1), (0, 1), (0, 1)](triple)nil, exit 0[(0, 1), (1, 2)](one edge, two different pairs)[(0, 1), (1, 2), (2, 3), (3, 0)](every corner)Injection matrix (prove-the-test-fails)
@Testcannot assert a SIGSEGV without killing the whole suite, which is exactly the shape ofdefect
okf/policies/prove-the-test-fails.mdand this repo's own precedent (Uncatchable SIGSEGV in parallel swift test run, right after concurrent OBJ imports — possibly related to #341, unconfirmed #344/Uncatchable SIGABRT in parallel swift test run — cause unknown, very little evidence (see #344 for the companion SIGSEGV) #345) call forexternal evidence, not an in-process red/green cycle.
chamfer2DRejectsDuplicatePair(new, in-process, asserts the fixed behaviour only):before the fix landed, red would mean a crash, not an assertion failure, so this test's own
red/green cycle is the table above.
chamfer2DAcceptsSharedEdgeAcrossDifferentPairs(new, in-process, proves the guard keys onthe pair, not either index alone): injected a plausible wrong fix (reject on any repeated
single index) and confirmed it turns this test red (2 assertion failures), then restored the
correct pair-based guard and confirmed green again.
Neither existing assertion in
Tests/OCCTModelingTests/Issue568IndexSkipTests.swiftwas changed;both new tests are additions after the existing
chamfer2DAcceptsResolvablePairs.Docs
Sources/OCCTSwift/Shape+Geom2d.swift:///note + a fencedswiftsnippet showing therefused duplicate.
docs/reference/Shape-Measurement.md: matching reference-page note.docs/SEMVER.md: recorded exception, a call that used to crash now returnsnil.docs/CHANGELOG.md: full writeup under## Unreleased.Scripts/repro/cluster-b-fillet-edge-contract/README.mdandScripts/repro/censuses/ClusterB.swift:the
chamfer2Dduplicate-pair row is now measured live (safe, since it no longer crashes) insteadof being noted as unsafe to run; row count unchanged at 16.
What the issue and prior measurement got wrong
AddChamferrebuilds the face incrementally, so..stale edgehandles") was a reasonable, explicitly-hedged hypothesis, not a confirmed mechanism. The actual
upstream root cause (above) is different: it is an unchecked failure status on a connectivity
lookup, not staleness from incremental rebuilding. Every doc/comment in this PR that touches the
mechanism states the corrected version.
classify_fillet_sites.py) still reportsOCCTFace2DChamferas
OTHERfor index handling, a known, already-documented blind spot (it does not recognise theocctMappedSubShapeAt-based reject idiom). Unaffected by this fix and left as-is per theexisting README note.
Verify
swift build, 0 errors, no new warnings, noOCCTSWIFT_LOCAL.swift test: 5,348 passed (baseline 5,346 plus these 2 new tests), 0 failures.swift run Censuses cluster-aunchanged at 45 rows;cluster-bunchanged at 16 rows (one cellchanged, no rows added/removed).
check-bridge-index,check-null-handle-guards,check-docs-defaultseach withand without
--self-test, andcount-operations, all exit 0.Closes #705