← Slim64FS

slim64fs On-Disk Format Specification

Version: V9 (format version 0x00090002) Status: LOCKED — no field may be added, removed, or reinterpreted without a version bump and a new format.html revision. Endianness: All multi-byte integers are little-endian on disk. Block size: 4096 bytes (fixed; not configurable post-format).


1. Invariants

These constraints are enforced by _Static_assert in source. Any toolchain that cannot satisfy them must not build slim64fs.

_Static_assert(S64_DISK_SUPER_SIZE  == 128u, "superblock wire size changed — format version bump required");   // s64_disk.h
_Static_assert(S64_DISK_COMMIT_SIZE == 100u, "commit wire size changed — format version bump required");       // s64_disk.h
_Static_assert(S64_BLOCK_DATA_SIZE  == 4088u, ...);                                                            // s64_fs.h
_Static_assert(S64_INODE_SIZE == 128u, ...);                                                                   // s64_fs.h
_Static_assert(sizeof(s64_disk_inode) == S64_INODE_SIZE, ...);                                                 // s64_fs.h
_Static_assert(offsetof(s64_disk_dirent, name) == S64_DIRENT_HDR_SIZE, ...);                                   // s64_disk.h
_Static_assert(S64_DIRLOG_BLOCK_HDR_SIZE == 16u, ...);                                                         // s64_dirlog.h
_Static_assert(S64_DIRLOG_REC_HDR_SIZE   == 44u, ...);                                                         // s64_dirlog.h
_Static_assert(sizeof(s64_txn_header) == 64u, ...);                                                            // s64_txn.h

All integers are encoded/decoded exclusively through s64_le{16,32,64}_{get,put}. No struct overlay on disk bytes is permitted, with one exception: the transaction block (src/s64_txn.c) uses memcpy directly for encode/decode, which is correct on little-endian targets where struct layout matches wire layout but is not portable to big-endian.


2. Image Layout

Blocks are numbered from 0. The layout below is computed by s64_fs_compute_layout() and must match exactly. Fields in the superblock are the authoritative source of truth at runtime.

Structural ordering (all counts are image-size-dependent; see superblock fields):

Block 0                                      : Superblock
Block 1 .. bitmap_blocks                     : Allocation bitmap (primary)
Block 1+bitmap_blocks .. commit_block_a − 1  : Inode table (primary)
Block commit_block_a                         : Commit block A
Block commit_block_b  (= commit_block_a + 1) : Commit block B
Block txn_block       (= commit_block_b + 1) : Transaction block (see §5)
Block shadow_bitmap_start                    : Shadow allocation bitmap
Block shadow_inode_start .. alloc_start − 1  : Shadow inode table
Block alloc_start ..                         : Data allocation region

Concrete example for a 128 MiB image (32768 blocks; bitmap_blocks=1, inode_table_blocks=128, inode_count=4096):

Block 0          : Superblock
Block 1          : Allocation bitmap (primary)
Block 2..129     : Inode table (primary) — 128 blocks × 32 inodes/block = 4096 inodes
Block 130        : Commit block A
Block 131        : Commit block B
Block 132        : Transaction block
Block 133        : Shadow allocation bitmap
Block 134..261   : Shadow inode table — mirrors primary layout
Block 262+       : Data allocation region (alloc_start = 262, alloc_len = 32506)

For larger images, bitmap_blocks and inode_table_blocks grow (e.g. a 4 GiB image has bitmap_blocks=32, inode_table_blocks=2048, alloc_start=4164). All structural block numbers are computed by s64_fs_compute_layout() at format and mount time; the superblock stores them as the authoritative runtime values.

Root inode is always inode 1 (root_ino = 1). Inode 0 is reserved and never allocated.


3. Superblock (Block 0)

Wire size: 128 bytes. CRC covers bytes 0–127 with sb_crc32c zeroed during computation.

Offset Size Field Description
0 8 magic 0x534C363446533100 ("SL64FS1\0" LE)
8 4 version Format version; V9 = 0x00090002
12 4 block_size Must equal 4096
16 8 total_blocks Total blocks in image
24 8 root_ino Root directory inode number (always 1)
32 8 alloc_start First block of data allocation region
40 8 alloc_len Number of allocatable data blocks
48 4 generation Checkpoint counter; incremented by s64_fs_checkpoint(). Initialized to 0 on format.
52 4 sb_crc32c CRC32C of bytes 0–127 (this field zeroed during computation)
56 8 bitmap_start Block number of primary allocation bitmap
64 8 bitmap_blocks Number of bitmap blocks (variable; computed by s64_fs_compute_layout())
72 8 inode_table_start Block number of first primary inode table block
80 8 inode_table_blocks Number of inode table blocks (variable; minimum 128 for ≥ 4096 inodes; computed by s64_fs_compute_layout())
88 8 inode_count Total inode slots provisioned (computed as volume_bytes / 65536, minimum 4096)
96 8 shadow_bitmap_start Block number of shadow allocation bitmap
104 8 shadow_inode_start Block number of first shadow inode table block
112 8 reserved[0..7] commit_block_b (LE u64); 0 = legacy single-slot image
120 1 reserved[8] Advisory active commit slot hint: 0=A, 1=B (tie-break only)
121 7 reserved[9..15] Reserved; must be zero on write; ignored on read

