Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ suite into these targets (each `Tests/OCCT<Domain>Tests/`, declared in `Package.
## Known OCCT Bugs

- `BRepExtrema_ExtCC` crashes when edges are parallel — guard with `if (result.isParallel) { return result; }` before accessing points
- ~~Container-overflow in NCollection on arm64 macOS~~ — **this claim was never characterized and does not hold up.** Investigated for #341 (2026-07-21) using the #298 TSan protocol (minimal-module ThreadSanitizer build of V8_0_0_p1 + all 10 carried patches): `FoundationClasses`+`ModelingData`+`ModelingAlgorithms` stress scenarios (concurrent create/fuse/fillet, independent meshing) are clean except the already-known benign `BOPAlgo_InitMessages` lazy-init race. No NCollection race reproduced anywhere. Re-enabled the 3 suites in `Tests/OCCTStressTests/StressConcurrencyTests.swift` that had been `.disabled()` under this same unevidenced claim (some since ~v0.51.0) — 25/25 clean runs. **A real, different, previously-undetected race was found instead**: `XCAFDoc_ShapeTool::theAutoNaming`, a process-global `static bool` that `RWMesh_CafReader::fillDocument()`, `RWGltf_CafReader::fillDocument()` (a separate near-duplicate override — reachable via OBJ **and** glTF import), and `XCAFDoc_Editor::Expand()` (reentrant — recurses into itself) all save/mutate/restore with zero synchronization; `XCAFDoc_ShapeTool::AddShape` reads the same flag from every one of those and from ordinary unscoped calls too. Same failure class as #298 (unsynchronized global save/modify/restore), but logical/cosmetic (wrong auto-naming, or racy for the plain `bool` itself) rather than geometric. **Fixed upstream in v1.15.5** (`Scripts/patches/0011-*`, xcframework rebuilt): `XCAFDoc_ShapeTool::AutoNamingScope` (RAII, `std::recursive_mutex`-backed) replaces the three ad hoc save/restore call sites, and `theAutoNaming` itself is now `std::atomic<bool>` so unscoped readers (e.g. `AddShape` calls outside any of the three sites) are no longer racing on the raw storage either. Verified via TSan: 0 races across 4 runs (was 9-17/run), zero regression on the #298/independent-meshing scenarios. The interim bridge-side `meshCafMutex()` mitigation shipped in v1.15.4 was removed once this kernel patch shipped — matches the #298 PR1→PR2 pattern. Filed upstream as [OCCT#1387](https://github.com/Open-Cascade-SAS/OCCT/issues/1387) (repro) / [OCCT#1388](https://github.com/Open-Cascade-SAS/OCCT/pull/1388) (fix, draft). See [`Scripts/repro/341-meshcaf/`](https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/341-meshcaf) for the TSan reproducer and full writeup. #341. The two hard crashes (SIGSEGV/SIGABRT) observed empirically in ~2/20 full-suite parallel `swift test` runs during this investigation remain uncharacterized and are filed separately: #344 (SIGSEGV, garbage fault address, immediately after two concurrent OBJ imports — possibly the same `theAutoNaming` race in a rarer timing window, unconfirmed, worth re-testing now that #341 shipped) and #345 (SIGABRT, essentially no localizing evidence).
- ~~Container-overflow in NCollection on arm64 macOS~~ — **this claim was never characterized and does not hold up.** Investigated for #341 (2026-07-21) using the #298 TSan protocol (minimal-module ThreadSanitizer build of V8_0_0_p1 + all 10 carried patches): `FoundationClasses`+`ModelingData`+`ModelingAlgorithms` stress scenarios (concurrent create/fuse/fillet, independent meshing) are clean except the already-known benign `BOPAlgo_InitMessages` lazy-init race. No NCollection race reproduced anywhere. Re-enabled the 3 suites in `Tests/OCCTStressTests/StressConcurrencyTests.swift` that had been `.disabled()` under this same unevidenced claim (some since ~v0.51.0) — 25/25 clean runs. **A real, different, previously-undetected race was found instead**: `XCAFDoc_ShapeTool::theAutoNaming`, a process-global `static bool` that `RWMesh_CafReader::fillDocument()`, `RWGltf_CafReader::fillDocument()` (a separate near-duplicate override — reachable via OBJ **and** glTF import), and `XCAFDoc_Editor::Expand()` (reentrant — recurses into itself) all save/mutate/restore with zero synchronization; `XCAFDoc_ShapeTool::AddShape` reads the same flag from every one of those and from ordinary unscoped calls too. Same failure class as #298 (unsynchronized global save/modify/restore), but logical/cosmetic (wrong auto-naming, or racy for the plain `bool` itself) rather than geometric. **Fixed upstream in v1.15.5** (`Scripts/patches/0011-*`, xcframework rebuilt): `XCAFDoc_ShapeTool::AutoNamingScope` (RAII, `std::recursive_mutex`-backed) replaces the three ad hoc save/restore call sites, and `theAutoNaming` itself is now `std::atomic<bool>` so unscoped readers (e.g. `AddShape` calls outside any of the three sites) are no longer racing on the raw storage either. Verified via TSan: 0 races across 4 runs (was 9-17/run), zero regression on the #298/independent-meshing scenarios. The interim bridge-side `meshCafMutex()` mitigation shipped in v1.15.4 was removed once this kernel patch shipped — matches the #298 PR1→PR2 pattern. Filed upstream as [OCCT#1387](https://github.com/Open-Cascade-SAS/OCCT/issues/1387) (repro) / [OCCT#1388](https://github.com/Open-Cascade-SAS/OCCT/pull/1388) (fix, draft). See [`Scripts/repro/341-meshcaf/`](https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/341-meshcaf) for the TSan reproducer and full writeup. #341. The two hard crashes (SIGSEGV/SIGABRT) observed empirically in ~2/20 full-suite parallel `swift test` runs during this investigation were filed separately as #344 (SIGSEGV, root-caused below) and #345 (SIGABRT, still uncharacterized — essentially no localizing evidence).
- `XCAFApp_Application::GetApplication()` / `CDF_Directory::Add` — **#344, the SIGSEGV #341 didn't explain.** Confirmed to survive the #341 kernel fix in v1.15.5 (re-ran the parallel `swift test` loop 12× on v1.15.5: 1 more hit, same signature) — a genuinely different, previously-undetected pair of races. `GetApplication()`'s lazy singleton init (`static Handle(XCAFApp_Application) locApp; if (locApp.IsNull()) { locApp = new XCAFApp_Application; }`) is a textbook double-checked-locking-without-locking bug: two threads' first concurrent call can both construct a new instance and race to assign `locApp`. TSan shows this is the dominant defect — it produces multiple concurrently-constructed `XCAFApp_Application` instances, cascading into races across dozens of unrelated destructors as the "losing" instances are torn down mid-flight. Separately, `CDF_Directory::Add`/`Remove`/`Contains` mutate/read `myDocuments` (a plain `NCollection_List`) with zero synchronization — every `CDF_Application` is normally one process-wide instance shared by every caller, so its one `CDF_Directory` receives `Add()` from every document-creating call on every thread, racing on `NCollection_BaseList::PAppend`. The #341 TSan stress never caught either: it builds `TDocStd_Document` directly, bypassing `XCAFApp_Application`/`CDF_Application` entirely — the real bridge path (`OCCTDocumentLoadOBJ` and every other document-producing call) does not. **Fixed in v1.15.6** (`Scripts/patches/0012-*`, xcframework rebuilt): `GetApplication()` folds construction into the static local's initializer (C++11 magic statics, thread-safe exactly once); `CDF_Directory` gets a private mutex guarding `Add`/`Remove`/`Contains`/`Length`/`IsEmpty`/`Last`. Verified via a debug (`-O0 -g`) build with a temporary `SIGSEGV`/`SIGBUS` handler: stock p1 crashes ~50% of runs at 10 threads × 3000 barrier-synchronized rounds, both captured backtraces resolving to `TDocStd_Application::NewDocument -> CDF_Application::Open`; TSan (same minimal-module protocol as #298/#319/#341) goes from 234 race reports to 9, all in `CDF_Directory::Add`/`PAppend` and all showing the same mutex held on both sides of the reported conflict — consistent with a TSan/allocator-recycling artifact (a control program with a trivially-correct mutex pattern shows no such warning under identical flags), not a genuine unaddressed race; the entire `GetApplication()`-driven destructor cascade is gone entirely. Filed upstream as [OCCT#1389](https://github.com/Open-Cascade-SAS/OCCT/issues/1389) (repro) / [OCCT#1390](https://github.com/Open-Cascade-SAS/OCCT/pull/1390) (fix, two commits). **Found during validation of this same fix**: correctly making `GetApplication()` a true singleton means every caller now genuinely shares ONE `TDocStd_Application` instance — surfacing more races on that instance's OTHER unsynchronized state, previously masked by threads sometimes getting different (uncontended) instances. Repeated `swift test` runs hit a SIGTRAP in `Resource_Manager::SetResource` (via `TDocStd_Application::DefineFormat`, itself called by the common `Document.defineAllFormats()` test-setup path) and a SIGSEGV in `TDocStd_Application::ReadingFormats` iterating `CDF_Application::myReaders` concurrently with a writer. `TDocStd_Application::Resources()` has the identical lazy-init bug as `GetApplication()`; `Resource_Manager`'s maps and `CDF_Application::myReaders`/`myWriters` have zero synchronization. Also fixed in v1.15.6 (same patch): a mutex for `Resources()`'s lazy-init, a `std::recursive_mutex` for `Resource_Manager`'s accessors (added an explicit copy constructor too — the new mutex broke `ShapeProcess_Context.cxx`'s existing `new Resource_Manager(*sRC)` thread-safety workaround, whose own comment already acknowledged this exact defect: *"calling of SetResource() for one object in multiple threads causes race condition"*), and a mutex for `myReaders`/`myWriters`. 0/12 further `swift test` runs of `OCCTXCAFTests` reproduce either crash after the fix. A THIRD, architecturally different crash surfaced in the same validation (`BinLDrivers_DocumentStorageDriver::Write` corrupting a shared, cached, non-reentrant storage-driver instance under concurrent `Save`/`SaveAs` of the same format) — a shared worker object, not a container needing a lock, so the kernel fix needs its own TSan investigation; filed separately as #349. It was severe enough on its own (~60% crash rate in `OCCTXCAFTests` alone once the two races above stopped masking it) that v1.15.6 ships an **interim bridge-side mitigation** for it too: `ocafStoreMutex()` (`OCCTBridge_Document.mm`) serializes `OCCTDocumentSaveOCAF`/`OCCTDocumentSaveOCAFInPlace`/`OCCTDocumentLoadOCAF` — the same #298/#341 PR1→PR2 pattern (bridge mutex now, kernel fix later). 0/12 further `swift test` runs of `OCCTXCAFTests` crash after this mitigation (some pre-existing, unrelated test-fixture flakes remain — hardcoded non-unique temp file paths across parallel `OCAF Save/Load` tests yielding `.alreadyRetrieved`, and the already-known `Issue173AssemblySTEPTests` flake — neither a kernel bug). See [`Scripts/repro/344-cdf-directory/`](https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/344-cdf-directory) for the full writeup. #344.
- `LocOpe_SplitDrafts` throws on incompatible geometry — always wrap `Perform()` in try-catch in bridge
- `BRepOffsetAPI_ThruSections` (loft) SIGSEGV'd (null deref, "Address 8") on mismatched closed profiles — `BRepFill_CompatibleWires::SameNumberByPolarMethod` over-advanced an unguarded correspondence-list iterator. It's an OS signal, so the bridge `catch(...)` cannot save it. **Fixed upstream in OCCT 8.0.0p1** (Open-Cascade-SAS/OCCT#1298, OCCTSwift #176/#178); the previously-carried `Scripts/patches/0001-*` was dropped — the current p1 xcframework has the guard natively (regression test "Loft polar-method SIGSEGV regression (#176)" passes against it). Note: `OCC_CATCH_SIGNALS` is inert in our build (no `OCC_CONVERT_SIGNALS`) — do not rely on it for signal safety; OS signals raised inside OCCT (e.g. #234) are still uncatchable in-process.
- `ShapeAnalysis_FreeBounds` (backs `Shape.freeBoundsClosedWires`/`freeBoundsClosedCount`/`freeBoundsOpenWires`, and `Shape.freeBounds`) used to SIGSEGV (uncatchable) on certain shapes with multiple free-boundary components — isolated via AddressSanitizer to `connectWiresToWiresImpl`'s empty-input early return (`ShapeAnalysis_FreeBounds.cxx`) leaving its `owires` out-parameter uninitialized (null), which later crashes `NCollection_HSequence::Append`. Minimally reproducible with just two disjoint planar faces in one compound; not a simple loop-count threshold (150+ loops can be fine, 2 can crash) — depends only on whether any single component's boundary closes with zero edges left over. **Fixed upstream in v1.12.6** (`Scripts/patches/0004-*`, xcframework rebuilt): `connectWiresToWiresImpl` now initializes `owires` to an empty sequence before its early return. #310, upstream repro filed as Open-Cascade-SAS/OCCT#1376, fix as [OCCT#1377](https://github.com/Open-Cascade-SAS/OCCT/pull/1377); sibling `Standard_OutOfRange` bug in the same file, OCCT#1330, was a separate, unrelated defect in the same function — also now carried, see the `0007` entry below.
Expand Down
16 changes: 9 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,22 @@ let occtTarget: Target = useLocalBinary
name: "OCCT",
path: "Libraries/OCCT.xcframework"
)
// v1.15.5 rebuild: OCCT 8.0.0p1 + our carried patches — 0001 (ShapeFix_Face guard, #263),
// v1.15.6 rebuild: OCCT 8.0.0p1 + our carried patches — 0001 (ShapeFix_Face guard, #263),
// 0002 (backport of upstream OCCT#1334, #280), 0003 (fillet TopOpeBRep thread_local, #298),
// 0004 (ShapeAnalysis_FreeBounds owires init, #310), 0005 (ShapeFix_Face null-Context guard
// in FixPeriodicDegenerated, #317), 0006 (BRepGProp_EdgeTool adaptor NbPoles, #318), 0007
// (ShapeAnalysis_FreeBounds lwire reset, #323), 0008 (Geom_BSplineCurve O(1)
// PeriodicNormalization, #323), 0009 (StepData_StepWriter split oversized string, #323), 0010
// (Intf_Interference O(1) tangent-zone lookup + checkpointed breaker, #319), and 0011
// (XCAFDoc_ShapeTool::AutoNamingScope, #341).
// (Intf_Interference O(1) tangent-zone lookup + checkpointed breaker, #319), 0011
// (XCAFDoc_ShapeTool::AutoNamingScope, #341), and 0012 (XCAFApp_Application::GetApplication/
// TDocStd_Application::Resources lazy-init races + CDF_Directory/Resource_Manager/
// CDF_Application reader-writer map synchronization, #344).
// Bump BOTH url and checksum whenever the xcframework is rebuilt, or URL-resolving consumers
// silently keep the previous kernel while local sibling builds get the new one.
: .binaryTarget(
name: "OCCT",
url: "https://github.com/SecondMouseAU/OCCTSwift/releases/download/v1.15.5/OCCT.xcframework.zip",
checksum: "292c79cfde971751533873b0594b739edf9c80faae7c90c848282b43a685736b"
url: "https://github.com/SecondMouseAU/OCCTSwift/releases/download/v1.15.6/OCCT.xcframework.zip",
checksum: "2744457ea311cd56d94c41621fb8977daa7141ec9a4f0e4bdc0aff26b0925b61"
)

// OCCTBridge is 16 Objective-C++ files / ~62K lines wrapping the OCCT header tree; SwiftPM recompiles
Expand Down Expand Up @@ -74,8 +76,8 @@ let occtBridgeTarget: Target = useBridgeLocalBinary
// the OCCT.xcframework convention above.
? .binaryTarget(
name: "OCCTBridge",
url: "https://github.com/SecondMouseAU/OCCTSwift/releases/download/v1.15.5/OCCTBridge.xcframework.zip",
checksum: "5564c99c570a4da5eb5bef25d3f4cf3e30e2701185b8b53fd118a2e5548fe7eb"
url: "https://github.com/SecondMouseAU/OCCTSwift/releases/download/v1.15.6/OCCTBridge.xcframework.zip",
checksum: "dbbbae1fc580d983fd5fd19e707319f2cd01b5c8f2461aa268708778428b3ff9"
)
: .target(
name: "OCCTBridge",
Expand Down
Loading