Durability & Consistency

This document describes the durability and consistency guarantees provided by ZeroFS, including write atomicity, operation ordering, and crash recovery semantics.

Overview

ZeroFS implements a transactional storage model on object storage. File contents live as compressed, encrypted frames inside immutable segment objects written directly to the object store. Metadata (inodes, directory entries, and one 32-byte FrameLoc pointer per 32 KiB extent) lives in an LSM-tree database. This architecture provides three properties:

  1. Write Atomicity: All filesystem operations are atomic at the transaction level
  2. Strict Ordering: Operations are totally ordered and this order is preserved across crashes
  3. Recovery-Free Startup: No filesystem check or repair is required after unexpected termination

Write Atomicity

Each filesystem operation executes within a single database transaction. A transaction bundles all related modifications into an atomic unit that either commits entirely or has no effect.

Transaction Scope

A write operation includes the following modifications within a single transaction:

  • Extent pointers: one 32-byte FrameLoc per 32 KiB extent
  • Per-segment live-byte counter deltas
  • Inode metadata (size, timestamps, mode)
  • Directory entry updates
  • Global statistics counters

The extent bytes themselves are compressed, encrypted, and appended to the in-RAM open segment buffer as the transaction is staged. No object-store PUT occurs on the write path; the transaction commits only pointers and metadata.

The database commits these changes using a WriteBatch, which provides all-or-nothing semantics. Partial writes cannot occur.

Failure Modes

EventOutcome
Crash before commitTransaction discarded, no changes visible
Crash during commitTransaction discarded, no changes visible
Crash after commitAll changes visible

The intermediate state where some but not all modifications are visible is not possible.

Transaction Structure

// Create transaction
let mut txn = db.new_transaction();

// Seal extents into the open segment buffer,
// stage FrameLoc pointers in the transaction
extent_store.write(&mut txn, id, offset, data, old_size);

inode.size = new_size;
inode.mtime = now;
inode_store.save(&mut txn, id, &inode);

directory_store.add(&mut txn, ...);

// Atomic commit via the single-writer coalescer
write_coordinator.commit(txn).await;

Operation Ordering

ZeroFS provides a total order over all write operations. This order is preserved exactly across process termination and restart.

Ordering Guarantee

All write transactions are submitted through a single commit coalescer: each operation sends its transaction to a dedicated worker task that drains the queue, merges pending transactions into one WriteBatch, and issues a single atomic write:

// caller
write_coordinator.commit(txn).await;

// worker (single task, FIFO drain)
let mut batch = vec![rx.recv().await];
while let Ok(m) = rx.try_recv() { batch.push(m); }
let mut merged = WriteBatch::new();
for (txn, _) in &batch { txn.apply_to(&mut merged); }
db.write(merged).await;

Per-inode locks (covering files and directories) serialize operations that observe each other's state; global statistics and per-segment counters are folded by the single commit worker itself, which is their sole writer. If operation A completes before operation B begins, B's transaction is sent strictly after A's commit returned, and therefore lands in a strictly later batch. The visibility of B implies the visibility of A.

Formal Property

For operations A and B where A completes before B starts:

visible(B) → visible(A)

The contrapositive also holds: if A is not visible, B cannot be visible.

Comparison with Other Filesystems

Many filesystems permit write reordering for performance optimization:

  • ext4: May reorder writes within journal transactions
  • XFS: Delayed allocation can reorder data relative to metadata
  • Hardware: SSDs and disk controllers may reorder writes internally

Such reordering can result in states where a later write is visible while an earlier write is not. Applications that depend on ordering semantics (databases, logging systems, configuration management) may observe inconsistent state after recovery.

ZeroFS does not reorder writes. The single-writer commit path enforces a total order over batches that survives crashes.

Crash Recovery

ZeroFS requires no recovery procedure after unexpected termination.

Recovery-Free Design

The underlying storage model has several properties that eliminate the need for crash recovery:

Atomic Commits: WriteBatch operations are atomic at the database level. A transaction either appears in full or not at all.

Immutable Storage: SST files and segment objects written to object storage are immutable. Once uploaded, they cannot be corrupted by subsequent operations. Each segment frame carries its own authenticated-encryption tag (XChaCha20-Poly1305); the segment footer carries a keyless CRC32C that detects torn writes.

