Skip to content

Commit fa00da7

Browse files
theory: crash-consistency design doc + Lean4 keystone theorem (crash_atomic_within_op_mkdir) (#62)
Closes the keystone slice of valence-shell#45 (FS-theory gaps). * docs/THEORY-CRASH-CONSISTENCY.adoc (280 lines): statement of the gap, the atomicity-within-op assumption (justified against ext4 journal-data / ZFS COW / metadata-only journaling), the keystone theorem statement, 4 deferred frontiers (concurrency, POSIX 2024 edges, Idris2-via-WASM, hardware), L4/echo-types cross-link note, bibliography (FSCQ, ALICE, Yggdrasil, COGENT). * proofs/lean4/CrashConsistency.lean (90 lines): CrashPoint inductive (BeforeApply | AfterApply), Op inductive with mkdir constructor, apply / crash_state functions, keystone theorem `crash_atomic_within_op_mkdir` (trivial-by-cases-on-cp), corollary `crash_state_no_third_value`, ends with `#print axioms`. * proofs/lean4/lakefile.lean: register the new lean_lib. Build oracle: `lake build CrashConsistency` GREEN on pinned Lean 4 v4.12.0. `#print axioms` reports the keystone theorem *does not depend on any axioms* — zero new axiom surface. The 4 deferred frontiers each warrant a separate Tier-S issue; see THEORY-CRASH-CONSISTENCY.adoc § Frontiers. Echo-types audit: L4 (dyadic-resource) cross-link recorded as future-link to EchoOFSUnivF5; RECORD-AS-NOT-RELEVANT for this slice (no L3 / echo obligation triggered).
1 parent d26a219 commit fa00da7

3 files changed

Lines changed: 373 additions & 0 deletions

File tree

docs/THEORY-CRASH-CONSISTENCY.adoc

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= Valence Shell — Crash-Consistency Theory
4+
Jonathan Jewell (hyperpolymath)
5+
:toc: left
6+
:toc-title: Contents
7+
:doctype: article
8+
:icons: font
9+
:sectnums:
10+
:sectanchors:
11+
12+
This document is the *theory companion* to the Lean 4 keystone proof
13+
`proofs/lean4/CrashConsistency.lean`. It states the gap, the assumption
14+
the keystone makes explicit, the theorem it proves, and the four
15+
remaining frontiers that are deferred to separate Tier-S issues.
16+
17+
It closes the keystone slice of `valence-shell#45`
18+
("Filesystem-theory + practice gap analysis"). It does *not* claim to
19+
solve crash-consistency in general — it solidifies one foundational
20+
assumption so that the larger story can be told without hand-waving.
21+
22+
See also: `docs/PROOF-OPEN-FRONTIER.adoc` (Tier S/A/B/C catalogue),
23+
`docs/PROOF-NARRATIVE.adoc` (what is proven today),
24+
`docs/POSIX-2024-NOTES.md` (POSIX edges that interact with §5.b).
25+
26+
== The gap
27+
28+
The Lean 4, Coq, Agda, Idris 2 and Isabelle models that valence-shell
29+
ships today share a single common shape: a `Filesystem` is a *pure
30+
function* from `Path` to `Option FSNode`, and an operation
31+
(`mkdir`, `rmdir`, …) is a *pure function* from `Filesystem` to
32+
`Filesystem`. This is a perfectly normal small-step / big-step
33+
operational semantics — it is the same model used by FSCQ, Yggdrasil
34+
and the Coq side of COGENT.
35+
36+
The problem is that *real* state-mutating systems can be interrupted
37+
*during* an operation. Power can fail, a SIGKILL can land, a
38+
container runtime can pull the rug. The pure model assumes that every
39+
operation is atomic — that the world only ever observes the pre-state
40+
or the post-state. Nothing in the current proof tree *justifies*
41+
that assumption.
42+
43+
This is the textbook *Fscq theorem* setup (Chen et al. 2015): the
44+
theorems you can prove against a pure model are only as strong as the
45+
atomicity-within-op assumption you make. Without an explicit
46+
statement, every higher-level theorem (reversibility, MAA, GDPR
47+
irreversibility) implicitly depends on it.
48+
49+
== The atomicity-within-op assumption
50+
51+
The keystone makes the assumption *explicit and minimal*:
52+
53+
[quote]
54+
____
55+
For each modelled `FilesystemOp`, an external observer (the next
56+
process to look at the filesystem after an interruption) sees *either*
57+
the pre-operation state *or* the post-operation state — never a third
58+
"partial" state.
59+
____
60+
61+
This is **not** the claim "no real filesystem ever exposes torn
62+
state". It is the claim "*for the operations valence-shell models at
63+
the proof layer*, the underlying filesystem's atomicity guarantee is
64+
strong enough that the two-point model is faithful". The justification
65+
varies by backing filesystem:
66+
67+
.How the assumption is discharged by real filesystems
68+
[cols="1,3",options="header"]
69+
|===
70+
| Backing filesystem | How atomicity-within-op is achieved
71+
| **ext4 `data=journal`** mode
72+
| Both metadata *and* data are journalled. A `mkdir(2)` is replayed
73+
or rolled back as a single journal transaction — the post-crash
74+
observer sees one of the two endpoints. Faithful to the model.
75+
76+
| **ZFS** (and Btrfs in equivalent COW mode)
77+
| Copy-on-write at the block layer. An interrupted `mkdir` leaves the
78+
old uberblock pointing at the pre-state tree; the new tree is only
79+
linked in atomically at commit. Faithful to the model.
80+
81+
| **ext4 default** (`data=ordered`)
82+
| *Metadata-only* journal. A `mkdir(2)` is still atomic at the
83+
directory-entry level — the entry either appears or doesn't. The
84+
keystone is faithful for `mkdir` and `rmdir`; it is *not* faithful for
85+
operations that mutate file *contents* (those need a separate slice).
86+
87+
| **APFS / NTFS / arbitrary network FS**
88+
| Vendor-dependent. Where vendor documentation does not guarantee
89+
single-op atomicity for the operation in question, the model is not
90+
faithful and the higher-level theorem is correspondingly weakened.
91+
|===
92+
93+
The keystone is therefore *conditional*: every theorem that derives
94+
from it inherits the precondition "the backing filesystem provides
95+
single-op atomicity for this op". Naming the assumption explicitly
96+
lets downstream proofs cite it instead of silently assuming it.
97+
98+
== Keystone theorem
99+
100+
Stated informally:
101+
102+
[quote]
103+
____
104+
*`crash_atomic_within_op_mkdir`* — for any pre-state filesystem `fs`,
105+
any path `p`, and any crash point `cp` observable to an external
106+
process, the filesystem the external process sees is either equal to
107+
`fs` (pre-state) or equal to `mkdir p fs` (post-state). No third state
108+
is observable.
109+
____
110+
111+
The Lean 4 mechanisation lives in
112+
`proofs/lean4/CrashConsistency.lean`. The proof is trivial by case
113+
analysis on the `CrashPoint` inductive — the value is in the modelling,
114+
not in the proof labour. The keystone introduces *zero new axioms*
115+
(verified via `#print axioms`).
116+
117+
A corollary, `crash_state_no_third_value`, states the same fact
118+
contrapositively: if a filesystem `g` is distinct from both the pre-
119+
and post-states, then no crash point of an `Op.mkdir p` produces `g`.
120+
121+
The same shape extends to the other operations valence-shell models
122+
(`rmdir`, file ops, RMOs, …) by extending the `Op` inductive. The
123+
keystone deliberately covers only `mkdir` so that the modelling story
124+
can be reviewed independently of the operational coverage story.
125+
126+
== Frontiers (deferred)
127+
128+
The four remaining frontiers are the parts of crash-consistency *not*
129+
covered by the keystone. Each warrants its own Tier-S issue.
130+
131+
=== Frontier A — Concurrency / linearizability under multi-process
132+
133+
The keystone says nothing about *two* concurrent vsh processes, or vsh
134+
plus an external editor, or vsh inside a container with sibling
135+
processes on the same mount. The Lean 4 model is purely sequential.
136+
137+
**Proposed approach.** Use a Herlihy-Wing-style linearizability
138+
specification. Each `FilesystemOp` is given a linearization point
139+
(usually the journal-commit moment on ext4, the COW commit on ZFS).
140+
The proof obligation is: for every interleaving of two processes'
141+
operations, there exists a sequential schedule that produces the same
142+
observable history. In Lean 4 this is most natural over an explicit
143+
operation log + per-inode locks; the first tractable slice is
144+
"non-overlapping-path operations linearize trivially under per-path
145+
locks", which is the same first slice already identified in
146+
`PROOF-OPEN-FRONTIER.adoc#F-4`.
147+
148+
The frontier interacts with the keystone: a concurrent crash is a
149+
crash *plus* a non-trivial schedule; the linearizability story has to
150+
be layered *on top of* the atomicity-within-op story, not in place of
151+
it.
152+
153+
=== Frontier B — POSIX 2024 edges (sub-second mtime, file-clone, copy_file_range)
154+
155+
POSIX 2024 (IEEE Std 1003.1-2024) introduces three classes of operation
156+
that the current valence-shell model does not cover, each with its own
157+
crash-consistency wrinkle:
158+
159+
1. **Sub-second mtime** (`struct timespec` in `stat`). A crash between
160+
the mtime write and the data write can leave the mtime *ahead* of
161+
the data. The keystone model has no notion of time, so the
162+
atomicity-within-op assumption needs to be stated against a
163+
`(Filesystem, Clock)` pair.
164+
165+
2. **`copy_file_range(2)`** as a single syscall. Linux implements it
166+
as either a reflink (atomic) or a fallback loop (non-atomic). The
167+
keystone is faithful in the reflink branch and faithful only at the
168+
*chunk* boundary in the fallback branch — the modelling needs to
169+
expose that distinction.
170+
171+
3. **`ioctl(FICLONE)` / `FICLONERANGE`**. Reflink semantics — a crash
172+
leaves either the pre-state or the post-state of the *destination*,
173+
while the source is unaffected. This is a clean fit for the
174+
keystone shape but needs to be modelled as a *two-filesystem*
175+
transition (source + destination), not a single one.
176+
177+
**Proposed approach.** Extend the `Op` inductive with `clone`,
178+
`copy_range`, and `touch_mtime` constructors, and extend `Filesystem`
179+
to a record `{ tree : Path → Option FSNode, clock : Time }`. Re-prove
180+
the keystone for each new constructor; the case-analysis structure
181+
carries over. See `docs/POSIX-2024-NOTES.md` for the practical-side
182+
notes on these calls.
183+
184+
=== Frontier C — Idris 2 via typed-WASM crash boundary
185+
186+
valence-shell's Idris 2 proof tree is intended to be extractable to
187+
typed-WASM (see `docs/SEAM_ANALYSIS_AND_IDRIS2_PLAN.md` and the
188+
typed-wasm sibling repo). The crash story at the WASM boundary is
189+
*different* from the crash story at the syscall boundary:
190+
191+
- The WASM host can terminate the module mid-instruction; the host
192+
observes the linear memory at the termination point.
193+
- The WASM module has no syscall-level atomicity guarantee; whatever
194+
atomicity exists has to come from the host's import-function
195+
contracts.
196+
197+
**Proposed approach.** Define a `WasmCrashPoint` inductive parallel to
198+
`CrashPoint` but indexed by WASM instruction boundaries. Prove that
199+
for each `FilesystemOp` *as compiled to typed-WASM*, the only
200+
host-observable crash points coincide with the host's import-function
201+
boundaries — and that the import-function contracts then provide the
202+
atomicity story. This is a two-level proof: the Lean / Idris layer
203+
proves the source-level keystone (this document), and the
204+
typed-WASM-layer proof shows the compilation preserves it. Gates on
205+
typed-wasm proposal 0003 (cross-module L13).
206+
207+
=== Frontier D — Hardware-level (power loss, drive cache, write barriers)
208+
209+
Below the filesystem there is the *block device*. A `mkdir(2)` that
210+
the kernel reports as committed may still be sitting in the drive's
211+
volatile write cache when power fails. The keystone assumption
212+
*presupposes* that write barriers (`fsync`, `fdatasync`, `FUA`) have
213+
done their job; if they haven't, the post-state observable to the next
214+
boot is neither pre- nor post- of the keystone — it is a hardware-level
215+
torn state.
216+
217+
**Proposed approach.** This frontier is *not* a proof obligation in
218+
the same sense as A–C — it is a *modelling boundary*. The
219+
recommendation is:
220+
221+
1. Document the assumption explicitly: "every modelled `FilesystemOp`
222+
is followed by a barrier sufficient to make its effects durable
223+
before any subsequent op is started".
224+
2. Annotate the operations in `proofs/lean4/FilesystemModel.lean` with
225+
the barrier each one assumes (most are `fsync`-on-the-parent-dir for
226+
directory ops; file ops need `fsync`-on-the-file *and* on the
227+
parent).
228+
3. Cross-check against the Rust implementation: `impl/rust/` should
229+
issue those barriers, and an audit ticket should track any gap.
230+
231+
The modelling boundary is intentional: a proof system cannot promise
232+
what the firmware will do. Stating the boundary explicitly is the
233+
honest move.
234+
235+
== L4 (dyadic-resource) cross-link
236+
237+
Crash-consistency is *fundamentally* an L4 (dyadic-resource) shape: a
238+
power-loss event is the canonical interruption of a linear resource.
239+
The pre-state is the resource before consumption; the post-state is
240+
the resource after consumption; the keystone's claim that no third
241+
state is observable is exactly the linear-resource discipline applied
242+
to an externally-observable filesystem.
243+
244+
Intended future cross-link: `echo-types::EchoOFSUnivF5` or whichever
245+
dyadic-layer construct ends up canonical there. The `Op` inductive in
246+
the keystone can be re-stated as a dyadic resource (consumed at
247+
linearization, produced at commit) without changing the proof
248+
structure. This is recorded in `PROOF-NEEDS.md` as
249+
RECORD-AS-NOT-RELEVANT for this slice and as future-link for the L4
250+
slice.
251+
252+
== Bibliography (informal)
253+
254+
- Chen, H., Ziegler, D., Chajed, T., Chlipala, A., Kaashoek, M.F.,
255+
Zeldovich, N. (2015). *Using Crash Hoare Logic for Certifying the
256+
FSCQ File System.* SOSP '15. — the FSCQ paper; the textbook
257+
example of why pure-model atomicity-within-op assumptions need to be
258+
named.
259+
- Pillai, T.S., Chidambaram, V., Alagappan, R., Al-Kiswany, S.,
260+
Arpaci-Dusseau, A.C., Arpaci-Dusseau, R.H. (2014). *All File Systems
261+
Are Not Created Equal: On the Complexity of Crafting
262+
Crash-Consistent Applications.* OSDI '14. — ALICE-style
263+
crash-state enumeration, motivates Frontier A.
264+
- Sigurbjarnarson, H., Bornholt, J., Torlak, E., Wang, X. (2016).
265+
*Push-Button Verification of File Systems via Crash Refinement.*
266+
OSDI '16. — Yggdrasil; pure-model crash refinement, closest in
267+
spirit to the keystone shape used here.
268+
- O'Connor, L., Chen, Z., Susarla, S., Klein, G., Keller, G., Murray,
269+
T., Heiser, G. (2016). *Refinement through Restraint: Bringing Down
270+
the Cost of Verification.* ICFP '16. — COGENT; linear-types story
271+
that motivates the L4 cross-link in §6.
272+
273+
== Status
274+
275+
- **Keystone (this document + Lean 4 proof)**: shipped, zero axiom
276+
dependencies, `lake build` green on Lean 4 v4.12.0.
277+
- **Frontiers A–D**: deferred to separate Tier-S issues, filed at
278+
closure of `valence-shell#45`.
279+
- **Cross-link to echo-types L4**: future-link recorded in
280+
`PROOF-NEEDS.md`; no implementation work here.

proofs/lean4/CrashConsistency.lean

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
-- SPDX-License-Identifier: MPL-2.0
2+
-- Valence Shell — Crash-Consistency Keystone (Lean 4)
3+
--
4+
-- This file models the *atomicity-within-op* assumption that underlies
5+
-- every higher-level crash-consistency theorem for valence-shell.
6+
--
7+
-- Companion design document: docs/THEORY-CRASH-CONSISTENCY.adoc
8+
-- Closes the keystone slice of issue #45 (FS-theory gaps); the four
9+
-- remaining frontiers (concurrency, POSIX 2024 edges, Idris2-via-WASM,
10+
-- hardware-level) are deferred to separate Tier-S follow-ups.
11+
--
12+
-- L4 (dyadic-resource) note: A crash event is the canonical
13+
-- linear-resource interruption. Future cross-link to
14+
-- echo-types `EchoOFSUnivF5` / the dyadic layer is intended.
15+
16+
import FilesystemModel
17+
18+
namespace ValenceShell.CrashConsistency
19+
20+
/-- A `CrashPoint` describes *when* an external interruption observes
21+
the operation. The atomicity-within-op assumption is that the only
22+
two observable points are *before* applying the state mutation and
23+
*after* applying it — never in the middle. -/
24+
inductive CrashPoint where
25+
| BeforeApply : CrashPoint
26+
| AfterApply : CrashPoint
27+
deriving DecidableEq, Repr
28+
29+
/-- Operations valence-shell currently models at the proof layer.
30+
Only `mkdir` is needed for the keystone; the broader catalogue
31+
(`rmdir`, file ops, RMOs, …) reuses the same crash-point shape. -/
32+
inductive Op where
33+
| mkdir : Path → Op
34+
deriving Repr
35+
36+
/-- Apply an `Op` to a `Filesystem`. This is the *post-state* of the
37+
operation; pre-state is just the original `fs`. -/
38+
def apply (fs : Filesystem) : Op → Filesystem
39+
| Op.mkdir p => mkdir p fs
40+
41+
/-- The crash-state function: given a starting filesystem, an op, and
42+
a crash point, the observable filesystem after the interruption is
43+
*either* the pre-state (BeforeApply) *or* the post-state (AfterApply).
44+
Nothing else is observable — that is the content of the keystone
45+
theorem below. -/
46+
def crash_state (fs : Filesystem) (op : Op) : CrashPoint → Filesystem
47+
| CrashPoint.BeforeApply => fs
48+
| CrashPoint.AfterApply => apply fs op
49+
50+
/-- **Keystone: crash atomicity within a single mkdir.**
51+
For any pre-state `fs`, any path `p`, and any crash point `cp`,
52+
the observable filesystem at the crash boundary is *either* the
53+
pre-state or the post-state — never a partial third state.
54+
55+
This is trivial-by-cases on `cp` once the model is set up. The
56+
value is in the modelling: it pins down the assumption that
57+
valence-shell's correctness story relies on (journal-data mode on
58+
ext4, COW on ZFS, atomic-rename idioms on metadata-only journals). -/
59+
theorem crash_atomic_within_op_mkdir
60+
(fs : Filesystem) (p : Path) (cp : CrashPoint) :
61+
crash_state fs (Op.mkdir p) cp = fs
62+
∨ crash_state fs (Op.mkdir p) cp = mkdir p fs := by
63+
cases cp with
64+
| BeforeApply =>
65+
left
66+
rfl
67+
| AfterApply =>
68+
right
69+
rfl
70+
71+
/-- Corollary: the crash state is *never* a "third" filesystem
72+
distinct from both the pre- and post-states. -/
73+
theorem crash_state_no_third_value
74+
(fs : Filesystem) (p : Path) (cp : CrashPoint)
75+
(g : Filesystem)
76+
(h1 : g ≠ fs)
77+
(h2 : g ≠ mkdir p fs) :
78+
crash_state fs (Op.mkdir p) cp ≠ g := by
79+
intro heq
80+
cases crash_atomic_within_op_mkdir fs p cp with
81+
| inl hpre => rw [hpre] at heq; exact h1 heq.symm
82+
| inr hpost => rw [hpost] at heq; exact h2 heq.symm
83+
84+
end ValenceShell.CrashConsistency
85+
86+
-- Axiom audit (Lean 4 equivalent of Coq's `Print Assumptions`).
87+
-- Expected output: "'crash_atomic_within_op_mkdir' depends on axioms: [propext]"
88+
-- or no axioms at all — both acceptable; the keystone introduces no
89+
-- new axioms beyond what FilesystemModel.lean already uses.
90+
#print axioms ValenceShell.CrashConsistency.crash_atomic_within_op_mkdir

proofs/lean4/lakefile.lean

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,6 @@ lean_lib CopyMoveOperations where
4545

4646
lean_lib PermissionOperations where
4747
srcDir := "."
48+
49+
lean_lib CrashConsistency where
50+
srcDir := "."

0 commit comments

Comments
 (0)