Skip to content

DataExchange, XCAF - AutoNamingScope guards XCAFDoc_ShapeTool::theAutoNaming#1388

Open
gsdali wants to merge 1 commit into
Open-Cascade-SAS:masterfrom
gsdali:fix/341-xcafdoc-shapetool-autonaming-race
Open

DataExchange, XCAF - AutoNamingScope guards XCAFDoc_ShapeTool::theAutoNaming#1388
gsdali wants to merge 1 commit into
Open-Cascade-SAS:masterfrom
gsdali:fix/341-xcafdoc-shapetool-autonaming-race

Conversation

@gsdali

@gsdali gsdali commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

XCAFDoc_ShapeTool::theAutoNaming is a process-global static bool (documented as such — "This
setting is global; it cannot be made a member function") that three independent call sites each
save/modify/restore around their own work with no locking:

  • RWMesh_CafReader::fillDocument() — shared base of RWObj_CafReader and RWGltf_CafReader, so
    reachable from both OBJ and glTF import
  • RWGltf_CafReader::fillDocument() — a separate, near-duplicate override, not a call into the
    base class's version
  • XCAFDoc_Editor::Expand() — additionally recurses into itself while the save/restore is in flight

XCAFDoc_ShapeTool::AddShape/addShape reads the same flag internally to decide whether to
auto-generate a name.

ThreadSanitizer on a concurrent-OBJ-import stress (8-10 threads, each writing/reading its own
uniquely-named file — not a file-path collision) reports 9-17 races per run on theAutoNaming,
resolving to two distinct problems:

  1. Two threads' save/modify/restore critical sections can interleave, so one thread's restore
    stomps another's still-in-progress override — a logical bug (wrong auto-naming mode active for
    part of a build), independent of memory safety.
  2. theAutoNaming itself is a plain bool, read and written with no synchronization from any
    caller — including ordinary AddShape calls made outside all three save/restore sites above — a
    genuine data race on every access.

Fix

Two parts, both needed:

  1. XCAFDoc_ShapeTool::AutoNamingScope — a new RAII helper (AutoNamingScope(bool) constructor /
    destructor) that holds a std::recursive_mutex for its entire lifetime, not just around the
    individual get/set calls, so two threads' save-modify-restore sequences serialize instead of
    interleaving. Recursive because XCAFDoc_Editor::Expand() reenters it on the same thread. All
    three call sites now use it; Expand()'s two duplicate manual-restore-before-return sites
    collapse into one destructor-driven restore that fires on every exit path.
  2. theAutoNaming is now std::atomic<bool> instead of a plain bool, so every access anywhere in
    the file — including AddShape's internal read, which participates in none of the three scoped
    sections — is well-defined, closing the gap the mutex alone doesn't reach.

Testing

Minimal-module ThreadSanitizer build (FoundationClasses+ModelingData+ModelingAlgorithms+
DataExchange, RelWithDebInfo, -fsanitize=thread). A pure-C++ harness runs N threads each
building a box, meshing it, writing it to its own uniquely-named .obj file (RWObj_CafWriter),
and reading it back (RWObj_CafReader) into a fresh TDocStd_Document.

  • Before: 10 threads × 200 iterations reports 9-17 theAutoNaming races per run (4 runs).
  • After: 0 theAutoNaming races across the same stress, 4 separate runs — only an unrelated,
    pre-existing Message_PrinterOStream/std::cout console-write race remains (OCCT's default
    messenger has no internal locking; out of scope for this PR).
  • Zero regression on unrelated concurrent-shape-creation and independent-meshing scenarios.
  • RWGltf_CafReader's copy of the fix compiles cleanly (mechanically identical to the
    RWMesh_CafReader path that was exercised under TSan) but wasn't run under TSan directly in this
    session — the minimal-module build excludes TKDEGLTF (needs RapidJSON, disabled for build speed).

No behavior change for the existing (documented) semantics of SetAutoNaming/AutoNaming as a
single process-wide setting — this only serializes the internal save/modify/restore idiom and makes
the underlying storage well-defined.

