Skip to content

Commit e045c20

Browse files
docs(audits): FFI boundary §§9-11 (FLINT + Zig bridges + Zig overlay) — closes #177 (#186)
## Summary Closes #177 (post-#104 panic-attack re-sweep — 6 new legitimate-FFI High findings need audit + classification). Extends `reports/audits/audit-ffi-boundary.md` with three new sections and adds 6 matching entries to `reports/audits/assail-classifications.a2ml`. ### `audits/audit-ffi-boundary.md` - **§9 — `src/rust/coprocessor/flint.rs`** (UnsafeCode × 17): FLINT C bigint library FFI (feature-gated `--features flint`). Documents the 4 unsafe-block patterns (RAII init/clear, string round-trip via `fmpz_set_str`/`get_str` + `flint_free`, `fmpz_poly` arithmetic, integer root/binomial), the `#[repr(C)]` layout invariants for `FmpzPolyStruct`, and the blocking-thread-only ownership model. - **§10 — Zig FFI bridges**: - §10.1 `src/zig/ffi/axiom_spark_bridge.zig` (Ada/SPARK → Zig shim → Rust `extern "C"` in `spark_axiom.rs`, §2) - §10.2 `src/zig_ffi/chapel_bridge.zig` (Chapel C API → Zig `@cImport` → Rust safe-call layer in `proof_search.rs`, §3) - Documents the `@cImport` intent + enum-from-int bounds check + owned-string-copy invariants. - **§11 — `ffi/zig/src/` overlay** (UnsafeCode × 3): three identical `echidna_<subsystem>_last_error()` functions in `boj.zig`, `overlay.zig`, `typell.zig` that `@ptrCast` a module-static `error_buf [N]u8` to `[*:0]const u8`. Documents the static-lifetime + nul-terminator + null-fast-path + read-only contract invariants. ### `reports/audits/assail-classifications.a2ml` Adds 6 classification entries cross-referencing §§9-11: | File | Category | Audit ref | |---|---|---| | `src/rust/coprocessor/flint.rs` | UnsafeCode | §9 | | `src/zig/ffi/axiom_spark_bridge.zig` | UnsafeFFI | §10.1 | | `src/zig_ffi/chapel_bridge.zig` | UnsafeFFI | §10.2 | | `ffi/zig/src/boj.zig` | UnsafeCode | §11 | | `ffi/zig/src/overlay.zig` | UnsafeCode | §11 | | `ffi/zig/src/typell.zig` | UnsafeCode | §11 | Brings the total classification count from 16 → 22, covering all 6 new High-tier FFI findings reported by `panic-attack` v2.5.0 on 2026-06-01. ## Out-of-scope observation (meander) The panic-attack registry loader (`load_user_classifications` in `panic-attack/src/assail/mod.rs:220`) reads from `<project>/audits/assail-classifications.a2ml` or `<project>/.panic-attack-classifications.a2ml`, but this repo keeps the registry at `reports/audits/`. This means the registry entries (both pre-existing and these new ones) do not currently flip findings to `suppressed = true` in panic-attack output — the kanren structural-suppression pass is what suppresses today. Out-of-scope for this PR; can be addressed by either a one-line symlink (`audits → reports/audits`) or moving the file. Filing separately if desired. ## Test plan - [x] `audits/audit-ffi-boundary.md` §§9-11 follow the established §1-§8 format (header + boundary description + invariants list + classification verdict). - [x] All 6 new classification entries are syntactically consistent with the existing 16 (s-expr form, `(file …)` + `(category …)` + `(audit …)` + `(rationale …)`). - [x] GPG-signed commit (key `4A03639C…`). - [ ] Auto-merge SQUASH armed post-CI. 🤖 Discovered + audited during the post-#104 panic-attack estate sweep (2026-06-01). See [hyperpolymath/panic-attack#32](hyperpolymath/panic-attack#32) for campaign tracker. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 872041c commit e045c20

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

reports/audits/assail-classifications.a2ml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,49 @@ schema_version = "1.0.0"
127127
(audit "audits/audit-pa021-axioms-2026-05-26.md §2")
128128
(rationale "Conflicts is an abstract binary predicate parameter; caller supplies concrete proof of ¬ Conflicts when sets are disjoint. INTENTIONAL parameter, not proof debt. Inline justification at line 44-53."))
129129

