Skip to content

Commit 70d9f65

Browse files
checked_add: make it genuinely checked (overflow traps) — resolve the wrapping misnomer (#40)
## Summary Follow-up to #39. `checked_add` was a wrapping `a +% b` despite its name — the GAP-1b metamorphic gate surfaced the misnomer in #39. This makes it a genuinely **checked** add. - **zig** (`safe_nif.zig`): `@addWithOverflow` + `unreachable` → overflow **traps** (WASM `unreachable`, all build modes, like `crash_unreachable`) → wasmex `{:error, _}`, BEAM survives; a non-overflowing add returns the **exact sum**. Signature unchanged. - **metamorphic gate** (`snif_metamorphic_test.exs`): flipped from the `wrap32` ring oracle to *non-overflow = exact sum / overflow = `{:error,_}`*; added the boundary-trap test. - **bench + docs**: fixed the stale "intentional wrap" row + comments in `benches/snif_eval.sh`; reconciled `CHANGELOG` + `PROOF-STATUS` GAP-1b. The `AFFIRMATION.adoc` (merged in #39, anchored to `a82bb31`) is intentionally **not** edited — at that anchor `checked_add` genuinely *was* wrapping, and the affirmation is a frozen, signed snapshot. A fresh affirmation can be stamped at a later commit if desired. ## Verification (green this commit) - `just proof-check-all` → exit 0 (signature-only ABI unaffected) - `just abi-conformance` → 15/15 (signature unchanged) - `mix test` → 30/30 (OTP 25); metamorphic 9/9 under the new checked semantics - Zig 0.15.2 compiles `@addWithOverflow` + tuple indexing + `unreachable` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 1d7fac8 + 3af8bec commit 70d9f65

5 files changed

Lines changed: 58 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
invocation (no `--source-dir`, never resolved the `ABI.*` graph) and silently passed
2626
when the prover was absent (SKIP = exit 0). Now uses the correct invocation and
2727
fails-on-skip.
28-
- **`checked_add` doc-comment corrected.** The comment claimed "overflow -> trap" but the body is
29-
`a +% b` (wrapping); the GAP-1b metamorphic gate surfaced the contradiction. The comment now
30-
states the wrapping behaviour accurately (the trapping-overflow demo is `crash_overflow`); the
31-
export name is unchanged.
28+
- **`checked_add` made genuinely checked.** The export was a wrapping `a +% b` despite its name
29+
(the GAP-1b metamorphic gate surfaced the misnomer). It is now a real checked add: overflow
30+
TRAPS (`@addWithOverflow` + `unreachable` → WASM trap → `{:error, _}`, BEAM survives) in all
31+
build modes, and a non-overflowing add returns the exact sum. Signature unchanged (the ABI gate
32+
stays green); the metamorphic oracle now asserts trap-on-overflow.
3233

3334
### Changed
3435
- Project gloss **"Safe NIFs" → "Safer NIFs"** (acronym SNIF unchanged): WASM sandboxing
@@ -51,7 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5152
Zig export sites; `verification/tools/abi_conformance.py` is now guest-aware (per-guest model
5253
manifest, multi-value/void parsing). The conformance gate now runs in CI (`proofs.yml`, CI-1).
5354
- **GAP-1b behaviour gate.** `demo/test/snif_metamorphic_test.exs` — dependency-free metamorphic
54-
relations over the scalar kernels (fibonacci recurrence + base cases; `checked_add` `wrap32`
55-
oracle + boundary-wrap).
55+
relations over the scalar kernels (fibonacci recurrence + base cases; `checked_add`
56+
non-overflow = exact-sum / overflow = trap oracle).
5657
- `AFFIRMATION.adoc` — point-in-time, ground-truthed honesty snapshot (the README/EXPLAINME/AFFIRMATION
5758
trio); SPDX header parked for the owner to add + sign.

PROOF-STATUS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ obligations (e.g. the Coq slot → the WasmCert-Coq isolation theorem).
223223
| SEC-1-TCB | Discharge **"wasmtime ⊨ FaithfulRuntime"** in-prover: prove that wasmtime + the wasmex embedding actually realise the primitive single-step facts (WASM trap-soundness; trap → `{:error,_}`; host scheduler resumed unchanged) that `SnifIsolation.agda` assumes as its TCB record | SEC | Coq (WasmCert-Coq) | SEC-1's **operational** layer is now PROVEN-MODULO-EXPLICIT-TCB in Agda (`Model.isolation`). This row is the REMAINING half: machine-verifying the runtime-faithfulness assumption that is currently in the theorem's *type* but not proven. |
224224
| ABI-6 | Buffer/array marshalling round-trip + in-bounds (unblocks "powerful" NIFs; FFT) | ABI | Idris2 | `Compliance.WasmArray*` is only a framework today |
225225
| ABI-7 |**Coverage 15 of 20 — buffer_abi DONE 2026-06-16.** `safe_nif` (8, `Foreign.idr`) + `buffer_abi` (7, `BufferAbi.idr`, incl. 3 void returns) are modelled+gated, both in the `abi_conformance.py` guest manifest. **Remaining (ledgered):** `zig/src/burble_fft.zig` (5 — `fft`/`ifft`/… use `(ptr,len)` slice marshalling = ABI-6, and it is not built into any artifact) + the Rust buffer guest | ABI | Idris2 + Python | Buffer guest closed; FFT + Rust pending the multi-language buffer-ABI gate |
226-
| GAP-1b |**Behaviour faithfulness — scalar kernels DONE 2026-06-16.** `demo/test/snif_metamorphic_test.exs` (dep-free, 9 metamorphic tests, green on OTP 25). **Load-bearing relations (do not delete):** the `fibonacci` recurrence n=2..40 + base cases (uniquely determines fib), and the `checked_add` `wrap32` oracle over a 100-case boundary-spanning family + the boundary-wrap test. Buffer kernels (`sum_f32` permutation/additivity) are next | TP | metamorphic tests (extraction long-term) | Surfaced a finding: `checked_add` is a **misnomer** — it is wrapping (`a +% b`), not trapping (the trapping demo is `crash_overflow`); doc-comment corrected |
226+
| GAP-1b |**Behaviour faithfulness — scalar kernels DONE 2026-06-16.** `demo/test/snif_metamorphic_test.exs` (dep-free, 9 metamorphic tests, green on OTP 25). **Load-bearing relations (do not delete):** the `fibonacci` recurrence n=2..40 + base cases (uniquely determines fib), and the `checked_add` oracle over a 100-case boundary-spanning family (non-overflow = exact sum; overflow = `{:error,_}`) + the boundary-trap test. Buffer kernels (`sum_f32` permutation/additivity) are next | TP | metamorphic tests (extraction long-term) | The gate first surfaced that `checked_add` was a **misnomer** (wrapping `a +% b`); **resolved 2026-06-16 by making it genuinely checked** — overflow traps (`@addWithOverflow` + `unreachable`, all modes) |
227227
| CI-1 |**DONE 2026-06-16**: `abi-conformance` now runs as a CI job in `.github/workflows/proofs.yml` (builds both guests, fails on signature drift) | ABI | CI wiring | Making it a *required* status check is the owner-only branch-protection step |
228228
| MODEL-1 |**Largely RESOLVED by F2 (SNIFs 2).** The 6 `error_reason` origins are now modelled via `TrapOrigin` (guestFault/hostBudget/preExec) + the `call` front-end in `SnifIsolation`. **Residual:** the `snif_alloc`-returns-0 OOM "third outcome" is not yet modelled as a distinct verdict | TP | Agda | Taxonomy half done; only the OOM-sentinel nuance remains |
229229

@@ -235,4 +235,4 @@ obligations (e.g. the Coq slot → the WasmCert-Coq isolation theorem).
235235
| 2026-04-16 | (Claimed) all 7 proofs complete — see History correction; not actually compiling | Claude Code |
236236
| 2026-06-16 | All 7 proofs **genuinely** machine-checked (clean-cache); real gate wired; scaffold de-counted; scope honesty added | Claude Opus 4.8 |
237237
| 2026-06-16 | **SEC-1 (`Model.isolation`) PROVEN-MODULO-EXPLICIT-TCB** in Agda (`SnifIsolation.agda`, `--safe --without-K`, clean-cache exit 0); TCB = `FaithfulRuntime` record hypothesis (primitive single-step facts), non-circularity + non-vacuity confirmed by mutation; WASM-opsem discharge ("wasmtime ⊨ FaithfulRuntime") remains as SEC-1-TCB | Claude Opus 4.8 (1M context) |
238-
| 2026-06-16 | **SNIFs 2.** SEC-1 sharpened: F1 deniability wired operationally (`run-deniable`/`fault-via-observe` + `SecretWitness`), F2 6-origin `TrapOrigin` taxonomy + `call` front-end (`PreExecWitness`), F5 non-trivial-`Alive` recovery witness (`PartialAlive`) — all mutation-confirmed load-bearing by a 4-skeptic re-audit. ABI-7: `buffer_abi` modelled+gated (`BufferAbi.idr`, 15/20 sites); guest-aware `abi_conformance.py`; CI-1 conformance job added; GAP-1b scalar metamorphic gate (found + corrected the `checked_add` misnomer). | Claude Opus 4.8 (1M context) |
238+
| 2026-06-16 | **SNIFs 2.** SEC-1 sharpened: F1 deniability wired operationally (`run-deniable`/`fault-via-observe` + `SecretWitness`), F2 6-origin `TrapOrigin` taxonomy + `call` front-end (`PreExecWitness`), F5 non-trivial-`Alive` recovery witness (`PartialAlive`) — all mutation-confirmed load-bearing by a 4-skeptic re-audit. ABI-7: `buffer_abi` modelled+gated (`BufferAbi.idr`, 15/20 sites); guest-aware `abi_conformance.py`; CI-1 conformance job added; GAP-1b scalar metamorphic gate (found the `checked_add` misnomer, then made it genuinely checked — overflow traps). | Claude Opus 4.8 (1M context) |

benches/snif_eval.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,13 @@ invoke "$SAFE" fibonacci 20; fib_safe="$G_OUT"
146146
invoke "$FAST" fibonacci 20; fib_fast="$G_OUT"
147147
add_row "{\"kind\":\"control\",\"fn\":\"fibonacci(20)\",\"safe_out\":$(jstr "$fib_safe"),\"fast_out\":$(jstr "$fib_fast"),\"expect\":\"6765\"}"
148148

149-
# checked_add uses wrapping (+%) by design: i32::MAX + 1 wraps in BOTH modes (NOT
150-
# a trap) — this is intentional and documented in the source. Record it so the
151-
# table shows the deliberate wrap rather than implying it is a discrimination row.
152-
invoke "$SAFE" checked_add 2147483647 1; ca_safe="$G_OUT"
153-
invoke "$FAST" checked_add 2147483647 1; ca_fast="$G_OUT"
154-
add_row "{\"kind\":\"control\",\"fn\":\"checked_add(MAX,1)\",\"safe_out\":$(jstr "$ca_safe"),\"fast_out\":$(jstr "$ca_fast"),\"expect\":\"-2147483648 (intentional wrap, both modes)\"}"
149+
# checked_add is genuinely CHECKED: a non-overflowing add returns the exact sum
150+
# (this control row uses MAX-1 + 1 = MAX). An OVERFLOWING call (e.g. MAX + 1) FAULTS
151+
# in BOTH modes (overflow -> `unreachable` -> WASM trap), unlike crash_overflow which
152+
# only traps under ReleaseSafe — so this is a value row, not a discrimination row.
153+
invoke "$SAFE" checked_add 2147483646 1; ca_safe="$G_OUT"
154+
invoke "$FAST" checked_add 2147483646 1; ca_fast="$G_OUT"
155+
add_row "{\"kind\":\"control\",\"fn\":\"checked_add(MAX-1,1)\",\"safe_out\":$(jstr "$ca_safe"),\"fast_out\":$(jstr "$ca_fast"),\"expect\":\"2147483647 (exact sum; an overflowing add would trap in both modes)\"}"
155156

156157
# ── (4) liveness / DoS guard (language-agnostic) ──────────────────────────────
157158
# Too little fuel -> deterministic "all fuel consumed" trap; enough -> result.
@@ -279,7 +280,7 @@ fmt_num() { printf "%.3f" "$1" 2>/dev/null || printf "%s" "$1"; }
279280
printf " %-22s | %-13s | %-13s | %s\n" "fn" "Safe" "Fast" "expect"
280281
printf " %-22s | %-13s | %-13s | %s\n" "still_alive" "$sa_safe" "$sa_fast" "42"
281282
printf " %-22s | %-13s | %-13s | %s\n" "fibonacci(20)" "$fib_safe" "$fib_fast" "6765"
282-
printf " %-22s | %-13s | %-13s | %s\n" "checked_add(MAX,1)" "$ca_safe" "$ca_fast" "-2147483648 (intentional wrap)"
283+
printf " %-22s | %-13s | %-13s | %s\n" "checked_add(MAX-1,1)" "$ca_safe" "$ca_fast" "2147483647 (exact sum; overflow traps)"
283284
echo
284285
echo " (4) LIVENESS / DoS GUARD — fuel (the wasmex-0.14 mechanism)"
285286
echo " -------------------------------------------------------------------------------------------"

demo/test/snif_metamorphic_test.exs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,40 +62,36 @@ defmodule SnifMetamorphicTest do
6262
assert val!("fibonacci", [46]) == 1_836_311_903
6363
end
6464

65-
# ── checked_add: a WRAPPING i32 add (`a +% b`, intentional per zig/src/safe_nif.zig) ──
65+
# ── checked_add: a genuinely CHECKED i32 add — overflow TRAPS (see zig/src/safe_nif.zig) ──
6666
#
67-
# NOTE: the export name "checked_add" is a MISNOMER in the guest source — it is two's-
68-
# complement WRAPPING addition, not a trapping/checked one (the trapping overflow demo is
69-
# the separate `crash_overflow`). This gate verifies the kernel's ACTUAL behaviour: the
70-
# i32 modular ring. (Finding surfaced by this very gate — see PROOF-STATUS GAP-1b.)
71-
72-
# Signed-i32 wrap of an arbitrary integer (the +% oracle).
73-
defp wrap32(x) do
74-
import Bitwise
75-
m = band(x, 0xFFFFFFFF)
76-
if m >= 0x8000_0000, do: m - 0x1_0000_0000, else: m
77-
end
67+
# The guest computes a + b via `@addWithOverflow` and hits `unreachable` on overflow, so an
68+
# overflowing call FAULTS (-> {:error, _}, BEAM survives) instead of returning a value; a
69+
# non-overflowing call returns the exact sum. (History: this export was a wrapping `a +% b`
70+
# despite its name — the misnomer this gate originally surfaced — and was then made genuinely
71+
# checked, the behaviour the name promises. See PROOF-STATUS GAP-1b.)
72+
73+
# Does a + b overflow signed i32? (arbitrary-precision host-side oracle.)
74+
defp overflows_i32?(a, b), do: a + b > @i32_max or a + b < @i32_min
7875

79-
test "checked_add METAMORPHIC commutativity: add(a,b) = add(b,a) (incl. boundary)" do
80-
pairs = [{1, 2}, {-5, 9}, {1000, 24}, {-7, -8}, {@i32_max, 1}, {@i32_min, -1}]
76+
test "checked_add METAMORPHIC commutativity on non-overflowing pairs: add(a,b) = add(b,a)" do
77+
pairs = [{1, 2}, {-5, 9}, {1000, 24}, {-7, -8}, {@i32_max - 3, 2}, {@i32_min + 3, -2}]
8178

8279
Enum.each(pairs, fn {a, b} ->
80+
refute overflows_i32?(a, b), "test pair (#{a},#{b}) must not overflow"
8381
assert val!("checked_add", [a, b]) == val!("checked_add", [b, a]),
8482
"commutativity broken at (#{a},#{b})"
8583
end)
8684
end
8785

88-
test "checked_add METAMORPHIC identity: add(a,0) = a and add(0,a) = a" do
86+
test "checked_add METAMORPHIC identity: add(a,0) = a and add(0,a) = a (adding 0 never overflows)" do
8987
Enum.each([0, 1, -1, 42, -42, @i32_max, @i32_min], fn a ->
9088
assert val!("checked_add", [a, 0]) == a
9189
assert val!("checked_add", [0, a]) == a
9290
end)
9391
end
9492

95-
test "checked_add METAMORPHIC associativity EVERYWHERE (modular ring, incl. boundary)" do
96-
# Wrapping add is associative for ALL inputs (it is the Z/2^32 ring), unlike a trapping
97-
# add — so boundary triples that would trap a checked add must still associate here.
98-
triples = [{1, 2, 3}, {-4, 5, -6}, {@i32_max, 1, 1}, {@i32_min, -1, -1}, {@i32_max, @i32_max, 2}]
93+
test "checked_add METAMORPHIC associativity where no intermediate overflows" do
94+
triples = [{1, 2, 3}, {-4, 5, -6}, {100, 200, 300}, {-1, -1, -1}]
9995

10096
Enum.each(triples, fn {a, b, c} ->
10197
left = val!("checked_add", [val!("checked_add", [a, b]), c])
@@ -104,21 +100,29 @@ defmodule SnifMetamorphicTest do
104100
end)
105101
end
106102

107-
test "checked_add WRAPS at the i32 boundary (the defining +% behaviour, vs a trapping add)" do
108-
# i32_max + 1 wraps to i32_min; i32_min + (-1) wraps to i32_max — a TRUE value, not {:error}.
109-
assert val!("checked_add", [@i32_max, 1]) == @i32_min
110-
assert val!("checked_add", [@i32_min, -1]) == @i32_max
111-
# ...and one short of the boundary does not wrap.
103+
test "checked_add is CHECKED: overflow at the boundary TRAPS (BEAM survives); just-below does not" do
104+
# The defining behaviour: i32_max + 1 and i32_min + (-1) FAULT, they do not wrap.
105+
assert {:error, _} = Loader.call(@safe, "checked_add", [@i32_max, 1])
106+
assert {:error, _} = Loader.call(@safe, "checked_add", [@i32_min, -1])
107+
assert {:error, _} = Loader.call(@safe, "checked_add", [@i32_max, @i32_max])
108+
# ...one short of the boundary is fine (so it is not "always traps").
112109
assert val!("checked_add", [@i32_max - 1, 1]) == @i32_max
113110
assert val!("checked_add", [@i32_min + 1, -1]) == @i32_min
111+
# ...and the boundary trap did not take the BEAM with it.
112+
assert val!("still_alive", []) == 42
114113
end
115114

116-
test "checked_add ORACLE: add(a,b) = wrap32(a+b) across a boundary-spanning family" do
115+
test "checked_add ORACLE: non-overflow = exact sum; overflow = {:error,_}, over a boundary family" do
117116
family = [-3, -1, 0, 1, 3, @i32_max - 1, @i32_max, @i32_min, @i32_min + 1, 1_000_000_000]
118117

119118
for a <- family, b <- family do
120-
assert val!("checked_add", [a, b]) == wrap32(a + b),
121-
"modular mismatch at (#{a},#{b})"
119+
if overflows_i32?(a, b) do
120+
assert {:error, _} = Loader.call(@safe, "checked_add", [a, b]),
121+
"expected a trap on overflow at (#{a},#{b})"
122+
else
123+
assert val!("checked_add", [a, b]) == a + b,
124+
"expected the exact sum at (#{a},#{b})"
125+
end
122126
end
123127
end
124128
end

zig/src/safe_nif.zig

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ export fn fibonacci(n: i32) i64 {
4949
return b;
5050
}
5151

52-
/// Two's-complement WRAPPING i32 addition (`a +% b`). NOTE: despite the historical name
53-
/// `checked_add`, this does NOT trap on overflow — it wraps (i32_max + 1 = i32_min), in BOTH
54-
/// ReleaseSafe and ReleaseFast (`+%` is defined wrapping, not UB, so the build mode is irrelevant).
55-
/// The trapping-overflow demo is `crash_overflow`. Behaviour pinned by the GAP-1b metamorphic
56-
/// `wrap32` oracle in `demo/test/snif_metamorphic_test.exs`.
52+
/// Genuinely CHECKED i32 addition (the name now matches the behaviour). On overflow the guest
53+
/// TRAPS: `@addWithOverflow` computes the result + an overflow bit with no UB, and on overflow we
54+
/// hit `unreachable`, which in this wasm32-freestanding target emits the WASM `unreachable`
55+
/// instruction and traps in ALL build modes (-> wasmex `{:error, _}`, BEAM survives) — like
56+
/// `crash_unreachable`, and unlike `crash_overflow` which only traps under ReleaseSafe. With no
57+
/// overflow it returns the exact sum. Behaviour pinned by the GAP-1b metamorphic boundary-trap
58+
/// oracle in `demo/test/snif_metamorphic_test.exs`.
5759
export fn checked_add(a: i32, b: i32) i32 {
58-
return a +% b; // wrapping add — intentional; see the doc-comment above
60+
const r = @addWithOverflow(a, b);
61+
if (r[1] != 0) unreachable; // overflow -> WASM unreachable -> trap
62+
return r[0];
5963
}
6064

6165
// --- Crash isolation demos ---

0 commit comments

Comments
 (0)