Skip to content

Shape Healing - initialize owires before ShapeAnalysis_FreeBounds's empty-input early return#1377

Open
gsdali wants to merge 2 commits into
Open-Cascade-SAS:masterfrom
gsdali:fix/310-freebounds-owires-uninit
Open

Shape Healing - initialize owires before ShapeAnalysis_FreeBounds's empty-input early return#1377
gsdali wants to merge 2 commits into
Open-Cascade-SAS:masterfrom
gsdali:fix/310-freebounds-owires-uninit

Conversation

@gsdali

@gsdali gsdali commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #1376.

Problem

ShapeAnalysis_FreeBounds's constructor (which runs SplitWires() internally) crashes with an
uncatchable SIGSEGV on some shapes containing multiple free-boundary components. Minimally
reproducible with just two disjoint planar faces in one compound — no shared or even nearby
geometry needed. Not a data-volume threshold: a shape with 150+ free-boundary loops can be fine
while a much smaller one crashes, depending only on whether any single component happens to close
with nothing left over.

Root cause

ShapeAnalysis_FreeBounds::SplitWire (the per-wire helper) finds each wire's closed sub-loops,
then hands whatever edges weren't consumed to ConnectEdgesToWires to chain into the "open"
result:

for (i = 1; i <= nbedges; i++)
  if (statuses.Value(i) != 2)
    edges->Append(sewd->Edge(i));
open = ShapeAnalysis_FreeBounds::ConnectEdgesToWires(edges, toler, shared);

When every edge of the wire was consumed into a closed loop, edges is a valid but empty
sequence. The call chain ConnectEdgesToWiresConnectWiresToWires (3-arg) →
ConnectWiresToWires (4-arg, with a local occ::handle<...> owires;) → connectWiresToWiresImpl
starts with:

if (iwires.IsNull() || !iwires->Length())
{
  return;
}

For empty input this returns without ever assigning owires — every caller in the file starts
from a freshly-defaulted (null) handle, so the null propagates back through ConnectEdgesToWires's
return value into SplitWire's open output parameter, into
ShapeAnalysis_FreeBounds::SplitWires's per-wire loop:

SplitWire(TopoDS::Wire(wires->Value(i)), toler, shared, tmpclosed, tmpopen);
closed->Append(tmpclosed);
open->Append(tmpopen);          // tmpopen is null here

NCollection_HSequence::Append(const opencascade::handle<T>&) dereferences the null handle
(theOther->ChangeSequence()) to obtain a reference, which is then passed into
NCollection_Sequence::Append(NCollection_Sequence&), whose first statement reads
theSeq.IsEmpty() (== theSeq.mySize) through that invalid reference — SIGSEGV.

Fix

The one-line contract restoration connectWiresToWiresImpl's own non-empty path already follows a
few lines down (owires = new NCollection_HSequence<TopoDS_Shape>; before populating it): "no
wires to connect" should produce a valid empty result, not an untouched (and, for every caller
in this file, null-defaulted) out-parameter.

Reproducer / test

BRepBuilderAPI_MakeWire mw1;
mw1.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(0,0,0), gp_Pnt(1,0,0)).Edge());
mw1.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(1,0,0), gp_Pnt(1,1,0)).Edge());
mw1.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(1,1,0), gp_Pnt(0,1,0)).Edge());
mw1.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(0,1,0), gp_Pnt(0,0,0)).Edge());
TopoDS_Face faceA = BRepBuilderAPI_MakeFace(mw1.Wire()).Face();

BRepBuilderAPI_MakeWire mw2;                                    // far away, no shared geometry
mw2.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(5,5,0), gp_Pnt(6,5,0)).Edge());
mw2.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(6,5,0), gp_Pnt(6,6,0)).Edge());
mw2.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(6,6,0), gp_Pnt(5,6,0)).Edge());
mw2.Add(BRepBuilderAPI_MakeEdge(gp_Pnt(5,6,0), gp_Pnt(5,5,0)).Edge());
TopoDS_Face faceB = BRepBuilderAPI_MakeFace(mw2.Wire()).Face();

BRep_Builder B; TopoDS_Compound comp; B.MakeCompound(comp);
B.Add(comp, faceA); B.Add(comp, faceB);

ShapeAnalysis_FreeBounds safb(comp, 0.01, true, true);  // crashes on master

Crashes on current master; fixed here. Added GTest
ShapeAnalysis_FreeBoundsTest.TwoDisjointFacesNoCrash (plus a single-face baseline).

Validation

Isolated with AddressSanitizer (an ASan-instrumented macOS-arm64 build of ModelingAlgorithms +
ModelingData + FoundationClasses, RelWithDebInfo, MMGT_OPT=0 at runtime so allocations route
through malloc/free): the two-disjoint-face repro crashes 100% of the time on stock master with an
identical signature at both -O2 and -O0 (same function, same NCollection_Sequence::Append
call, same 0xfffffffffffffff8 fault address), and returns the correct 2 closed, 0 open after
this patch. On a real-world 150-face analytic-BRep fixture (sewn with BRepBuilderAPI_Sewing):
tol=0.05 gives 152 closed / 0 open byte-identical before and after this patch (confirming no
behavior change on the working path); tol=0.10 crashes on stock master and returns
144 closed / 0 open after.