Self-Describing State: The database manifest describes the complete set of valid SST files. On startup, ZeroFS reads the manifest and has immediate access to the latest consistent state.

Seal-Before-Manifest Ordering: A flush uploads the open segment before the metadata flush that references it. A durable manifest never points at a missing segment object. A crash between the two steps leaves only an orphaned segment, later reclaimed by segment GC, never a dangling pointer.

No Write-Ahead Log: The WAL is permanently disabled and not configurable. A WAL flush is not gated by the seal barrier: it could make a FrameLoc pointer durable while the segment it points at is still the un-uploaded in-RAM buffer. With the WAL off, the barrier-gated flush is the only path that makes metadata durable, and startup has no log to replay.

Startup Procedure

After any termination (graceful or unexpected):

  1. Read manifest from object storage
  2. Load database state from referenced SST files
  3. Resume operation

No scanning, replay, or repair is performed. The time to resume operation is independent of filesystem size.

Durability Semantics

ZeroFS follows standard POSIX durability semantics. Write operations are buffered in memory and persisted to durable storage upon explicit synchronization.

Buffering Model

File data flows through the following stages:

  1. Application Buffer: User-space buffers managed by the application
  2. Page Cache: OS-managed in-memory buffers
  3. Open Segment Buffer: In-RAM buffer of compressed, encrypted frames
  4. Segment Objects: Immutable objects in object storage

Metadata (inodes, directory entries, FrameLoc pointers) flows through the LSM memtable into immutable SST files. The open segment uploads in the background once it reaches 256 MiB; at most 4 uploads run concurrently.

A flush is two-phase under a single barrier lock: seal and upload the open segment, then flush the memtable. If the seal fails, the metadata flush is aborted. Flushes occur when:

  • A client requests durability (fsync(), NFS COMMIT, NBD flush)
  • The periodic flush interval elapses (flush_interval_secs, default 30 s, minimum 5 s)
  • sync_writes = true completes a commit batch
  • A segment GC pass begins
  • The process terminates gracefully

There is no memtable-capacity flush trigger; the memtable drains only at the barrier-gated flush.

POSIX Compliance

This behavior conforms to POSIX semantics, which specify that write() transfers data to system buffers while fsync() ensures data reaches stable storage. All major filesystems (ext4, XFS, APFS, ZFS...) implement this model.

Synchronization API

// POSIX interface
write(fd, data, len);   // Buffer in memory
fsync(fd);              // Persist to storage
# Python
f.write(data)
f.flush()
os.fsync(f.fileno())
// Go
f.Write(data)
f.Sync()
// Rust
file.write_all(data)?;
file.sync_all()?;

Applications requiring immediate durability must call the appropriate synchronization function.

Verified fsync

A successful fsync is verified: it means every write the descriptor acknowledged before it is durable on object storage. Acknowledged file data lives in the in-RAM open segment buffer, and its metadata in the memtable, until the next flush, so a crash before that flush loses them. ZeroFS does not let that loss pass silently. A fsync issued after a restart, over writes made before it, returns a stale-handle error (ESTALE) rather than a false success.

Each acknowledged write is tagged with a durability lineage token naming the current unbroken durable lineage. A restart reopens the database and, with no way to prove it inherited the un-flushed buffers, regenerates the token. A fsync presents the token its writes were made under; if the database has since regenerated, the tokens differ and the fsync fails. The first fsync after a write is the durability boundary, so a failure reports real loss: redo the writes and fsync again.

This holds on a single node and under replication alike. With a replicated standby a clean failover can prove it inherited the writes (the standby receives un-flushed extent frames over replication and materializes their segments at takeover) and keeps the token, so the fsync succeeds transparently; the failover cases are diagrammed in High availability. The check is per descriptor and matches POSIX: it covers the whole file across every open handle, speaks for no other file, and verifies data, metadata, directory entries, and rename.

Eliminating the Durability Window

By default, writes between fsync calls live in the open segment buffer and memtable and become durable only when the next periodic flush or explicit fsync persists them. Committed-but-unflushed file data is bounded by the 256 MiB open buffer plus up to 4 × 256 MiB of in-flight segment uploads. For workloads that need every write durable on return, set sync_writes = true in the [lsm] section.

When enabled, the commit coordinator forces a flush after every coalesced write batch before returning success to the caller. Concurrent writes still merge into a single batch and a single flush.