Validation rules (enforced by s64_fs_open()):

Note: block_size and root_ino are not validated by the open path; they are validated by slim64fs-check (fsck) as BAD_SUPER_BLOCK_SIZE and BAD_SUPER_ROOT_INO.


4. Commit Block (A and B)

Wire size: 100 bytes. The filesystem maintains two commit slots (A and B) for atomic checkpoint. Active slot is selected by s64_fs_select_active_commit() using generation number, falling back to the advisory hint.

CRC covers bytes 0–99 plus 4 trailing zero bytes (104 bytes total) with commit_crc32c zeroed during computation. The 4 zero bytes are an enshrined legacy artifact: the original implementation CRC'd the in-memory struct, whose tail padding extended coverage to 104 bytes, and every existing image carries that value. Implementers MUST append 4 zero bytes to the 100-byte wire form before computing or verifying this CRC (see s64_commit_crc(), src/s64_fs.c).

Offset Size Field Description
0 8 magic 0x533634434D543100 ("S64CMT1\0" LE)
8 4 version Must equal S64_SUPER_VERSION
12 4 generation Monotonically increasing; higher generation = more recent
16 4 bitmap_crc32c CRC32C of primary allocation bitmap
20 4 inode_crc32c CRC32C of primary inode table
24 4 shadow_bitmap_crc32c CRC32C of shadow allocation bitmap
28 4 shadow_inode_crc32c CRC32C of shadow inode table
32 4 commit_crc32c CRC32C of bytes 0–99 + 4 zero bytes (104 total; this field zeroed during computation)
36 64 reserved Must be zero on write; ignored on read

Active slot selection: The slot with the higher valid generation wins. If both are invalid (bad CRC or magic), the filesystem is unrecoverable without fsck. If generations are equal, the advisory hint in superblock.reserved[8] breaks the tie.


5. Transaction Block

One block, at the fixed position commit_block_b + 1 (derived; not stored as a separate superblock field). Records the intent of a multi-step file-append extent operation so that incomplete operations can be identified on remount.

Wire size: 64 bytes used; block is 4096 bytes; remainder zeroed on write.

Offset Size Field Description
0 8 magic S64_TXN_MAGIC (0x533634545800); validates record
8 8 txn_id Monotonically increasing transaction ID; fs->sb.generation + 1 at write time
16 8 parent_checkpoint_gen Generation of the active commit slot when this txn was written
24 4 kind 1 = EXTEND (append new extents), 2 = REWRITE (full extent rewrite)
28 4 state 0 = PENDING, 1 = COMMITTED
32 4 crc32c CRC32C of bytes 0–63 (this field zeroed during computation)
36 28 reserved Must be zero on write; ignored on read

State machine (file-append path):

  1. PENDING written before extent allocation and data writes begin.
  2. COMMITTED written after s64_fs_write_inode() completes with the new extent map.
  3. Block zeroed unconditionally during s64_fs_checkpoint().

Recovery on open: The block is read and classified at every open. ZERO, COMMITTED, and INVALID all proceed without action. PENDING on writable open is cleared (zeroed) — treated as stale. Shadow restore (which runs earlier in the open path) has already reverted the bitmap and inode table to the last checkpoint state, so the partial operation is fully rewound; the cleared txn block leaves the filesystem ready for the next checkpoint. PENDING on read-only open is left in place.

_Static_assert guard: _Static_assert(sizeof(s64_txn_header) == 64u, "txn block wire size changed — format version bump required") — in src/s64_txn.h.


6. Inode (128 bytes)

32 inodes pack into one 4096-byte block (S64_INODES_PER_BLOCK = 32). Encoded with s64_inode_encode() / s64_inode_decode(). The struct is #pragma pack(1); no padding bytes exist between fields.

Offset Size Field Description
0 2 mode POSIX mode bits (S_IFREG, S_IFDIR, S_IFLNK, permission bits)
2 2 links Hard link count
4 4 uid Owner UID
8 4 gid Owner GID
12 8 size File size in bytes
20 8 first_block Block number of first data/extent block; 0 = empty file
28 8 atime_sec Last access time (seconds since Unix epoch)
36 8 mtime_sec Last modification time
44 8 ctime_sec Last status change time
52 76 reserved Type-specific overlay (see below); zero for unused inodes

reserved field ownership by inode type:


7. Data Block

Each 4096-byte data block has an 8-byte header:

Offset Size Field Description
0 4 crc32c CRC32C of all 4096 bytes with this field (bytes 0–3) zeroed during computation
4 4 reserved Must be zero on write
8 4088 payload User data or directory entries

Usable payload per block: 4088 bytes (S64_BLOCK_DATA_SIZE).


