→ / space advance · ← back
Moss · formal methods, 6 minutes

Prove it, don't
just test it.

A bug every web dev has shipped, caught by a compiler that refuses to make human assumptions — and pinned to the real code that ships.

Press → to break something.

Step 1 · the code you'd write

The transfer function everyone writes

bank.js
function withdraw(balance, amount) {
if (amount <= balance)
return balance - amount;
return balance;
}
100 − 20 → 80 ✓
0 − 200 blocked ✓
tests green ✓

Two unit tests pass. Ship it?

A human reads amount as "a positive number".

The compiler reads it as "every integer that exists".

Step 2 · the claim, not a test case

State the property for every input

Mini.lean
-- a withdrawal never grows your balance:
theorem never_grows (balance amount : Int) :
withdraw balance amount ≤ balance

No test values. balance and amount are variables over all integers — infinitely many pairs, one claim.

what the code actually does
#eval withdraw 0 (-50)
50 ← a zero balance became £50. Alice stole money.
Step 2b · the compiler pushes back

The proof fails — and names the bug's address

lean Mini.lean
$ lean Mini.lean
error: omega could not prove the goal:
a possible counterexample may satisfy:
b ≤ -1 ← the amount is NEGATIVE
where a := balance, b := amount

Nobody typed −£50 as a test case. The failed proof derived that negative amounts are exactly where it breaks.

A failing test says "this one input broke". A rejected proof says the shape of every breaking input — which is why the fix is mechanical.

Step 3 · one clause, now proved

Add what the human assumed

Mini.lean · fixed
def withdraw (balance amount : Int) : Int :=
if 0 ≤ amount ∧ amount ≤ balance
then balance - amount else balance
-- theorem unchanged:
theorem never_grows ... := by
unfold withdraw; split <;> omega
lean Mini.lean
$ lean Mini.lean
✓ no output — silence IS the proof
#print axioms never_grows
[propext, Quot.sound] ← no sorry: no fake proof

Proved for every pair of integers — and the axiom audit means a future "temporary sorry" fails the build instead of faking green.

Step 4 · now it's your codebase

The upload guard in the Moss app

uploads.js — the guard everyone writes
if (abs.startsWith(UPLOAD_DIR)) // looks fine
srv/uploads/../.ssh/id_rsa

A .. buried mid-path. You'd unit-test a.png and ../../etc/passwd — nobody tests a .. in the middle.

the model, computed
-- careless guard accepts?
true
-- where does it really resolve?
["srv", ".ssh", "id_rsa"] ← OUT of the vault
-- the shipped guard (resolve FIRST)?
false ✓ rejected

Shout a filename. Whatever you say — the theorem already covers it.

Step 4b · the machine picked the witnesses

Every path of length ≤ 4, exhaustively

0
paths in the slice — every combination of
srv · uploads · .. · . · a · etc
0
escapes found for the
careless startsWith guard
0
leaks for the shipped
resolve-first guard

All six escapes share one shape: a .. right after the managed prefix — the exact condition the proof isolates. We never picked them.

The sweep is exhaustive for the slice. The theorem is what covers the infinity beyond it — by induction, not enumeration.

The honest asterisk

Same witness, two worlds

node --test uploads.test.js
✔ accepts a real file inside the managed dir
✔ rejects the Lean counterexample (mid-path ..)
✔ rejects sibling dir "moss-uploads-evil"
✔ rejects missing files, relative, empty
ℹ pass 4 · fail 0
layerguarantee
theorems ↔ modelcertain — compiler-checked, zero sorry
model ↔ JSpinned — same witness runs in Node; refactor away path.resolve and CI goes red
known gapssymlinks (statSync follows links) · POSIX only · named in README, not hidden

Lean turns "is this safe?" into "is my model faithful?" — a small, attackable question instead of a vibe.

Take it home

It's one file, not a research project

your terminal, today
$ curl https://elan.lean-lang.org/elan-init.sh -sSf | sh # toolchain
$ lean Mini.lean # 9 lines, seconds
- run: (cd proofs && lake build) # one line, next to node --test

Unit tests check what you imagined.
Proofs check what's possible.

Proofs: moss/proofs/Uploads.lean · pinned by uploads.test.js — Moss 🪨