Skip to content

Fix Resource_Manager::Debug and Storage_Schema::ICurrentData() data races#1399

Open
gsdali wants to merge 1 commit into
Open-Cascade-SAS:masterfrom
gsdali:fix/374-resource-manager-storage-schema-race
Open

Fix Resource_Manager::Debug and Storage_Schema::ICurrentData() data races#1399
gsdali wants to merge 1 commit into
Open-Cascade-SAS:masterfrom
gsdali:fix/374-resource-manager-storage-schema-race

Conversation

@gsdali

@gsdali gsdali commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #1398.

Resource_Manager::Resource_Manager(const char*, bool) (Resource_Manager.cxx:109) writes a
file-scope static bool Debug on every construction with zero synchronization. Two
Resource_Manager instances constructed concurrently on different threads (e.g. two independent
TDocStd_Application instances each lazily building their own via Resources()/DefineFormat())
race on this write. Fixed by making Debug a std::atomic<bool> — it is a plain process-wide
debug-logging flag, not meant to express per-instance intent, so an atomic is sufficient here.

Storage_Schema::ICurrentData() (Storage_Schema.cxx:802) is a function-local static
Handle(Storage_Data) mutated with no synchronization anywhere: Write() sets it for the
duration of one store, and any Storage_Schema construction — including a throwaway instance
built only to read header info, e.g. PCDM_ReadWriter_1::ReadDocumentVersion/
ReadReferenceCounter/ReadReferences, all reachable unconditionally from a plain
TDocStd_Application::Open() via CDF_Application::Retrieve — calls Clear() ->
ICurrentData().Nullify() in its constructor, which can null out a different, unrelated
Storage_Schema instance's in-flight Write() on another thread (or race a concurrent load's own
throwaway construction). Fixed by adding Storage_Schema::ICurrentDataMutex(), a
std::recursive_mutex (recursive because Write()'s own critical section re-enters
BindType()/AddPersistent()/PersistentToAdd() on the same thread via per-type
Storage_CallBack::Write() callbacks — a plain mutex would deadlock there) guarding every touch
point: the constructor's Clear(), Write()'s entire body (held for the whole call, since one
Write() is a single atomic "session" against this global, not just each individual access),
BindType(), TypeBinding(), AddPersistent(), PersistentToAdd(), HasTypeBinding(), and
ISetCurrentData().

Both races are invisible under the traditional single shared XCAFApp_Application /
TDocStd_Application usage pattern, because in that pattern Resources()'s own lazy-init mutex
happens to serialize Resource_Manager/Storage_Schema construction down to "runs once, ever, for
the whole process." They only become reachable once an application is constructed privately per
caller/thread (the pattern this project's own TDocStd_Application documentation recommends over
the deprecated XCAFApp_Application::GetApplication() singleton) and multiple independent
instances are built concurrently — see the linked issue for the full TSan writeup and a
standalone reproducer.

No public API signature changes.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

ThreadSanitizer (minimal-module build: FoundationClasses + ModelingData + ModelingAlgorithms +
DataExchange, RelWithDebInfo, -fsanitize=thread -g), 8 threads x 30 rounds, each round building
a brand-new private TDocStd_Application, defining formats, populating and saving a document,
closing it, then a separate brand-new private application opening + verifying + closing it — no
two threads ever share an application/schema instance.

  • Before this fix: 13 TSan race warnings (2x Resource_Manager::Resource_Manager, 11x
    Storage_Schema::Storage_Schema's Clear() call), 0 functional failures.
  • After this fix: 0 races across 4 repeated runs (8x30, 8x50, 10x60, 8x40), 0 functional failures.

Reproducer and full writeup:
https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/374-resource-manager-storage-schema-race

Checklist:

  • My code follows the code style of this project (clang-format --dry-run --Werror clean on
    every touched file)
  • My change requires a change to the documentation
    • I have updated the documentation accordingly (no user-facing documentation covers this
      internal-only synchronization change)

…aces

Resource_Manager::Resource_Manager(const char*, bool) writes a file-scope
static bool Debug on every construction with zero synchronization; two
Resource_Manager instances constructed concurrently on different threads
(e.g. two independent TDocStd_Application instances each lazily building
their own via Resources()/DefineFormat()) race on this write. Fixed by
making Debug a std::atomic<bool>.

Storage_Schema::ICurrentData() is a function-local static Handle(Storage_Data)
mutated with no synchronization: Write() sets it for the duration of one
store, and any Storage_Schema construction (including a throwaway instance
built only to read header info, e.g. PCDM_ReadWriter_1::ReadDocumentVersion)
calls Clear() -> ICurrentData().Nullify() in its constructor, which can null
out a different, unrelated Storage_Schema instance's in-flight Write() on
another thread. Fixed by adding Storage_Schema::ICurrentDataMutex(), a
recursive mutex (recursive because Write()'s own critical section re-enters
BindType()/AddPersistent()/PersistentToAdd() on the same thread via
Storage_CallBack::Write() callbacks), guarding every touch point: the
constructor's Clear(), Write()'s entire body, BindType(), TypeBinding(),
AddPersistent(), PersistentToAdd(), HasTypeBinding(), and ISetCurrentData().

Both races were found while testing a private TDocStd_Application-per-caller
usage pattern (multiple independent application instances constructed
concurrently, never sharing state) -- see the attached issue for a full TSan
writeup and reproducer.
gsdali added a commit to SecondMouseAU/OCCTSwift that referenced this pull request Jul 24, 2026
…aces (v1.15.18)

Fixes the two upstream OCCT foundation-layer races #371's confirmation
harness turned up (OCCT#1398): Resource_Manager::Debug is now a
std::atomic<bool>, and Storage_Schema::ICurrentData() is guarded by a new
recursive mutex spanning construction, Write(), and the type-binding
accessors. Kernel-only change (Scripts/patches/0016); OCCT.xcframework
rebuilt, OCCTBridge.xcframework unchanged.

Upstream fix proposed as Open-Cascade-SAS/OCCT#1399 (fixes OCCT#1398).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Resource_Manager::Debug and Storage_Schema::ICurrentData() race under legitimate multi-instance TDocStd_Application usage

1 participant