8. Directory Entry (dirent)

Directory data is stored as a packed sequence of variable-length records within data block payloads. Records are not block-aligned; they are byte-packed sequentially.

Header (12 bytes, S64_DIRENT_HDR_SIZE):

Offset Size Field Description
0 8 ino Target inode number
8 2 reclen Total record length including header and name+NUL
10 1 namelen Length of name, excluding NUL terminator
11 1 type Entry type: 1=regular, 2=directory, 3=symlink

Name: Immediately follows header. namelen bytes of name data followed by one NUL byte. Total record: 12 + namelen + 1 bytes.

Constraints:


9. Dirlog Block

The dirlog is a singly-linked chain of blocks recording directory mutation operations. The head block number is stored in the directory inode's reserved[0..7]. A zero head means no dirlog exists for this directory.

Block header (16 bytes, S64_DIRLOG_BLOCK_HDR_SIZE):

Offset Size Field Description
0 8 next Block number of next dirlog block; 0 = end of chain
8 2 used Bytes of record data written into this block's payload
10 6 reserved Must be zero

Payload begins at offset 16. Records are packed sequentially into bytes 16 through 16 + used − 1.

Dirlog record header (44 bytes, S64_DIRLOG_REC_HDR_SIZE):

Offset Size Field Description
0 8 magic 0x00533634444C474F (S64_DIRLOG_MAGIC); validates record boundary
8 8 seq Monotonically increasing sequence number within directory
16 8 parent_ino Inode number of the parent directory
24 8 child_ino Inode number of the child being operated on
32 4 op Operation: 1=CREATE, 2=DELETE, 3=RENAME_FROM, 4=RENAME_TO
36 2 name_len Length of name payload following this header
38 2 flags Reserved; must be zero
40 4 crc32c CRC32C of bytes 0–43 (this field zeroed) concatenated with name bytes

Name payload: name_len bytes immediately following the 44-byte header. No NUL terminator stored.

Total record size: 44 + name_len bytes.


10. Allocation Bitmap

bitmap_blocks consecutive blocks (variable; computed by s64_fs_compute_layout()) covering all block numbers 0 through total_blocks − 1. Bit N corresponds to absolute block N. Bit = 0 means free; bit = 1 means allocated. Blocks 0 through alloc_start − 1 (structural blocks) are pre-marked allocated at format time. Bits for block numbers ≥ alloc_start + alloc_len are also pre-marked allocated to prevent the allocator from issuing them.

The shadow bitmap is a byte-for-byte copy written before the primary during checkpoint, providing rollback capability.


11. Crash Windows and Ordering Constraints

The following write orderings are invariants. Violating them corrupts the image in ways that may not be fsck-recoverable.

Window Constraint
W1: Superblock update Write superblock last; never update in place during operation
W2: Commit slot write Write full commit block atomically; partial writes render the slot invalid
W3: Bitmap before inode On alloc: write bitmap (mark used) before writing inode. On free: write inode (clear reference) before bitmap (mark free)
W4: Checkpoint ordering Shadow structures written and fsync'd before commit block updated
W5: Dirlog head free Zero and flush inode's dirlog_head_block to disk before calling s64_fs_free_block on the dirlog head. A crash between these two writes leaks a block (fsck-visible); a reversed ordering produces a live dirent head pointing to a freed block (silent corruption)
W6: Dirlog append Write new dirlog block to disk before updating used count in the preceding block

12. Version History

Format Version Description
0x00080002 V8: Shadow bitmap/inode table; dual commit slots; deferred orphan reclamation
0x00090002 V9: Format locked; _Static_assert guards; this document

Images with version 0x00080002 can be migrated to V9 using s64-migrate-v8-to-v9. Images with any other version are rejected at open time.


13. fsck Findings and Severity

Four internal severity levels (S64CHK_WARNING, S64CHK_UNREACHABLE, S64CHK_CORRUPT, S64CHK_UNRECOVERABLE) are collapsed into exit codes by derive_exit_code():

Exit code Meaning
0 Clean — no findings
1 Warnings only (e.g. EMPTY_COMMIT_SLOT, BAD_COMMIT_HINT) — no structural corruption, image safe to mount
2 Structural issue: corrupt or unreachable blocks present, but not both simultaneously. Includes ORPHAN_INODE (severity UNREACHABLE) and hard corruption without orphans. Image should not be mounted until repaired.
3 Both corruption and unreachable blocks present — compound failure. Image must not be mounted.
4 Tool/usage failure: bad arguments, internal scan error, or mutation refused because out-of-scope corruption is present and --force was not passed.

Exit 1 is mount-safe. Exit 2+ requires repair before mounting. Exit 3 is the worst structural state short of tool failure.


This document is the authoritative reference for the slim64fs V9 on-disk format. The source of truth for field sizes is the encode/decode functions in src/s64_disk.c and src/s64_dirlog.c. In any conflict between this document and those functions, the functions govern and this document must be corrected.


See Also