130+
; ─── src/rust/coprocessor/flint.rs — FLINT C bigint FFI ───────────────────
131+
; Feature-gated CAS backend (--features flint); 17 unsafe blocks across
132+
; RAII init/clear, string round-trip, and arithmetic ops on RAII-owned
133+
; fmpz / fmpz_poly objects. Documented in audit §9 (issue #177).
134+
(classification
135+
(file "src/rust/coprocessor/flint.rs")
136+
(category "UnsafeCode")
137+
(audit "audits/audit-ffi-boundary.md §9")
138+
(rationale "FLINT C bigint library FFI (feature-gated --features flint); 17 unsafe blocks across RAII init/clear, fmpz string round-trip, and fmpz_poly arithmetic; all pointers are &mut self.inner to RAII-owned objects on the blocking thread, never aliased, never shared across threads"))
139+
140+
; ─── src/zig/ffi/axiom_spark_bridge.zig — Zig shim over SPARK ─────────────
141+
(classification
142+
(file "src/zig/ffi/axiom_spark_bridge.zig")
143+
(category "UnsafeFFI")
144+
(audit "audits/audit-ffi-boundary.md §10.1")
145+
(rationale "Zig C-ABI shim re-exporting SPARK axiom-policy symbols with predictable C ABI; @cImport({}) is the only construct flagged. SPARK contracts enforce output-pointer write-before-return; Rust side already audited in §2"))
146+
147+
; ─── src/zig_ffi/chapel_bridge.zig — Zig bridge to Chapel runtime ─────────
148+
(classification
149+
(file "src/zig_ffi/chapel_bridge.zig")
150+
(category "UnsafeFFI")
151+
(audit "audits/audit-ffi-boundary.md §10.2")
152+
(rationale "Type-safe Zig bridge between Chapel parallel-search runtime and Rust core (feature-gated chapel); @cImport pulls in chapel_ffi_exports.h. Enum-from-int is bounds-checked (line 156); strings copied into Zig-owned storage before Chapel call returns; Rust side already audited in §3"))
153+
154+
; ─── ffi/zig/src/boj.zig — BoJ client last_error getter ───────────────────
155+
(classification
156+
(file "ffi/zig/src/boj.zig")
157+
(category "UnsafeCode")
158+
(audit "audits/audit-ffi-boundary.md §11")
159+
(rationale "Single @ptrCast at line 456 in echidna_boj_last_error(): casts module-static error_buf [N]u8 to [*:0]const u8 with null-fast-path guard. Buffer is static (lifetime = program), nul-terminated by all writers, read-only contract. Identical pattern to overlay.zig and typell.zig"))
160+
161+
; ─── ffi/zig/src/overlay.zig — Overlay subsystem last_error getter ───────
162+
(classification
163+
(file "ffi/zig/src/overlay.zig")
164+
(category "UnsafeCode")
165+
(audit "audits/audit-ffi-boundary.md §11")
166+
(rationale "Single @ptrCast at line 684 in echidna_overlay_last_error(): identical pattern to boj.zig — module-static error_buf [N]u8 → [*:0]const u8 with null-fast-path guard. Single-threaded request-response model"))
167+
168+
; ─── ffi/zig/src/typell.zig — TypeLL subsystem last_error getter ─────────
169+
(classification
170+
(file "ffi/zig/src/typell.zig")
171+
(category "UnsafeCode")
172+
(audit "audits/audit-ffi-boundary.md §11")
173+
(rationale "Single @ptrCast at line 333 in echidna_typell_last_error(): identical pattern to boj.zig and overlay.zig — module-static error_buf [N]u8 → [*:0]const u8 with null-fast-path guard"))
174+
130175
)

reports/audits/audit-ffi-boundary.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,174 @@ This is structural overhead from the 105-prover architecture, not FFI or
131131
memory mismanagement.
132132

