Closes the dimensions the prior audits did not cover: - numerical-stability: tolerance hierarchy (FD-step vs Newton tol), discontinuous clamps, cancellation in cotangent/area, magic constants. - input-validation: malformed JSON/XML deserialization, NaN/Inf vertex coords on load, schema-drift handling. - thread-safety: no mutable static state (good); undocumented same-mesh concurrency contract; Eigen threading. - dependency-license: existing THIRD-PARTY-LICENSES matrix is sound but predicated on the MIT premise that G0 undermines; test meshes have no provenance/license. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
6.1 KiB
Markdown
140 lines
6.1 KiB
Markdown
# 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.
|