You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#371 replaced our bridge's use of the shared XCAFApp_Application::GetApplication() singleton
with a private TDocStd_Application per document, closing our exposure to the #341/#344/#349/#353
race family. A dedicated confirmation harness built to test that new pattern (private app per
thread, zero shared state, zero serialization, run against the real TSan-instrumented kernel)
found it was not a clean win: it surfaced two previously uncaught OCCT races that only become
reachable once application construction itself runs concurrently, something the old shared
singleton never allowed to happen.
Root cause
Resource_Manager::Resource_Manager(const char*, bool) (Resource_Manager.cxx:107-109) writes
a file-scope global on every construction, unsynchronized:
OSD_Environment envDebug("ResourceDebug");
Debug = (!envDebug.Value().IsEmpty()); // static bool Debug; at file scope, line 54
Reached via TDocStd_Application::Resources() (called from DefineFormat()) constructing its
lazily initialized Resource_Manager. Resources()'s own lazy init is already mutex guarded
per instance (the Uncatchable SIGSEGV in parallel swift test run, right after concurrent OBJ imports — possibly related to #341, unconfirmed #344 fix), but that only protects one TDocStd_Application instance calling Resources() twice, not N different instances each constructing their own Resource_Manager
concurrently, all writing the same global Debug.
Storage_Schema::ICurrentData() (Storage_Schema.cxx:802-805) is a function-local static Handle(Storage_Data), thread-safe to initialize (C++11 magic statics), but every Storage_Schema instance mutates the handle it returns with no synchronization at all. Storage_Schema::Storage_Schema()'s constructor calls Clear(), which does Storage_Schema::ICurrentData().Nullify(), so constructing a new Storage_Schema on one
thread can null out the handle another thread's in-flight (de)serialization is concurrently
reading via ICurrentData()->InternalData()->.... Reached via PCDM_ReadWriter_1::ReadDocumentVersion during TDocStd_Application::Open().
Neither had ever surfaced in this project's prior TSan gates (#298/#319/#341/#344/#345/#348/#349/ #353), because every one of them, and all of production until #371, shared one application
instance. Resources()'s own per-instance lazy-init mutex accidentally serialized Resource_Manager/Storage_Schema construction down to "runs once, ever, for the whole process."
Evidence
ThreadSanitizer (minimal-module build: FoundationClasses+ModelingData+ModelingAlgorithms+
DataExchange), 8 threads x 50 rounds, each round a brand new private app, DefineFormat, SaveAs, Close, then a separate brand new private app, Open, verify, Close. No two threads
ever touch the same TDocStd_Application instance. 16 race warnings, 0 functional failures.
Upstream status
Filed as OCCT#1398, not yet fixed in the
kernel. Reproducer: Scripts/repro/371-getapplication-singleton-elimination/occt_371_private_app.cpp.
Fix scope
Same pattern as #341/#344/#349/#353: a minimal, surgical mutex guarding the shared global rather
than a structural redesign.
Storage_Schema::ICurrentData(): trickier. It is read across multiple statements by callers
(ICurrentData()->InternalData()->...), so a lock held only inside ICurrentData() itself
would not protect a caller's subsequent dereference from a race against another thread's
concurrent Clear()/ISetCurrentData(). A correct fix likely needs a lock held for the
duration of one Storage_Schema-driven (de)serialization operation, acquired at the entry
points (CDF_Application-level Store/Retrieve, matching Concurrent Save/SaveAs to the same format corrupts a shared, cached storage driver instance (SIGSEGV) #349's "lock at the call sites that
invoke the shared resource" precedent), not scattered across every internal accessor.
Needs its own TSan-driven investigation before a patch is proposed upstream, per this project's
usual protocol (docs/thread-safety.md).
Practical status in this repo right now
Not blocking. ocafStoreMutex()'s coverage was expanded in #371 to also wrap the six OCCTDocumentDefineFormatBin/BinL/Xml/XmlL/BinXCAF/XmlXCAF functions and OCCTDocumentCreateWithFormat, so our own bridge is already safe against both races (confirmed:
adding an equivalent mutex to a copy of the harness gives 0 TSan warnings). This issue tracks
proposing an actual kernel fix upstream, not an outstanding safety gap in OCCTSwift itself.
Background
#371 replaced our bridge's use of the shared
XCAFApp_Application::GetApplication()singletonwith a private
TDocStd_Applicationper document, closing our exposure to the #341/#344/#349/#353race family. A dedicated confirmation harness built to test that new pattern (private app per
thread, zero shared state, zero serialization, run against the real TSan-instrumented kernel)
found it was not a clean win: it surfaced two previously uncaught OCCT races that only become
reachable once application construction itself runs concurrently, something the old shared
singleton never allowed to happen.
Root cause
Resource_Manager::Resource_Manager(const char*, bool)(Resource_Manager.cxx:107-109) writesa file-scope global on every construction, unsynchronized:
Reached via
TDocStd_Application::Resources()(called fromDefineFormat()) constructing itslazily initialized
Resource_Manager.Resources()'s own lazy init is already mutex guardedper instance (the Uncatchable SIGSEGV in parallel swift test run, right after concurrent OBJ imports — possibly related to #341, unconfirmed #344 fix), but that only protects one
TDocStd_Applicationinstance callingResources()twice, not N different instances each constructing their ownResource_Managerconcurrently, all writing the same global
Debug.Storage_Schema::ICurrentData()(Storage_Schema.cxx:802-805) is a function-local staticHandle(Storage_Data), thread-safe to initialize (C++11 magic statics), but everyStorage_Schemainstance mutates the handle it returns with no synchronization at all.Storage_Schema::Storage_Schema()'s constructor callsClear(), which doesStorage_Schema::ICurrentData().Nullify(), so constructing a newStorage_Schemaon onethread can null out the handle another thread's in-flight (de)serialization is concurrently
reading via
ICurrentData()->InternalData()->.... Reached viaPCDM_ReadWriter_1::ReadDocumentVersionduringTDocStd_Application::Open().Neither had ever surfaced in this project's prior TSan gates (#298/#319/#341/#344/#345/#348/#349/
#353), because every one of them, and all of production until #371, shared one application
instance.
Resources()'s own per-instance lazy-init mutex accidentally serializedResource_Manager/Storage_Schemaconstruction down to "runs once, ever, for the whole process."Evidence
ThreadSanitizer (minimal-module build: FoundationClasses+ModelingData+ModelingAlgorithms+
DataExchange), 8 threads x 50 rounds, each round a brand new private app,
DefineFormat,SaveAs,Close, then a separate brand new private app,Open, verify,Close. No two threadsever touch the same
TDocStd_Applicationinstance. 16 race warnings, 0 functional failures.Upstream status
Filed as OCCT#1398, not yet fixed in the
kernel. Reproducer:
Scripts/repro/371-getapplication-singleton-elimination/occt_371_private_app.cpp.Fix scope
Same pattern as #341/#344/#349/#353: a minimal, surgical mutex guarding the shared global rather
than a structural redesign.
Resource_Manager: guard theDebugwrite, or make itstd::atomic<bool>(it is a plainprocess-wide flag, not meant to express per-instance intent, so atomic looks sufficient here,
unlike NCollection race under parallel execution: documented downstream as 'run --no-parallel', never filed or characterised upstream — needs the #298 TSan protocol #341's
theAutoNamingwhich needed the deeper per-instance redesign).Storage_Schema::ICurrentData(): trickier. It is read across multiple statements by callers(
ICurrentData()->InternalData()->...), so a lock held only insideICurrentData()itselfwould not protect a caller's subsequent dereference from a race against another thread's
concurrent
Clear()/ISetCurrentData(). A correct fix likely needs a lock held for theduration of one
Storage_Schema-driven (de)serialization operation, acquired at the entrypoints (
CDF_Application-level Store/Retrieve, matching Concurrent Save/SaveAs to the same format corrupts a shared, cached storage driver instance (SIGSEGV) #349's "lock at the call sites thatinvoke the shared resource" precedent), not scattered across every internal accessor.
Needs its own TSan-driven investigation before a patch is proposed upstream, per this project's
usual protocol (
docs/thread-safety.md).Practical status in this repo right now
Not blocking.
ocafStoreMutex()'s coverage was expanded in #371 to also wrap the sixOCCTDocumentDefineFormatBin/BinL/Xml/XmlL/BinXCAF/XmlXCAFfunctions andOCCTDocumentCreateWithFormat, so our own bridge is already safe against both races (confirmed:adding an equivalent mutex to a copy of the harness gives 0 TSan warnings). This issue tracks
proposing an actual kernel fix upstream, not an outstanding safety gap in OCCTSwift itself.