133133
**Classification**: legitimate backend registry. No remediation required.
134+
135+
---
136+
137+
## §9 — `src/rust/coprocessor/flint.rs` (UnsafeCode × 17) — FLINT C bigint bindings
138+
139+
**Added**: 2026-06-01 (post-#104 panic-attack re-sweep, issue #177)
140+
141+
FLINT (Fast Library for Number Theory, LGPL-3) is the only CAS backend in
142+
ECHIDNA that links the library in-process rather than shelling out (earning
143+
`CoprocessorTrustTier::LibraryWrapped`, Tier 3). Feature-gated via
144+
`--features flint`; without it the module compiles out entirely and
145+
`CoprocessorFactory::native(CoprocessorKind::FlintMath)` returns `None`.
146+
147+
The 17 `unsafe` blocks (lines 145, 158, 168, 172, 173, 180, 199, 204, 214,
148+
233, 251, 327, 335, 349, 356, 366, 376) fall into four well-bounded patterns:
149+
150+
- **`fmpz_init` / `fmpz_clear` / `fmpz_poly_init` / `fmpz_poly_clear`**:
151+
RAII bookkeeping — each `Fmpz` / `FmpzPoly` Rust wrapper owns exactly one
152+
FLINT object; `Drop` runs `fmpz_clear` / `fmpz_poly_clear` exactly once.
153+
Pointers passed are always `&mut self.inner`, never null, never aliased
154+
(Rust's borrow checker prevents the latter).
155+
- **`fmpz_set_str` / `fmpz_get_str`**: string round-trip through CString /
156+
CStr. Inputs are validated CStrings (no interior nul); outputs are
157+
immediately copied into owned `String` and the FLINT-allocated buffer is
158+
released via `flint_free` (line 173) — no leak, no use-after-free.
159+
- **`fmpz_poly_*` arithmetic** (`gcd`, `mul`, `pseudo_rem`, `content`,
160+
`set_coeff_fmpz`, `get_coeff_fmpz`, `length`): all called on `&mut`
161+
pointers to RAII-owned `FmpzPolyStruct` instances created on the same
162+
blocking thread, used, and dropped before the thread returns.
163+
- **`fmpz_root` / `fmpz_bin_uiui`**: integer k-th root and binomial.
164+
Input bounds (`n`, `k` as `u64`) are validated by callers before the
165+
FFI call; FLINT itself returns 0/1 status to indicate exact-vs-approximate
166+
result (line 366: `exact = fmpz_root(...)`).
167+
168+
**Thread safety**: FLINT is thread-safe from version 2.4 onwards provided
169+
each thread owns its `fmpz` / `fmpz_poly` instances exclusively. All FLINT
170+
objects in this module are created on the blocking thread, used, and dropped
171+
before the thread returns — no sharing occurs (module docstring lines 38-43).
172+
173+
**Type-layout invariants**: `FmpzPolyStruct` (`#[repr(C)]`, lines 75-80) is
174+
a faithful mirror of FLINT's `fmpz_poly_struct` on 64-bit Linux (3 pointer-
175+
sized fields, 24 bytes). We never read or write the fields directly — all
176+
access goes through FLINT functions (lines 73-74 docstring).
177+
178+
**Classification**: legitimate FFI (Rust→FLINT C library via in-process
179+
linkage, feature-gated). No remediation required.
180+
181+
---
182+
183+
## §10 — Zig FFI bridges (`src/zig/ffi/axiom_spark_bridge.zig`, `src/zig_ffi/chapel_bridge.zig`) — UnsafeFFI × 2
184+
185+
**Added**: 2026-06-01 (post-#104 panic-attack re-sweep, issue #177)
186+
187+
Two Zig modules that bridge between two non-Rust backends (SPARK/Ada and
188+
Chapel) and the Rust FFI surface. Each contains exactly one `@cImport`
189+
directive — the only construct panic-attack flags as `UnsafeFFI` for Zig.
190+
191+
### §10.1 — `src/zig/ffi/axiom_spark_bridge.zig`
192+
193+
Zig C-ABI shim over the SPARK axiom-policy layer, sitting between the
194+
Ada/SPARK static library (`libechidna_spark.a`) and the Rust FFI surface
195+
defined in `src/rust/ffi/spark_axiom.rs` (already audited in §2).
196+
197+
```
198+
SPARK/Ada (axiom_policy.ads/.adb)
199+
→ C exports (axiom_c_bridge.ads/.adb: echidna_spark_enforce_policy_inner)
200+
→ this Zig shim (input validation + C-ABI re-export)
201+
→ Rust extern "C" (src/rust/ffi/spark_axiom.rs)
202+
```
203+
204+
The `@cImport({})` (line 20) is intentionally empty — the shim's job is to
205+
re-export the SPARK symbols with predictable C ABI (no name mangling,
206+
predictable layout) and provide a compile-time `MAX_USAGES = 1024` size
207+
assertion that Rust's `bindgen` cannot see across the Ada boundary
208+
(module header, lines 4-17).
209+
210+
Safety invariants:
211+
- `danger_levels: [*]const u8` + `count: usize` is the standard
212+
pointer+length C slice; validated non-empty before crossing the Rust
213+
boundary in `spark_axiom.rs` (see §2).
214+
- Output `policy_out` / `status_out` are `*i32` to caller-allocated
215+
storage; the Ada side guarantees they are always written before the
216+
call returns (SPARK contract).
217+
218+
### §10.2 — `src/zig_ffi/chapel_bridge.zig`
219+
220+
Type-safe Zig bridge between Chapel's parallel-search runtime and the
221+
Rust FFI surface, gated by the `chapel` Cargo feature (see §3).
222+
223+
```
224+
Chapel (C API: chapel_ffi_exports.h)
225+
→ Zig (@cImport, this module)
226+
→ Rust (safe calls via src/rust/proof_search.rs)
227+
```
228+
229+
The `@cImport({ @cInclude("chapel_ffi_exports.h"); })` (lines 18-20) pulls
230+
in the Chapel-exported C declarations for 30 prover backends. The Zig
231+
wrappers (`ProverKind`, `ProverCategory`, `ProofResult`) translate
232+
Chapel's C types into Zig-safe enums and structs, owning all string copies
233+
(`fromChapel` at line 143 takes `std.mem.Allocator` and copies before
234+
returning).
235+
236+
Safety invariants:
237+
- Enum-from-int boundaries are bounds-checked (line 156: `c_result.prover_id
238+
>= 0 and c_result.prover_id < @as(c_int, ProverKind.count)` before
239+
`@enumFromInt`).
240+
- All string references returned from Chapel are copied into Zig-owned
241+
storage before the Chapel call returns (per module header line 7).
242+
- Chapel runtime guarantees null-terminated string lifetime until next
243+
Chapel call (see §3 for the corresponding Rust-side contract).
244+
245+
**Classification (both files)**: legitimate FFI (Zig bridge layer between
246+
Ada/SPARK & Chapel backends and Rust core; both feature-gated). No
247+
remediation required.
248+
249+
---
250+
251+
## §11 — `ffi/zig/src/*` overlay layer (UnsafeCode × 3) — pointer casts on static error buffers
252+
253+
**Added**: 2026-06-01 (post-#104 panic-attack re-sweep, issue #177)
254+
255+
Three Zig modules in the `ffi/zig/src/` overlay (the Idris2-ABI → Zig-FFI →
256+
C-ABI surface for BoJ cartridges, the overlay subsystem, and the TypeLL
257+
type-language) each contain exactly one `@ptrCast` flagged by panic-attack
258+
as `UnsafeCode`.
259+
260+
| File | Line | Function |
261+
|------------------------------|------|---------------------------------------|
262+
| `ffi/zig/src/boj.zig` | 456 | `echidna_boj_last_error` |
263+
| `ffi/zig/src/overlay.zig` | 684 | `echidna_overlay_last_error` |
264+
| `ffi/zig/src/typell.zig` | 333 | `echidna_typell_last_error` |
265+
266+
All three are the **identical pattern**: a `last_error` getter that returns
267+
a C-ABI null-terminated string pointer to a module-private static error
268+
buffer:
269+
270+
```zig
271+
pub export fn echidna_<subsystem>_last_error() ?[*:0]const u8 {
272+
if (error_len == 0) return null;
273+
return @ptrCast(&error_buf);
274+
}
275+
```
276+
277+
Where `error_buf: [ERROR_BUF_SIZE]u8` (typically 512 bytes) is a
278+
module-private static array and `error_len: usize` is its current
279+
fill-length.
280+
281+
Safety invariants:
282+
- `error_buf` is module-static — its address is fixed at program load and
283+
never freed; the returned pointer is always valid for the program
284+
lifetime.
285+
- The cast is from `*[N]u8` (array reference) to `[*:0]const u8`
286+
(sentinel-terminated many-item pointer); the buffer is guaranteed to
287+
contain a trailing nul because all writers (`setError` helpers in each
288+
module) explicitly write a nul terminator before incrementing
289+
`error_len`.
290+
- Return type is `?[*:0]const u8` (optional); a null-fast-path
291+
(`if (error_len == 0) return null;`) guards against returning a pointer
292+
to an empty buffer, so consumers can distinguish "no error" from
293+
"empty-string error".
294+
- Read-only on the Rust/Idris2 side — the pointer is `const`; callers
295+
must not write through it.
296+
297+
**Concurrency note**: the static error buffer is not thread-safe; each
298+
subsystem's contract is that errors are read on the same thread that
299+
triggered them (consistent with the "BoJ client" / "overlay" / "TypeLL"
300+
single-threaded request-response model). Multi-threaded callers must
301+
serialise error retrieval externally.
302+
303+
**Classification**: legitimate FFI (idiomatic C-style `last_error` getters
304+
for three FFI subsystems sharing a common pattern). No remediation required.

0 commit comments

Comments
 (0)