The trade-off is per-operation latency. The WAL is permanently off, so each batch waits for the full durability barrier: the open segment is sealed and uploaded, then the memtable flushes to an L0 SST. No cheaper WAL-append path exists. This is expensive for chatty workloads.

[lsm]
sync_writes = true

Protocol Considerations

The durability guarantees available to applications depend on the access protocol.

9P Protocol

The 9P protocol provides direct mapping of POSIX synchronization semantics:

  • fsync() issues a durability-verified flush (Tfsyncdur, a ZeroFS 9P extension negotiated by zerofs mount and the client libraries)
  • ZeroFS flushes all buffered data to object storage, then verifies the writes' lineage; the call returns success only when durability is confirmed, otherwise ESTALE (see Verified fsync)
  • Plain 9P2000.L clients (e.g. the Linux kernel v9fs client) issue Tfsync: a durable but unverified flush. With ignore_fsync, both standard and durability-verified fsync requests return without forcing a flush; selecting that option explicitly opts out of the guarantee.

NFS Protocol

NFS client implementations do not reliably invoke the COMMIT operation:

  • Clients typically report writes as stable without issuing COMMIT
  • ZeroFS accepts this to avoid per-write latency penalties
  • Effective durability depends on client behavior

For workloads requiring predictable durability semantics, the 9P protocol is recommended.

Verification Through Crash Testing

ZeroFS verifies its consistency guarantees through systematic crash simulation using failpoints injected throughout the data path.

Failpoint Coverage

Failpoints are placed at critical points within each filesystem operation, allowing tests to simulate crashes at any stage:

OperationFailpoints
writeafter extent write, after inode update, after commit
fallocateafter extent edits, after inode update, after commit
createafter inode allocation, after directory entry, after commit
removeafter inode delete, after tombstone, after directory unlink, after commit
renameafter target delete, after source unlink, after new entry, after commit
mkdirafter inode allocation, after directory entry, after commit
truncateafter extent deletion, after inode update, after commit
linkafter directory entry, after inode update, after commit
symlinkafter inode allocation, after directory entry, after commit
rmdirafter inode delete, after directory cleanup
gcafter extent delete, after tombstone update
flushafter segment seal, before manifest flush
segment compactionafter packed-segment seal, before repoint; between per-inode repoints
segment reclaimafter barrier, before scan; after verify, before delete; after segment delete, before counter drop
sealforced open-segment seal error

Fallocate exposes these boundaries as fallocate_after_extents, fallocate_after_inode, and fallocate_after_commit. Its focused atomicity test verifies that aborting at either pre-commit point discards the staged extent and inode changes, while aborting after commit leaves the complete range operation visible.

The crash-harness failpoints abort the operation at the injection point; the filesystem instance is torn down (discarding all in-memory state) and restarted from object storage, simulating a crash at that exact point. The seal failpoint injects an error return instead, verifying that a failed seal leaves the open buffer intact for retry rather than committing dangling pointers.

Consistency Verification

After each simulated crash, a consistency checker validates the filesystem state:

Consistency Checks

verify_all()
  enumerate_inodes()
  enumerate_tombstones()
  enumerate_orphans()
  walk_directory_tree()
  verify_directory_counts()
  verify_nlink_counts()
  verify_directory_nlinks()
  find_orphaned_inodes()
  verify_orphan_set_drained()
  verify_stats_counters()
  verify_tombstones()
  verify_file_extents()
  verify_inode_counter()
  verify_orphaned_extents()
  verify_dir_entry_scan_consistency()
  verify_orphaned_directory_metadata()
  verify_dir_cookie_counters()

The checker detects: dangling references, orphaned inodes, nlink mismatches, missing extents, stale tombstones, counter inconsistencies, and directory entry corruption.

Test Methodology

For each failpoint, the test suite:

  1. Performs a filesystem operation with the failpoint enabled
  2. Aborts the operation at the injection point and tears down the filesystem instance, discarding all in-memory state
  3. Restarts the filesystem from object storage
  4. Runs the full consistency checker
  5. Verifies either complete rollback or complete commit, never partial state

This verifies that the atomicity and ordering guarantees hold at each injected crash point.

Deterministic Simulation Testing