Fixes #1387.

@gkv311

gkv311 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@gsdali

I think that a mutex is not the right tool here and I don't quite understand how current patch fixes concurrency issue as theAutoNaming usage remains unprotected within XCAFDoc_ShapeTool.cpp itself.

What I would suggest is to add local property overriding global value as a XCAFDoc_ShapeTool class field and use it in specific places. I have prototyped the patch for an older OCCT - fill free to borrow the idea for your patch:

---------------------- src/XCAFDoc/XCAFDoc_ShapeTool.hxx
   Standard_EXPORT static Standard_Boolean AutoNaming();
-  
+
+  //! Return autonaming flag for this tool instance.
+  Standard_Boolean OwnAutoNaming() const
+  {
+    return myOwnAutonaming == -1 ? XCAFDoc_ShapeTool::AutoNaming() : myOwnAutonaming == 1;
+  }
+
+  //! Set autonaming flag for this tool instance.
+  void SetOwnAutoNaming (Standard_Boolean theOwnFlag)
+  {
+    myOwnAutonaming = theOwnFlag ? 1 : 0;
+  }
+
+  //! Reset autonaming flag for this tool instance to global state.
+  void UnsetOwnAutoNaming()
+  {
+    myOwnAutonaming = -1;
+  }
...
   //! Makes a shape on label L to be a reference to shape refL
   //! with location loc
-  Standard_EXPORT static void MakeReference (const TDF_Label& L, const TDF_Label& refL, const TopLoc_Location& loc);
+  Standard_EXPORT void MakeReference (const TDF_Label& L, const TDF_Label& refL, const TopLoc_Location& loc);
...
-  Standard_Boolean hasSimpleShapes;
-
+  Standard_Boolean hasSimpleShapes = false;
+  //! Define own autonaming property; -1 by default which means to use global flag
+  Standard_Integer myOwnAutonaming = -1;
 
 };
...
--------------------- src/XCAFDoc/XCAFDoc_ShapeTool.cxx ----------------------
index 3b36233880..b3357c9d65 100644
@@ -454,7 +454,7 @@ void XCAFDoc_ShapeTool::MakeReference (const TDF_Label &L,
   refNode->Remove(); // abv: fix against bug in TreeNode::Append()
   mainNode->Append(refNode);
 
-  if (theAutoNaming)
+  if (OwnAutoNaming())
     SetLabelNameByLink(L);
 }
 