Reported downstream and isolated at SecondMouseAU/OCCTSwift#310.

gsdali added 2 commits July 19, 2026 15:39
… in connectWiresToWiresImpl

ShapeAnalysis_FreeBounds crashes (uncatchable SIGSEGV) analyzing a shape whose free
boundaries include any wire whose edges are ENTIRELY consumed by SplitWire's
closed-loop detection, leaving zero leftover ("open") edges for that component.
Minimally reproducible with two disjoint planar faces in one compound -- no shared
or even nearby geometry required, and no data-volume threshold (a shape with 150+
free-boundary loops can be fine while a 2-loop shape crashes, and vice versa,
depending only on whether any single component happens to close with nothing left
over).

Root cause: ShapeAnalysis_FreeBounds::SplitWire (the per-wire helper) finds the
closed sub-loops in a wire, then builds an "edges" list of whatever wasn't
consumed (status != 2) and hands it to ConnectEdgesToWires to chain into open
wires:

    for (i = 1; i <= nbedges; i++)
      if (statuses.Value(i) != 2)
        edges->Append(sewd->Edge(i));
    open = ShapeAnalysis_FreeBounds::ConnectEdgesToWires(edges, toler, shared);

When every edge of the wire was consumed into a closed loop, "edges" is a valid
but EMPTY sequence. ConnectEdgesToWires -> ConnectWiresToWires(3-arg) ->
ConnectWiresToWires(4-arg, with a local "occ::handle<...> owires;") ->
connectWiresToWiresImpl, whose very first statement is:

    if (iwires.IsNull() || !iwires->Length())
    {
      return;
    }

For an empty (but non-null) input this returns WITHOUT EVER ASSIGNING "owires" --
the caller's handle is left exactly as it started: null. That null propagates
back through ConnectEdgesToWires's return value into SplitWire's "open" output
parameter, into ShapeAnalysis_FreeBounds::SplitWires's per-wire loop:

    SplitWire(TopoDS::Wire(wires->Value(i)), toler, shared, tmpclosed, tmpopen);
    closed->Append(tmpclosed);
    open->Append(tmpopen);          // tmpopen is null -- crash here

NCollection_HSequence::Append(const opencascade::handle<T>&) dereferences the null
handle (theOther->ChangeSequence()) to obtain a reference to pass into
NCollection_Sequence::Append(NCollection_Sequence&), whose first statement reads
theSeq.IsEmpty() (== theSeq.mySize) through that invalid reference: SIGSEGV.

The fix is the one-line contract restoration connectWiresToWiresImpl's own
non-empty path already follows a few lines down ("owires = new
NCollection_HSequence<TopoDS_Shape>;" before populating it): "no wires to connect"
should produce a valid EMPTY result, not an untouched (and, for every caller in
this file, null-defaulted) out-parameter.

Validation (fast path, no full rebuild -- see Scripts/patches/README.md's Open-Cascade-SAS#1
entry for the override-link technique, extended here with AddressSanitizer):
built an ASan-instrumented macOS-arm64 OCCT (ModelingAlgorithms + ModelingData +
FoundationClasses, RelWithDebInfo, MMGT_OPT=0 at runtime to route allocations
through malloc/free so ASan's redzones apply). Two disjoint 4-edge planar faces in
one compound SIGSEGV 100% of the time on stock V8_0_0_p1 (confirmed identical
crash signature -- same function, same NCollection_Sequence::Append call, same
0xfffffffffffffff8 fault address -- at both -O2 and -O0); after this patch the
same input returns the correct "2 closed, 0 open" and the process exits 0. On a
608 KB real-world fixture (a 150-face analytic BRep compound, sewn with
BRepBuilderAPI_Sewing): tol=0.05 gives 152 closed/0 open both before and after the
patch (byte-identical -- confirms no behavior change on the working path); tol=0.10
crashes on stock p1 and returns 144 closed/0 open after the patch.

Reported downstream and isolated at SecondMouseAU/OCCTSwift#310. Filed upstream as
Open-Cascade-SAS#1376 (repro only, filed before the root cause was pinned
down); this patch supersedes that repro with the actual fix.
…sh (Open-Cascade-SAS#310)

Regression test for the fix in the previous commit: two disjoint planar
faces in one compound used to SIGSEGV inside
ShapeAnalysis_FreeBounds::SplitWires (via connectWiresToWiresImpl's
uninitialized owires on empty input). Verifies the correct 2 closed / 0 open
result instead.
@gsdali
gsdali force-pushed the fix/310-freebounds-owires-uninit branch from 7be6171 to b9bf0b7 Compare July 19, 2026 05:40
@dpasukhi
dpasukhi self-requested a review July 19, 2026 06:04
@dpasukhi dpasukhi added this to the Release 8.0.1 milestone Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

ShapeAnalysis_FreeBounds crashes (uncatchable SIGSEGV) on a compound of 2+ disjoint faces

2 participants