The data path, namespace mutations, segment garbage collection, compaction, and crash recovery also run under deterministic simulation. A simulated world executes on a single-threaded runtime with virtual time: every object-store operation is assigned a pseudo-random latency from a seeded generator, and those latencies decide the order in which concurrent tasks wake. One seed defines one exact schedule, hours of simulated activity execute in seconds, and the same seed replays the same run. Seeds are drawn fresh from OS entropy on every run and printed, so repeated runs accumulate coverage and any failure is reproducible from its printed seed. A companion test replays one seed and compares the two runs' complete execution traces, so replay determinism is itself under test.

Each world races ordinary file writers (writes, truncates, hole punches, verified reads, whole-file scans, and fsyncs), two disjoint-region writers on one shared inode, a namespace actor, and live garbage-collection passes. The namespace actor creates and removes files and directories, mutates file content, renames and overwrites entries, creates hardlinks, symlinks, and special nodes, retries idempotent creates, and exercises open-unlink reclamation. The world runs over an in-memory object store in the production durability configuration and behind the production retry layer. Some worlds also inject seeded transient store faults, including puts and deletes whose effect lands but whose response is lost, so retries collide with their own already-applied writes. At a seeded instant the world crashes mid-flight. Every task is cancelled at whatever await point it happens to occupy, the store rejects all in-flight I/O (a dead process completes no writes), and a fresh instance recovers from the surviving objects.

At every quiesced point and after every crash, the state must satisfy a model-based oracle:

  1. Each ordinary file and shared-inode region equals the replay of an operation prefix no older than the last acknowledged fsync. One in-flight operation of unknown fate is permitted per actor at the cut, and the shared file's durable size is fixed.
  2. The namespace tree is structurally isomorphic to a prefix of its operation log: paths, node kinds and values, file contents, and hardlink identity must all match.
  3. Every referenced extent still resolves: the filesystem is re-read in full, so a dangling frame pointer or a wrongly deleted segment surfaces immediately. Modeled data files also require a key for every nonzero extent and reject keys beyond EOF.
  4. Segment accounting reconciles through typed snapshots: each counter's live bytes equal the byte sum of the extent pointers referencing it, live bytes never exceed appended bytes, and every referenced segment has a counter.
  5. All incremental footprint gauges (segment count, appended bytes, live bytes, and reclaimable bytes) equal an authoritative scan.
  6. The full consistency checker passes, including directory and link invariants, extent readability and EOF bounds, orphan metadata, inode and directory-cookie counters, and the global stats behind statfs.

Garbage-collection passes run with seeded per-pass tuning so the rarely-taken paths execute too: checkpoint-pinned passes, the write-cold gates (tail scrub, chain compaction), small round budgets that exercise the gather caps, and multi-batch drains.

A companion mode composes the simulation with failpoints. Some windows are too narrow for a timer to land in (a read-decide-act gap with no await inside it), so each round can arm a failpoint that crashes the world at that exact code point, and can widen such a gap by a few virtual milliseconds so concurrent commits land inside it before the code acts. This combination reproduces historical bugs whose window is the space between a garbage-collection decision and the irreversible action it drives. The failpoint suite remains alongside as the deterministic pointwise guard for each window.

CI runs the simulation as a dedicated test target with the required SlateDB and Tokio configuration flags. A scheduled job additionally soaks fresh seeds for 50 minutes every hour.

Running the simulation

export DST_RUSTFLAGS="--cfg dst --cfg tokio_unstable --cfg io_uring_skip_arch_check"
RUSTFLAGS="$DST_RUSTFLAGS" cargo test --test dst

# A timed soak: fresh random seeds, fanned across all cores, until the
# budget elapses. Scale depth with DST_ROUNDS / DST_OPS / DST_CRASH_PCT.
DST_WALL_CLOCK_SECS=3600 RUSTFLAGS="$DST_RUSTFLAGS" \
  cargo test --test dst -- --nocapture seeds

# Failpoint-placed crashes and widened windows
RUSTFLAGS="$DST_RUSTFLAGS" \
  cargo test --features failpoints --test dst crash_points

# Reproduce a failure from its printed seed
DST_SEEDS=16039913183294875603 RUSTFLAGS="$DST_RUSTFLAGS" \
  cargo test --test dst -- --nocapture

Continuous Integration Testing

