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>
198 lines
8.6 KiB
Markdown
198 lines
8.6 KiB
Markdown
# Input-Validation & Malformed-Input Audit — ConformalLabpp
|
|
|
|
**Date:** 2026-05-31
|
|
**Auditor:** External reviewer (Claude Opus 4.8)
|
|
**Scope:** Robustness of the library's input boundaries — mesh I/O (OFF/OBJ/PLY) and
|
|
result (de)serialization (JSON/XML) — against malformed, adversarial, or
|
|
out-of-domain input.
|
|
**Focus:** What happens with *bad* input. Distinct from the error-handling audit,
|
|
which covered *internal* error propagation and the deliberate `throw` paths.
|
|
|
|
Status legend: 🔴 Critical · 🟡 Important · 🔵 Polish
|
|
|
|
> **Threat model:** this is a scientific library, not a network service, so the bar
|
|
> is "fail cleanly and diagnosably", not "resist attackers". But meshes and result
|
|
> files routinely come from other tools, other people, and older versions of this
|
|
> code — so malformed input is a *when*, not an *if*.
|
|
|
|
---
|
|
|
|
## Summary table
|
|
|
|
| ID | Sev | Title | Location |
|
|
|----|-----|-------|----------|
|
|
| V1 | 🟡 | JSON deserialization has no error handling — nlohmann exceptions leak the abstraction, not the library's `runtime_error` | `serialization.hpp:96-110` |
|
|
| V2 | 🟡 | XML parser uses unchecked `std::stoi`/`std::stod` on attributes → uncaught `std::invalid_argument` on garbage | `serialization.hpp:258-261` |
|
|
| V3 | 🟡 | No validation of vertex coordinate values on load — NaN/Inf coordinates flow silently into the solver | `mesh_io.hpp:56-66` |
|
|
| V4 | 🟡 | JSON loader accesses nested keys (`j["solver"]["converged"]`) without `contains` checks → throws `type_error`/creates nulls on schema drift | `serialization.hpp:104-108` |
|
|
| V5 | 🔵 | Hand-rolled XML parser assumes one element per line and no escaping — silently mis-reads valid-but-reformatted XML | `serialization.hpp:248-270` |
|
|
| V6 | 🔵 | No upper bound / sanity check on DOF-vector length vs mesh size when loading | `serialization.hpp` |
|
|
|
|
---
|
|
|
|
## V1 — 🟡 JSON deserialization leaks nlohmann exceptions
|
|
|
|
### Evidence
|
|
`serialization.hpp:96-108`:
|
|
```cpp
|
|
std::ifstream ifs(path);
|
|
if (!ifs) throw std::runtime_error("Cannot open: " + path);
|
|
json j; ifs >> j; // ← malformed JSON → nlohmann::parse_error
|
|
...
|
|
res->converged = j["solver"]["converged"].get<bool>(); // ← wrong type → type_error
|
|
```
|
|
|
|
### Problem
|
|
The "file missing" case is handled with the library's own `std::runtime_error`
|
|
(consistent with `mesh_io.hpp`). But the moment the file *exists and is malformed*,
|
|
`ifs >> j` throws `nlohmann::json::parse_error` and the typed accessors throw
|
|
`nlohmann::json::type_error` — neither is the library's documented error type, and
|
|
neither carries the file path. A caller writing `catch (const std::runtime_error&)`
|
|
(the pattern the examples use) will **miss** these. (`nlohmann` exceptions derive from
|
|
`std::exception`, not `std::runtime_error`.)
|
|
|
|
### Fix
|
|
Wrap the parse + extraction in `try { ... } catch (const json::exception& e)` and
|
|
rethrow as `std::runtime_error("conformallab: malformed JSON in " + path + ": " + e.what())`,
|
|
matching the `mesh_io.hpp` boundary convention.
|
|
|
|
### Acceptance criteria
|
|
- Loading a truncated/garbage JSON throws `std::runtime_error` with the path.
|
|
- A negative test (`EXPECT_THROW(..., std::runtime_error)`) covers it.
|
|
|
|
---
|
|
|
|
## V2 — 🟡 XML parser: unchecked stoi/stod on attributes
|
|
|
|
### Evidence
|
|
`serialization.hpp:258-261`:
|
|
```cpp
|
|
res->iterations = std::stoi(detail_xml::xml_get_attr(line, "iterations"));
|
|
res->grad_inf_norm = std::stod(detail_xml::xml_get_attr(line, "grad_inf_norm"));
|
|
```
|
|
|
|
### Problem
|
|
If the attribute is missing or non-numeric (`xml_get_attr` returns `""` or garbage),
|
|
`std::stoi("")` throws `std::invalid_argument` and out-of-range values throw
|
|
`std::out_of_range` — both uncaught, both not the library's `runtime_error`, neither
|
|
carrying the path. Same abstraction leak as V1, different exception family.
|
|
|
|
### Fix
|
|
Parse defensively: check the attribute is non-empty and numeric, or wrap in
|
|
try/catch and rethrow as `std::runtime_error` with context (path + attribute name).
|
|
|
|
### Acceptance criteria
|
|
- An XML file with a non-numeric `iterations` attribute throws `std::runtime_error`,
|
|
not `std::invalid_argument`.
|
|
|
|
---
|
|
|
|
## V3 — 🟡 No validation of vertex coordinates on load
|
|
|
|
### Evidence
|
|
`mesh_io.hpp:56-66` — `load_mesh` checks read success and `is_triangle_mesh`, but
|
|
does **not** inspect the vertex coordinate values.
|
|
|
|
### Problem
|
|
A mesh file containing `nan`/`inf` coordinates (common from a crashed upstream tool,
|
|
or `1e308`-style overflow) reads in cleanly. The NaN then propagates: edge lengths
|
|
→ `lambda0` → gradient → Hessian → the Newton solver, where it silently poisons the
|
|
result (`converged` may even read `true` if `NaN < tol` evaluates falsely in a way
|
|
that exits the loop). This is the worst kind of failure: silent, late, and
|
|
hard to trace back to the input.
|
|
|
|
### Fix
|
|
After `read_mesh`, scan vertex coordinates for finiteness
|
|
(`std::isfinite`) and throw `std::runtime_error("conformallab: non-finite vertex
|
|
coordinate in " + filename)` on the first offender. O(V), negligible cost at the I/O
|
|
boundary — the same philosophy as the existing `is_triangle_mesh` guard.
|
|
|
|
### Acceptance criteria
|
|
- Loading a mesh with a NaN/Inf coordinate throws `std::runtime_error`.
|
|
- Covered by a negative test.
|
|
|
|
---
|
|
|
|
## V4 — 🟡 JSON loader assumes schema without checking
|
|
|
|
### Evidence
|
|
`serialization.hpp:104-108` reads `j["solver"]["converged"]`, `["iterations"]`,
|
|
`["grad_inf_norm"]` and `j.at("dof_vector")` — only `geometry` is guarded with
|
|
`j.contains(...)` (`:99`).
|
|
|
|
### Problem
|
|
`operator[]` on a missing key in nlohmann **inserts a null** (for non-const json) or
|
|
throws; `.get<bool>()` on null throws `type_error`. So a result file written by a
|
|
*future or older* schema version fails with an opaque nlohmann error rather than a
|
|
clear "missing field X" message. (`dof_vector` correctly uses `.at()`, which throws a
|
|
clear `out_of_range` — but still not the library's type.)
|
|
|
|
### Fix
|
|
Validate the expected keys up front (or use `.at()` uniformly + the V1 try/catch),
|
|
producing a single `std::runtime_error("conformallab: result JSON missing field
|
|
'solver.converged'")`-style message.
|
|
|
|
### Acceptance criteria
|
|
- A JSON missing `solver.iterations` yields a `runtime_error` naming the field.
|
|
|
|
---
|
|
|
|
## V5 — 🔵 Hand-rolled XML parser is format-fragile
|
|
|
|
### Evidence
|
|
`serialization.hpp:248-270` parses line-by-line with `line.find("<Solver")` etc. and
|
|
assumes each element (and the `<DOFVector>…</DOFVector>` text) fits patterns on one
|
|
line.
|
|
|
|
### Problem
|
|
This is not an XML parser — it is a line-pattern matcher. A semantically identical
|
|
file that pretty-prints elements across multiple lines, or adds an XML declaration /
|
|
namespace / attribute reordering, will be **silently mis-read** (fields default to
|
|
zero/empty) rather than rejected. Round-trips written by this same code work; nothing
|
|
else is guaranteed.
|
|
|
|
### Fix
|
|
Either (a) document the XML format as a strict internal-only subset (and reject input
|
|
that doesn't match, rather than mis-reading it), or (b) if interop matters, use a real
|
|
XML parser. Given JSON is the primary format, (a) is the pragmatic choice — but the
|
|
"silently mis-read" behavior must become "explicitly reject".
|
|
|
|
### Acceptance criteria
|
|
- Reformatted-but-valid XML is either parsed correctly or rejected with an error —
|
|
never silently mis-read into zeros.
|
|
|
|
---
|
|
|
|
## V6 — 🔵 No DOF-vector vs mesh sanity check
|
|
|
|
### Evidence
|
|
`load_result_json` returns `x = j.at("dof_vector")` with no check that `x.size()`
|
|
matches the DOF count of the mesh it will be applied to.
|
|
|
|
### Problem
|
|
A result file from a *different* mesh loads happily; the size mismatch only surfaces
|
|
later (out-of-bounds or wrong-answer) when `x` is indexed against the new mesh.
|
|
|
|
### Fix
|
|
Where the loaded `x` is paired with a mesh, assert/throw on a size mismatch with a
|
|
clear message. (May belong at the call site rather than the loader.)
|
|
|
|
---
|
|
|
|
## What is already good
|
|
|
|
- The **file-missing** path is handled correctly and consistently
|
|
(`runtime_error` + path) in both `mesh_io.hpp` and `serialization.hpp`.
|
|
- `load_mesh` already enforces the triangle-mesh precondition at the boundary — the
|
|
right place and the right philosophy; V3 just extends it to coordinate finiteness.
|
|
- `dof_vector` uses `.at()` (throwing) rather than `operator[]` — partial good
|
|
practice that V4 asks to make uniform.
|
|
- JSON (nlohmann) is a robust, well-tested parser — the gap is only the missing
|
|
try/catch *around* it (V1), not the parser itself.
|
|
|
|
## Suggested order
|
|
1. **V3** (NaN/Inf coordinate guard) — highest impact, prevents silent solver poisoning.
|
|
2. **V1 + V4** (JSON error handling + schema checks) — do together.
|
|
3. **V2** (XML stoi/stod) — same pattern as V1.
|
|
4. **V5 + V6** (XML strictness, size check) — polish.
|