@@ -529,7 +529,7 @@ TDF_Label XCAFDoc_ShapeTool::addShape (const TopoDS_Shape& S, const Standard_Boo
 //  }
   A->SetShape(S);
   
-  if (theAutoNaming)
+  if (OwnAutoNaming())
     SetLabelNameByShape(ShapeLabel);
 
   // if shape is Compound and flag is set, create assembly
@@ -537,7 +537,7 @@ TDF_Label XCAFDoc_ShapeTool::addShape (const TopoDS_Shape& S, const Standard_Boo
     // mark assembly by assigning UAttribute
     Handle(TDataStd_UAttribute) Uattr;
     Uattr = TDataStd_UAttribute::Set ( ShapeLabel, XCAFDoc::AssemblyGUID() );
-    if (theAutoNaming)
+    if (OwnAutoNaming())
       TDataStd_Name::Set(ShapeLabel, TCollection_ExtendedString("ASSEMBLY"));
 
     // iterate on components
@@ -1564,7 +1564,7 @@ Standard_Boolean XCAFDoc_ShapeTool::SetSHUO (const TDF_LabelSequence& labels,
   
   TDF_TagSource aTag;
   TDF_Label UpperSubL = aTag.NewChild( labels( 1 ) );
-  if (theAutoNaming) {
+  if (OwnAutoNaming()) {
     TCollection_ExtendedString Entry("SHUO");
     TDataStd_Name::Set(UpperSubL, TCollection_ExtendedString( Entry ));
   }
@@ -1575,7 +1575,7 @@ Standard_Boolean XCAFDoc_ShapeTool::SetSHUO (const TDF_LabelSequence& labels,
   // add other next_usage occurrences.
   for (i = 2; i <= labels.Length(); i++) {
     TDF_Label NextSubL = aTag.NewChild( labels( i ) );
-    if (theAutoNaming) {
+    if (OwnAutoNaming()) {

------------------------ src/XCAFDoc/XCAFDoc_Editor.cxx ------------------------
index 6897238bc3..b18adf6217 100644
@@ -68,8 +68,7 @@ Standard_Boolean XCAFDoc_Editor::Expand (const TDF_Label& theDoc,
     return Standard_False;
   }
   Handle(XCAFDoc_ShapeTool) aShapeTool = XCAFDoc_DocumentTool::ShapeTool(theDoc);
-  Standard_Boolean isAutoNaming = aShapeTool->AutoNaming();
-  aShapeTool->SetAutoNaming(Standard_False);
+  aShapeTool->SetOwnAutoNaming(false);
 
   TDF_Label aCompoundPartL = theShape;
   if (aShapeTool->IsReference(theShape))
@@ -137,10 +136,10 @@ Standard_Boolean XCAFDoc_Editor::Expand (const TDF_Label& theDoc,
         }
       }
     }
-    aShapeTool->SetAutoNaming(isAutoNaming);
+    aShapeTool->UnsetOwnAutoNaming();
     return Standard_True;
   }
-  aShapeTool->SetAutoNaming(isAutoNaming);
+  aShapeTool->UnsetOwnAutoNaming();
   return Standard_False;
 }
----------------------- src/RWMesh/RWMesh_CafReader.cxx -----------------------
index 4164e4496f..d021a51043 100644
@@ -178,11 +178,10 @@ void RWMesh_CafReader::fillDocument()
     Message::SendWarning("Warning: Length unit of document not equal to the system length unit");
   }
 
-  const Standard_Boolean wasAutoNaming = XCAFDoc_ShapeTool::AutoNaming();
-  XCAFDoc_ShapeTool::SetAutoNaming (Standard_False);
   const TCollection_AsciiString aRootName; // = generateRootName (theFile);
   CafDocumentTools aTools;
   aTools.ShapeTool = XCAFDoc_DocumentTool::ShapeTool (myXdeDoc->Main());
+  aTools.ShapeTool->SetOwnAutoNaming (false);
   aTools.ColorTool = XCAFDoc_DocumentTool::ColorTool (myXdeDoc->Main());
   aTools.VisMaterialTool = XCAFDoc_DocumentTool::VisMaterialTool (myXdeDoc->Main());
   for (TopTools_SequenceOfShape::Iterator aRootIter (myRootShapes); aRootIter.More(); aRootIter.Next())
@@ -190,7 +189,7 @@ void RWMesh_CafReader::fillDocument()
     addShapeIntoDoc (aTools, aRootIter.Value(), TDF_Label(), aRootName);
   }
   XCAFDoc_DocumentTool::ShapeTool (myXdeDoc->Main())->UpdateAssemblies();
-  XCAFDoc_ShapeTool::SetAutoNaming (wasAutoNaming);
+  aTools.ShapeTool->UnsetOwnAutoNaming();
 }

In this way, the API with the global setting will remain working, while allowing to disable auto-naming locally within specific algorithms.

…rride

Follow-up to review feedback on the AutoNamingScope fix (a
std::recursive_mutex-guarded save/restore of the single process-wide
theAutoNaming flag): the mutex serialized the three known override call
sites against each other, but every other read of theAutoNaming in
XCAFDoc_ShapeTool.cxx (AddShape, MakeReference, SetSHUO) stayed outside
any scope, so an unrelated, unscoped caller on another thread could
still observe another thread's temporary override. Locking made the
raw flag access memory-safe; it did not make the override behavior
correct for callers outside the three scoped sites.

theAutoNaming was never meant to express per-document intent -- the
three call sites that override it (RWMesh_CafReader::fillDocument(),
RWGltf_CafReader::fillDocument(), XCAFDoc_Editor::Expand()) all want to
suppress naming for one document's build, not change a process-wide
setting. XCAFDoc_ShapeTool is already one instance per document, so
the override belongs there instead of on a shared global.

XCAFDoc_ShapeTool gets an own-override field (myOwnAutonaming: -1
inherits the process-wide default, 0/1 is a local override) and
OwnAutoNaming()/SetOwnAutoNaming()/UnsetOwnAutoNaming() accessors.
AddShape, MakeReference and SetSHUO read OwnAutoNaming() instead of
theAutoNaming directly. MakeReference is no longer static, since it
now reads instance state. OwnAutoNamingScope replaces AutoNamingScope
as the RAII helper, saving and restoring the instance's own override
(not a shared flag) -- no locking needed, since two threads working on
two different documents never touch each other's state, and nested
scopes on the same instance (Expand() recurses into itself) compose
correctly because each one restores exactly the override state it
observed on entry, not an unconditional "off".

Verified with the same TSan stress as the prior fix (10 threads x 200
iterations, obj_roundtrip_unique): zero theAutoNaming races, matching
the previous result. Added a second stress scenario exercising the
property the mutex fix couldn't guarantee: half the threads locally
force auto-naming off via OwnAutoNamingScope on their own document,
concurrently with the other half doing plain unscoped AddShape() on
independent documents relying on the process-wide default -- 3000
operations, zero instances of the unscoped threads observing another
thread's override.

theAutoNaming itself stays std::atomic<bool>: SetAutoNaming()/
AutoNaming() remain callable concurrently from any thread at any time,
independent of any instance's own override.

Reported as issue Open-Cascade-SAS#1387.
@gsdali
gsdali force-pushed the fix/341-xcafdoc-shapetool-autonaming-race branch from d0312c2 to 7673779 Compare July 23, 2026 05:42
@gsdali

gsdali commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@gkv311 Thank you for the review — and you're right, a mutex isn't the right tool here.

We'd used a mutex-guarded save/restore successfully for a couple of similar unsynchronized-global-state issues elsewhere in this codebase, so that was the pattern we defaulted to without stopping to ask whether the state should have been global in the first place. In hindsight this is a clearly better fix: theAutoNaming was only ever being overridden by three call sites that each want to suppress naming for their own document build, not change a process-wide setting — so the override belongs on XCAFDoc_ShapeTool (already one instance per document), not on a shared flag. That's the original intent the three call sites were reaching for anyway; the global was the wrong tool even before concurrency entered the picture.

Pushed an update using your suggested design: myOwnAutonaming (tri-state, -1 inherits the process-wide default) plus OwnAutoNaming()/SetOwnAutoNaming()/UnsetOwnAutoNaming(), with AddShape/MakeReference/SetSHUO reading OwnAutoNaming() instead of the flag directly. One thing worth flagging since it wasn't obvious from the one-line sketch: XCAFDoc_Editor::Expand() recurses into itself on the same document, so a bare SetOwnAutoNaming()/UnsetOwnAutoNaming() pair at entry/exit would clobber an outer call's override mid-recursion (the inner call's unconditional "reset to inherit global" would win over the outer call's still-active override). OwnAutoNamingScope does a proper save/restore of whatever override state the instance had on entry — same shape as the RAII scope this replaces, just scoped to the instance instead of a process-wide flag, so nesting composes correctly with no lock needed either way.

Re-verified with the same ThreadSanitizer stress as before (10 threads x 200 iterations) — zero theAutoNaming races, same result as the previous fix. Also added a stress scenario for the specific gap you flagged: half the threads locally override via OwnAutoNamingScope on their own document while the other half do plain unscoped AddShape() on independent documents relying on the process-wide default, concurrently — 3000 operations, zero instances of the unscoped threads observing another thread's override.

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.

XCAFDoc_ShapeTool::theAutoNaming global race: RWMesh_CafReader save/modify/restore has no locking

2 participants