In addition to crash simulation, CI runs pjdfstest, stress-ng, Linux kernel builds, Jepsen's local-fs suite, and a ZFS integration test on every commit.

POSIX Compliance

pjdfstest: A POSIX filesystem compliance test suite originally developed for FreeBSD. Tests cover file creation, permissions, hard links, symbolic links, timestamps, and other POSIX semantics. 8,662 cases run once per protocol: NFS, 9P, and FUSE. A few cases per protocol are excluded; the exclude lists are public in the repo.

Stress Testing

stress-ng: Exercises filesystem operations under concurrent load, including directory operations, file metadata, links, renames, and attribute modifications. Tests run over NFS, 9P, and FUSE.

Linux Kernel Compilation: Compiles the Linux kernel source tree on ZeroFS, exercising parallel file creation, compilation, and linking across thousands of files.

Model-Based Testing

Jepsen (local-fs): Generates random sequences of filesystem operations and checks each result against a reference model, shrinking any divergence to a minimal failing case. It runs over a 9P mount. A crash mode injects the loss of un-fsynced writes: it kills the server mid-run, dropping the in-memory open segment buffer and memtable, then recovers from the object store and verifies the surviving state is consistent with the last fsync.

Layered Filesystem Testing

The CI suite includes a test that creates a ZFS pool on a ZeroFS NBD block device:

  1. Create a 3GB block device file via 9P
  2. Connect via NBD and create a ZFS pool
  3. Extract the Linux kernel source (~80,000 files)
  4. Compute checksums of all files
  5. Export the ZFS pool and restart ZeroFS
  6. Reimport the pool and verify all checksums match

This test validates data integrity through multiple filesystem layers and across process restarts.

Test Matrix

Test SuiteProtocolCoverage
Unit testsn/aCore filesystem logic
Failpoint crash testsn/aCrash consistency at each operation stage
Deterministic simulationn/aData and namespace models, GC/compaction schedules, accounting invariants, and mid-flight crash recovery with exact replay
Jepsen (local-fs)9PModel-based correctness, crash consistency
pjdfstestNFS, 9P, FUSEPOSIX compliance
stress-ngNFS, 9P, FUSEConcurrent operations under load
Kernel compilationNFS, 9P, FUSEParallel build workload
ZFS integrationNBD + 9PBlock device integrity across restarts

All tests run on every pull request and merge to the main branch.

Conditional Writes and Fencing

ZeroFS uses conditional writes (put-if-not-exists) for fencing to prevent split-brain scenarios when multiple instances access the same storage backend. This ensures that only one writer can be active at a time.

AWS S3, Azure Blob Storage, and Google Cloud Storage support conditional writes natively. For S3-compatible object stores that do not support conditional puts, ZeroFS can use Redis as a coordination backend:

[aws]
conditional_put = "redis://localhost:6379"

When configured, ZeroFS uses Redis to coordinate conditional write operations, providing the same fencing guarantees as native conditional put support. See the Configuration Guide for details.

Backend Durability

ZeroFS delegates storage durability to the object storage backend.

BackendDesigned DurabilityReplication
Amazon S399.999999999%Automatic across availability zones
Azure Blob Storage99.999999999%Configurable geo-redundancy
Google Cloud Storage99.999999999%Configurable multi-region

These services maintain multiple replicas across independent failure domains. Once data is persisted to object storage, it is protected against hardware failures, facility outages, and other localized events without additional configuration.

Summary

ZeroFS provides the following durability and consistency guarantees:

  • Name
    Write Atomicity
    Description

    Filesystem operations are atomic. Each operation either completes fully or has no effect. Partial or torn writes are not possible.

  • Name
    Total Ordering
    Description

    All write operations flow through a single-writer commit path that defines a total order over batches. This order is preserved across crashes. If operation B is visible after recovery, all operations that completed before B started are also visible.

  • Name
    Immediate Recovery
    Description

    No recovery procedure is required after unexpected termination. The filesystem resumes operation by reading the current manifest from object storage.

  • Name
    POSIX Durability
    Description

    Data is persisted to stable storage upon fsync() invocation, consistent with POSIX semantics and the behavior of other filesystems.

  • Name
    Backend Replication
    Description

    Persisted data inherits the durability properties of the object storage backend, typically providing automatic replication across multiple failure domains.

Was this page helpful?