Skip to content

Commit 1d7fac8

Browse files
SNIFs 2: sharpen SEC-1 (deniability + taxonomy), gate the buffer ABI, behaviour gate + AFFIRMATION + Bustfile near-hit (#39)
## Summary SNIFs 2 — sharpened verification, a ground-truthed honesty snapshot, and a recorded near-hit. Signed commits (`id_ed25519_signing`, all `signed=G`): - **a82bb31** — the work - **e60a94c** — `AFFIRMATION.adoc` (anchored to `a82bb31`; **parent == anchor**) - **32bbe33** — Bustfile near-hit (Zig 0.15.2) + scaffold-banner note ### SEC-1 — operational crash-isolation (Agda `--safe --without-K`) - **F1**: confidentiality wired into the operational run (`run-deniable` / `fault-via-observe` + a two-distinct-secret `SecretWitness`). - **F2**: real 6-origin error taxonomy (`TrapOrigin` guestFault/hostBudget/preExec + a `call` front-end + `PreExecWitness`). - **F5**: non-trivial-`Alive` recovery witness (`PartialAlive`). - All **mutation-confirmed load-bearing** by a 4-skeptic adversarial re-audit (every targeted mutation rejected; `--safe` enforced; `run`/`drive` totality real). ### ABI-7 + CI-1 + GAP-1b - `BufferAbi.idr` models all 7 `buffer_abi` exports (multi-value/void-faithful) → **15/20** Zig export sites gated; guest-aware `abi_conformance.py`. - Conformance now runs as a CI job in `proofs.yml`. - `snif_metamorphic_test.exs` — dep-free metamorphic relations over the scalar kernels; surfaced + corrected the `checked_add` wrapping-vs-name misnomer. ### Honesty + aetiology - `AFFIRMATION.adoc`: ground-truthed snapshot; **Safer-not-Safe** (SEC-1-TCB trusted), ABI-7 15/20, GAP-1b scalar-only all stated. - `docs/templates/contractiles/bust/Bustfile.a2ml`: the Zig-0.15.2 ffi-template breakage as a **near-hit** (recognition, not a fix) — by-design type discipline; inert here (dead scaffold), genuinely *bust* in typed-wasm. ## Verification (green at `a82bb31`) - `just proof-check-all` → exit 0 (10 gated artifacts) - `just abi-conformance` → 15 specs / 2 guests - `mix test` → 30/30 (OTP 25) ## Base-branch note (flagged, not swept in) This branch sits on the owner's pre-existing local commit **913d141** ("restore third-party vendored deps to pristine upstream licences") — the branch HEAD when this work began, not yet on `main`. It is a **licence** commit and the owner's own work; included here **with explicit owner permission for this PR**, called out openly. Rebasing it out was avoided deliberately because it would rewrite `a82bb31` and break the AFFIRMATION's anchor. ## Owner follow-ups (not in this PR) - Make `abi-conformance` a **required** status check (branch-protection, owner-only). - Zenodo DOI still carries the old "Safe" title (next deposit). - Decide whether to **rename** the `checked_add` export (comment corrected; name kept). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 644456b + 47423eb commit 1d7fac8

86 files changed

Lines changed: 5463 additions & 333 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/proofs.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Proof gate for SNIFS: machine-checks the formal verification artifacts.
5+
# If this gate is red, the repo's "proven" claim is void. It replaces the
6+
# previously decorative `just proof-check-*` targets, which silently passed when
7+
# the prover was absent (SKIP = exit 0) and used a broken idris2 invocation that
8+
# never resolved the ABI.* module graph — so the proofs were never actually checked.
9+
#
10+
# Toolchain is provided via Nix (nixpkgs#idris2, nixpkgs#lean4) — estate-standard,
11+
# reproducible, and avoids unpinned setup actions. NOTE: this consumes CI minutes;
12+
# if/when the bag-of-actions migration lands, this gate should move onto owned compute.
13+
# To make it BLOCKING, add the job names "Formal proofs — Idris2 + Lean4" AND
14+
# "ABI conformance — interface drift gate" to branch-protection required status
15+
# checks (owner-only). The ABI job (added 2026-06-16, SNIFs 2) builds the wasm
16+
# guests and fails if their real export signatures drift from the verified Idris2
17+
# ABI model (Foreign.idr + BufferAbi.idr) — closing the gap-1 interface gate in CI
18+
# instead of only on a local `just abi-conformance`.
19+
20+
name: Proof Gate
21+
on:
22+
push:
23+
branches: [main, master, develop]
24+
paths:
25+
- 'verification/**'
26+
- 'zig/**'
27+
- 'Justfile'
28+
- '.github/workflows/proofs.yml'
29+
pull_request:
30+
branches: [main, master]
31+
paths:
32+
- 'verification/**'
33+
- 'zig/**'
34+
- 'Justfile'
35+
- '.github/workflows/proofs.yml'
36+
workflow_dispatch:
37+
permissions: read-all
38+
concurrency:
39+
group: proofs-${{ github.ref }}
40+
cancel-in-progress: true
41+
jobs:
42+
proofs:
43+
name: Formal proofs — Idris2 + Lean4
44+
runs-on: ubuntu-latest
45+
timeout-minutes: 20
46+
steps:
47+
- name: Checkout
48+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
49+
50+
- name: Install Nix (Determinate installer)
51+
run: |
52+
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix \
53+
| sh -s -- install --no-confirm
54+
55+
- name: Check Idris2 + Lean4 proofs (fail-on-skip, real invocation)
56+
run: |
57+
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
58+
nix shell nixpkgs#idris2 nixpkgs#lean4 nixpkgs#agda nixpkgs#just \
59+
--command bash -c 'just proof-check-all'
60+
61+
abi-conformance:
62+
name: ABI conformance — interface drift gate
63+
runs-on: ubuntu-latest
64+
timeout-minutes: 20
65+
steps:
66+
- name: Checkout
67+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
68+
69+
- name: Install Nix (Determinate installer)
70+
run: |
71+
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix \
72+
| sh -s -- install --no-confirm
73+
74+
- name: Build wasm guests + check ABI signatures vs the verified Idris2 model
75+
run: |
76+
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
77+
# Zig 0.15+ is required (matches the repo toolchain + the safe_nif/buffer_abi build
78+
# flags). The recipe builds both guests, then the multi-guest conformance tool fails
79+
# on any export-signature drift from Foreign.idr / BufferAbi.idr.
80+
nix shell nixpkgs#zig nixpkgs#wasm-tools nixpkgs#python3 nixpkgs#just \
81+
--command bash -c 'just abi-conformance'
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Rust SNIF guest: build + verifier-on-by-default gate (ADR-0005).
5+
# Fail-closed. The source verifier (Kani) runs as a standard build step.
6+
name: Rust SNIF guest verify
7+
on:
8+
push:
9+
branches: [main, master]
10+
paths: ["rust/**", ".github/workflows/rust-guest-verify.yml"]
11+
pull_request:
12+
paths: ["rust/**", ".github/workflows/rust-guest-verify.yml"]
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
verify:
19+
runs-on: ubuntu-latest
20+
defaults:
21+
run:
22+
working-directory: rust
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Toolchain + wasm32 target
27+
run: |
28+
rustup toolchain install 1.95.0 --profile minimal
29+
rustup default 1.95.0
30+
rustup component add clippy
31+
rustup target add wasm32-unknown-unknown
32+
33+
# ── Static gates (run for free) ────────────────────────────────────
34+
# #![forbid(unsafe_code)] on snif-logic and #![deny(unsafe_code)] on the
35+
# others are enforced by these compiles.
36+
- name: Build guest (ReleaseSafe invariant from [profile.release])
37+
run: cargo build --release --target wasm32-unknown-unknown
38+
39+
- name: wasm validate + assert ZERO imports (self-contained sandbox)
40+
run: |
41+
W=target/wasm32-unknown-unknown/release/demo_guest.wasm
42+
cargo install wasm-tools --locked || true
43+
wasm-tools validate "$W"
44+
if wasm-tools print "$W" | grep -q '(import'; then
45+
echo "::error::guest has imports — not self-contained"; exit 1
46+
fi
47+
48+
- name: Clippy (pedantic, warnings = errors)
49+
run: cargo clippy --all-targets -- -D warnings -W clippy::pedantic
50+
51+
- name: Supply-chain (bans rustler stack)
52+
run: |
53+
cargo install cargo-deny --locked || true
54+
cargo deny check
55+
56+
# ── The source verifier: verifier-on-by-default ────────────────────
57+
- name: Kani (proves snif-logic harnesses)
58+
run: |
59+
cargo install --locked kani-verifier || true
60+
cargo kani setup || true
61+
cargo kani -p snif-logic

.machine_readable/META.a2ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
(meta
55
(project "snif")
6-
(full-name "SNIFs: Safe Native Implemented Functions for the BEAM via WebAssembly Sandboxing")
6+
(full-name "SNIFs: Safer Native Implemented Functions for the BEAM via WebAssembly Sandboxing")
77
(license "MPL-2.0")
88
(author "Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>")
99
(architecture-decisions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Bustfile — design-aetiology / "bust" contract for snifs
3+
# Author: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
#
5+
# Paired runner: bust.ncl (NOT YET PRESENT — paired .ncl runners are an
6+
# estate-wide gap; cf the Adjustfile contractile-sync
7+
# check. Do not assume a runner exists.)
8+
# Verb: bust
9+
#
10+
# Semantics: BUST is about *broken* — the slang "damn, this thing is bust".
11+
# It records WHY things break (or nearly broke), cause-first, as
12+
# design aetiology — causal help/error/removal, NOT a bandaid that
13+
# suppresses the symptom. (Sibling: DUST = exnovation, the clean
14+
# reduction-to-dust / deliberate removal of a thing.)
15+
# Run with: bust check (list failure modes + recovery status)
16+
# Fix with: bust fix (apply the causal recovery where deterministic; advisory otherwise)
17+
18+
@abstract:
19+
The snifs Bustfile in two disjoint halves.
20+
21+
## Failure Modes — OPEN, actionable defect contracts: a class that CAN break,
22+
each pairing a detection probe with a causal recovery. This is the half a
23+
`bust` runner / k9 gate would drill (declared -> drilled -> verified | failing).
24+
25+
## Near-Hit Cases — CLOSED, non-actionable recognition records: something that
26+
COULD have bitten but DID NOT, written down with its aetiology so future work
27+
recognises the same shape rather than re-stumbling into it. A near-hit is NOT a
28+
defect, has no recovery to drill, and must never be auto-promoted into a Failure
29+
Mode (that would invent an open issue from a non-issue). It only graduates by an
30+
explicit human decision.
31+
@end
32+
33+
## Failure Modes
34+
35+
### releasefast-guest-silent-wrong-answers
36+
- description: A SNIF guest built with `-Doptimize=ReleaseFast` (or `ReleaseSmall`) strips Zig's runtime safety traps (overflow, bounds, unreachable). At the wasm32-freestanding guest boundary the same fault that should TRAP becomes undefined behaviour — the guest returns a plausible-but-wrong value across the buffer ABI and the BEAM host accepts a silently corrupt answer. There is no crash to observe.
37+
- cause: optimisation-for-speed removes the only mechanism (safety traps) that surfaces ABI/arithmetic violations at the cleave surface; ReleaseSafe keeps the trap and converts the same bug into an *observable* guest trap the host catches as `{:error, _}`.
38+
- probe: ! grep -qE 'optimize *= *\.(ReleaseFast|ReleaseSmall)' zig/src/*.zig build.zig 2>/dev/null # shipped guest must be ReleaseSafe
39+
- probe: just abi-conformance # + a differential corpus: ReleaseSafe vs ReleaseFast must AGREE on edge inputs — divergence == this mode firing
40+
- recovery: build/ship the guest `-OReleaseSafe` (traps preserved); CI-gate so a ReleaseFast artifact cannot ship; on a field wrong-answer, rebuild ReleaseSafe and replay the differential corpus to localise the trapping input.
41+
- severity: critical
42+
- status: declared
43+
44+
### wasmex-nif-guest-load-failure
45+
- description: The host-side wasmex NIF, or the `.wasm` guest it loads, fails to instantiate — a missing/renamed export, ABI/version skew between the Idris2-verified model and the real Zig exports, wrong wasm target, or a NIF that won't link into the BEAM. The Elixir call surfaces `{:error, _}` (or a NIF load crash) instead of a working SNIF.
46+
- cause: the guest export set drifted from the verified ABI (`verification/proofs/idris2/ABI/*`), or the guest was built for the wrong target / missing an export, so wasmex cannot bind the expected interface at instantiation — boundary-erosion / ABI-drift, exactly the class `abi_conformance.py` guards.
47+
- probe: just abi-conformance # signature drift gate (safe_nif + buffer_abi vs Foreign.idr / BufferAbi.idr)
48+
- probe: cd demo && mix test # host-side instantiate + call; must not be {:error, _} for the happy path
49+
- recovery: re-sync exports to the Idris2 ABI model (realign safe_nif.zig + buffer_abi.zig against Foreign.idr / BufferAbi.idr), rebuild for wasm32-freestanding, re-run `just abi-conformance` until green, then re-instantiate.
50+
- severity: critical
51+
- status: declared
52+
53+
## Near-Hit Cases
54+
55+
# RECOGNITION LEDGER — read, do not drill. Entries here are NOT defects and NOT
56+
# merge-gated. A `bust` runner / k9 gate binds ONLY to "## Failure Modes" above.
57+
# Do not migrate these into Failure Modes; promotion is a human decision via the
58+
# entry's `graduates_to_failure_mode_if:` trip-condition.
59+
60+
### zig-0.15.2-opaque-with-fields-template-rot
61+
- kind: near-hit
62+
- status: recognised
63+
- not_an_issue: true
64+
- class: toolchain # (extends the runner's failure-class enum; near-hits do not bind the runner)
65+
- surfaced_by: cross-repo AFFIRMATION signal, 2026-06-16 — typed-wasm's `ffi/zig` was found genuinely BUST under Zig 0.15.2 (its `tests/e2e.sh` Zig step fails, 48/1/6); we then checked whether the same shape bites snifs.
66+
- what_almost_broke: the rsr-template `src/interface/ffi/src/main.zig` carries pre-0.15 Zig patterns — an `opaque { ...fields... }` (`Handle`) and `std.heap.c_allocator` — that Zig 0.15.2 rejects. Rendered and built, it would not compile.
67+
- why_it_did_not_bite: that file is *unrendered dead scaffold* — it still holds `{{project}}` placeholders and is wired into NO build / test / gate (`tests/e2e.sh` does not touch it; `src/interface/ffi/build.zig`'s compile step is commented out; the proof + conformance gates use `zig/src/safe_nif.zig` + `buffer_abi.zig` only). A direct compile dies at the `{{project}}` placeholder *before* even reaching the type errors. The LIVE guests compile clean under Zig 0.15.2 (verified this session).
68+
- aetiology:
69+
- root-1-template: the rsr-template shipped a generic C-FFI example written in pre-0.15 Zig idioms; snifs never rendered it (it uses `zig/src/safe_nif.zig` instead), so the dead patterns sat latent.
70+
- root-2-toolchain: Zig 0.15.2 tightened two things — `opaque` may no longer carry fields, and `std.heap.c_allocator` now requires explicit libc linking.
71+
- mechanism: `opaque {}` *means* "a type whose size/layout is unknown"; a field declares a layout, so opaque-with-fields is a contradiction and 0.15.2 correctly forbids it (declarations remain legal). `c_allocator` calls libc `malloc`/`free`, which a `wasm32-freestanding` guest has no business linking.
72+
- non-cause-ruled-out: this is NOT a Zig regression (the change is load-bearing type discipline, by design — nothing to fix upstream) and NOT a defect in the live SNIF guests (they never used these patterns and build clean). The bustedness is entirely contained in the dead template.
73+
- recognition: treat Zig 0.15.2's tightening as type discipline to CONFORM TO, not work around. If this template is ever rendered + built, use the opaque-handle idiom (a concrete `struct`, hand C an `*anyopaque` / `*Handle` token, recover via `@ptrCast`/`@alignCast`) and a freestanding allocator (`FixedBufferAllocator` / page-style over a static buffer) — never `c_allocator` at the wasm32-freestanding boundary. Early signal it is about to bite for real: anyone rendering the `{{project}}` placeholders or wiring `src/interface/ffi/` into a build/CI step.
74+
- graduates_to_failure_mode_if: the ffi scaffold is rendered and wired into a real build/gate — at which point "ffi guest won't compile under the toolchain" becomes a live Failure Mode deserving a recovery contract.
75+
- notes: companion to the `SCAFFOLD` banner atop `src/interface/ffi/src/main.zig` (extended 2026-06-16 with this Zig-0.15.2 note) and PROOF-STATUS.md "Scaffold (NOT counted, NOT gated)". The same pattern is genuinely *bust* in typed-wasm — there it is a real failure, here a recognised near-hit.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Dustfile — exnovation contract for snifs
3+
# Author: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
#
5+
# Paired runner: dust.ncl (NOT YET PRESENT — paired .ncl runners are an
6+
# estate-wide gap; cf the Adjustfile contractile-sync
7+
# check. Do not assume a runner exists.)
8+
# Verb: dust
9+
#
10+
# Semantics: DUST is exnovation — the deliberate, CLEAN reduction-to-dust of a
11+
# thing, "without the mess". Per component it declares a disposition
12+
# (deprecate / untangle / dependency-exit / keep-forever) plus an
13+
# explicit exit-condition, so a removal is a recorded decision with a
14+
# trigger, never an ad-hoc deletion. (Sibling: BUST = broken — WHY
15+
# things break.) Nothing here removes anything by itself.
16+
# Run with: dust check (list exnovation declarations + whether the exit-condition holds)
17+
# Fix with: dust fix (execute a declared exnovation only when its exit-condition is met)
18+
19+
@abstract:
20+
Exnovation declarations for snifs. Each entry names a component, its disposition
21+
(deprecate / untangle / dependency-exit / keep-forever), the rationale, and the
22+
explicit exit-condition that must hold before the removal is carried out. This is
23+
the deliberate-retirement ledger; it states intent + trigger, it does not delete.
24+
@end
25+
26+
## Exnovation Declarations
27+
28+
### rsr-template-ffi-scaffold
29+
- target: src/interface/ffi/ (the unrendered rsr-template C-FFI tree, incl. src/main.zig, build.zig, test/integration_test.zig)
30+
- disposition: deprecate
31+
- rationale: dead scaffold — unrendered (`{{project}}` placeholders), wired into no build or gate, and would not compile under Zig 0.15.2 even if rendered (see the Bustfile near-hit `zig-0.15.2-opaque-with-fields-template-rot`). The real guest ABI is `zig/src/{safe_nif,buffer_abi}.zig` + `verification/proofs/idris2/ABI/*` + `abi_conformance.py`.
32+
- exit-condition: remove once the template-instantiation tooling (`tests/e2e/template_instantiation_test.sh`, `scripts/validate-template.sh`, `benches/template_bench.sh`) no longer requires the ffi scaffold present, OR once that tooling is itself retired. Until then KEEP as the untouched rsr-template baseline (it is harmless: it builds nothing).
33+
- status: declared
34+
- severity: advisory
35+
36+
### orphan-rust-guest-wasm-artifact
37+
- target: priv/demo_guest_rust.wasm (813-byte build artifact in the working tree)
38+
- disposition: dependency-exit
39+
- rationale: a stale built artifact NOT loaded by the demo — `SnifDemo.RustGuest` loads `rust/target/.../demo_guest.wasm`, not this. It is a generated leftover, not source. (`priv/*.wasm` is gitignored, so it is never committed.)
40+
- exit-condition: deletable from the working tree at any time; it is regenerated on demand from `rust/crates/demo-guest`. No source depends on this path.
41+
- status: declared
42+
- severity: advisory
43+
44+
### keep-forever-dual-rust-guest-trees
45+
- target: rust/ (Cargo workspace, canonical) and rust-guest/ (standalone single-crate experiment)
46+
- disposition: keep-forever
47+
- rationale: NOT duplication-to-exnovate — owner-declared deliberate (see rust-guest/README.adoc + ADR-0005). The workspace `rust/crates/demo-guest` is the demo's guest; `rust-guest/` is the minimal proof-of-shape. Recorded here so a future hygiene sweep does not "dedupe" them.
48+
- exit-condition: none — explicit keep. Revisit only on owner instruction.
49+
- status: declared
50+
- severity: advisory

0 commit comments

Comments
 (0)