Commit Graph

5 Commits

Author SHA1 Message Date
Dion Moult 19bff92a47 Fix data races when parsing IFC files on concurrent threads
Loading a federated project (.ifcfed) with several models segfaults
non-deterministically on a fresh start. The viewer's SceneLoader spawns
one background std::thread per model in startDataSourceLoad() to
construct an ifcopenshell::file; with cached sidecars all models reach
that point near-simultaneously, so multiple threads parse different IFC
files at once. Parsing touches the process-wide schema singleton, which
was not thread-safe in two places.

Race 1 — concurrent schema population
-------------------------------------
schema_registry::get() lazily runs the schema's get_() function (e.g.
Ifc4::get_schema() -> IFC4_populate_schema()) and mutates entries_ with
no lock. Two threads calling schema_by_name("IFC4") at once both run
IFC4_populate_schema() concurrently, which fills global arrays
(IFC4_types[], strings[]). One thread reads a slot the other is still
writing.

Core-dump evidence (gdb thread apply all bt):

  Thread 1  SIGSEGV in IFC4_populate_schema   Ifc4-schema.cpp:1989
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- schema_by_name("IFC4")
            <- ifcopenshell::file::file (NWCH-PIR-SS...ifc)
            <- SceneLoader::startDataSourceLoad lambda  SceneLoader.cpp:315

  Thread 3  also in IFC4_populate_schema (entity ctor for
            "IfcMaterialProfileSetUsageTapering")
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

Two threads inside IFC4_populate_schema() at the same time is the race.

Fix: guard schema_registry's bind()/get()/names()/clear() with a
recursive_mutex (recursive because get() re-enters bind() via
load_schema_plugin(), and a freshly populated schema registers itself
through register_schema()). get() is serialized, so only the first
thread populates the schema; the rest block briefly and then observe
the finished result. Returned schema pointers are stable for the
process lifetime, so holding the lock only across get() is sufficient.

Race 2 — lazy all_attributes_ cache filled during parsing
---------------------------------------------------------
entity::all_attributes() lazily fills a `mutable` optional cache on the
shared schema entity the first time it is accessed — and that first
access happens during parsing (parse_context::construct), not during
schema population. With race 1 fixed, two parser threads still raced
here: both saw the cache empty, both did all_attributes_.emplace() and
std::copy() into it, corrupting the vector.

Core-dump evidence after the race-1 fix:

  Thread 1  SIGSEGV in attribute::type_of_attribute (this=0xe130...55c)
            <- std::transform(first=0x4, last=0xb0d1...)   <-- garbage
               iterators into a corrupt std::vector
            <- parse_context::construct over
               decl->as_entity()->all_attributes()        file.cpp:249
            <- instance_streamer::read_instance
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

The begin pointer 0x4 is a half-written vector being read mid-resize by
another thread.

Fix: force every entity's all_attributes_ cache in the
schema_definition constructor, while construction is still
single-threaded. The schema is then genuinely immutable after
construction, so concurrent parsing needs no hot-path lock.

Both crashes reproduce reliably on a fresh start at native speed but
vanish under gdb (which serializes thread scheduling) — the classic
signature of a data race. With both fixes the federated load completes
cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Thomas Krijnen e893552f24 Fixes to plug-in loading in and outside of pyodide 2026-05-07 14:43:26 +02:00
Thomas Krijnen 98ff457fd6 Continue work on plug-in and tests 2026-05-06 21:17:57 +02:00
Thomas Krijnen aa10784154 First slice plug-in refactor 2026-04-14 13:51:09 +02:00
Thomas Krijnen a07f56db6f Restructure and rename 2026-03-31 15:32:36 +02:00