# Thread-Safety & Reentrancy Audit โ€” ConformalLabpp **Date:** 2026-05-31 **Auditor:** External reviewer (Claude Opus 4.8) **Scope:** Concurrency contract of the header-only library โ€” reentrancy, hidden mutable global state, and safety of parallel solves. **Focus:** Can two threads use this library at the same time, and is that contract documented? Currently undocumented everywhere. Status legend: ๐Ÿ”ด Critical ยท ๐ŸŸก Important ยท ๐Ÿ”ต Polish > **Good news up front:** a scan for mutable function-local `static` variables (the > classic hidden-shared-state hazard) found **none**. All `static` in the headers are > stateless factory/accessor functions (`MobiusMap::identity`, > `Default_conformal_map_traits::theta_map`, etc.). So the library is *close* to > trivially reentrant โ€” the findings below are about the mesh-mutation contract and > the missing documentation, not about data races in hidden globals. --- ## Summary table | ID | Sev | Title | Location | |----|-----|-------|----------| | T1 | ๐ŸŸก | No documented threading contract anywhere โ€” users cannot know what is safe | all public headers | | T2 | ๐ŸŸก | The solve pipeline mutates the mesh via property maps โ†’ concurrent solves on the *same* mesh are unsafe and silently so | `*_functional.hpp`, `newton_solver.hpp` | | T3 | ๐Ÿ”ต | Property-map *creation* (`add_property_map`) on a shared mesh from two threads is a data race on CGAL's internal map registry | `setup_*_maps`, traits accessors | | T4 | ๐Ÿ”ต | Eigen threading not pinned โ€” relies on Eigen's defaults; undocumented | `newton_solver.hpp` | --- ## T1 โ€” ๐ŸŸก No documented threading contract ### Evidence No header, no `doc/` file, and no README section states whether the API is thread-safe, reentrant, or single-threaded-only. `briefing.md` positions the library as potential "infrastructure for your work" โ€” which invites multi-threaded use it never characterizes. ### Problem Without a stated contract, a user must read the implementation to discover (T2) that distinct meshes are safe but a shared mesh is not. The safe pattern is real and simple โ€” it just needs to be written down. ### Fix Add a short "Thread safety" section to the main API doc (and a sentence in the key headers) stating the contract, e.g.: > *The library performs no synchronization and uses no shared mutable global state. > Operations on **distinct** `ConformalMesh` objects are independent and may run > concurrently. Operations on a **single** mesh (including any `setup_*_maps`, > `assign_*`, gradient, Hessian, or `newton_*` call) mutate that mesh's property maps > and must not run concurrently on the same mesh.* ### Acceptance criteria - A "Thread safety" subsection exists in the public API documentation and matches the actual behavior described in T2/T3. --- ## T2 โ€” ๐ŸŸก Solve pipeline mutates the mesh; same-mesh concurrency is unsafe ### Evidence - `setup_*_maps(mesh)` adds property maps to `mesh`. - `assign_*_dof_indices(mesh, m)` writes `m.v_idx` / `m.e_idx` into the mesh. - `compute_*_lambda0_from_mesh(mesh, m)` writes `m.lambda0`. - `newton_*` reads those maps and (via the gradient/Hessian) reads the mesh repeatedly; the functions take `ConformalMesh& mesh` (non-const). ### Problem Two threads calling `newton_euclidean(mesh, โ€ฆ)` on the **same** `mesh` race on the shared property maps โ€” a silent data race, not a clean error. Because the signature is `ConformalMesh&` (mutable) this is technically "expected", but nothing warns the user, and the natural-looking "solve the same input mesh under two geometries in parallel" is exactly the unsafe case. ### Fix - Document the contract (T1). - Optionally, offer a value-semantics convenience overload that takes the mesh by `const&` and operates on an internal copy, for users who want fire-and-forget parallel solves. (Larger change; document first.) ### Acceptance criteria - The same-mesh hazard is documented; if a const/copying overload is added, a test runs two concurrent solves on copies without a race (clean under ThreadSanitizer). --- ## T3 โ€” ๐Ÿ”ต Concurrent property-map creation races CGAL's registry ### Evidence `setup_*_maps` and the traits accessors call `mesh.add_property_map<โ€ฆ>(name, default)`. ### Problem CGAL's `Surface_mesh::add_property_map` mutates the mesh's internal property container. Two threads creating maps on the same mesh concurrently is undefined behavior at the CGAL level โ€” a sub-case of T2 but worth calling out because the traits accessors (`Conformal_map_traits.h:158-179`) create-on-demand, so even a "read-only-looking" first access mutates. ### Fix Covered by the T1 contract ("any `setup_*` โ€ฆ on the same mesh must not run concurrently"). No code change beyond documentation unless the const overload (T2) is pursued. --- ## T4 โ€” ๐Ÿ”ต Eigen threading is unpinned and undocumented ### Evidence `newton_solver.hpp` uses `SimplicialLDLT` / `SparseQR` without calling `Eigen::setNbThreads(โ€ฆ)` or documenting OpenMP behavior. ### Problem Eigen may use OpenMP threads internally depending on build flags. For a library that might itself be called from parallel user code, the nesting behavior (and whether `EIGEN_DONT_PARALLELIZE` is wanted) is unspecified. Distinct Eigen objects are thread-safe, so this is low risk โ€” but it should be a documented choice. ### Fix State the intended Eigen threading mode in the build/threading docs; consider pinning it explicitly (`EIGEN_DONT_PARALLELIZE` for predictability, or document that callers control it). --- ## What is already good - **No mutable static locals** anywhere in the headers โ€” the single most important reentrancy property holds. - All `static` members are pure functions (factories/accessors) โ€” no shared state. - The header-only, value-oriented design (results returned by value as `NewtonResult` / `Conformal_map_result`) means outputs are not shared. - Distinct-mesh concurrency genuinely *is* safe today โ€” the contract just needs documenting (T1), not implementing. ## Suggested order 1. **T1** (write the contract) โ€” trivial, unblocks safe parallel use immediately. 2. **T2/T3** (covered by the contract; optional const overload later). 3. **T4** (pin/document Eigen threading) โ€” polish.