cetrafs
ReferenceInformation-oriented

On-disk format

The cetrafs on-disk layout: superblock, Zone and Block Group descriptors, Node Registry, journals, inodes, extents, directory entries and feature flags.

The authoritative specification is include/cetrafs.h in the source tree. The kernel module and the userspace tools both compile against it. This page summarises it for format version 0x0006.

Conventions

  • Block size is fixed at 4096 bytes. All offsets are block numbers (LBAs).
  • Every multi-byte field is little-endian.
  • Structures are byte-packed, with no padding added by the compiler.
  • Checksums are CRC32C, computed as ~crc32c(~0, buf) over the structure with its own checksum field zeroed, and stored as a little-endian 64-bit value.
  • A record that nodes update by compare-and-write fits entirely in the first 512 bytes (CETRAFS_HOT_SECTOR) of its block.
  • Several structures carry zero-filled reserved space. New fields have been taken from it without a format change, because zero reads as the correct default.

Layout

Z = Zone Group count, B = Block Group count, N = Node Registry capacity.

LBA 0                 Superblock
LBA 1 .. Z            Zone Group table          (1 block per Zone Group)
LBA 1+Z .. Z+B        Block Group table         (1 block per Block Group)
...                   Global inode bitmap
...                   Global inode table        (1 inode per block)
...                   Node Registry             (128-byte rows, packed)
...                   Pending-free queues       (fixed slots per Zone Group)
...                   Per-node journals         (N slices)
bg_regions_start      Block Group regions 1 .. B

Every region start is a superblock field. Mount checks the whole map: every region starts after block 0, nothing wraps, everything fits inside total_blocks, no regions overlap, and every count fits its region.

Superblock (block 0, 424 bytes)

FieldMeaning
magic0xC4553770
versionFormat version (0x0006). Can change between beta releases; never migrated.
sb_bytesSize of the superblock structure, as a sanity check
block_size4096
total_blocksDevice size in blocks
zone_group_count, block_group_countZ, B
zg_table_start, bg_table_startDescriptor tables
inode_bmap_start/_blocks, inode_table_start/_blocksGlobal inode bitmap and table
bg_regions_startFirst Block Group region
root_inoRoot directory inode (1)
inode_size, inodes_countOn-disk inode size; total inode slots
bg_size_blocksTarget Block Group span
cluster_idCluster identifier
node_table_start/_blocks/_count, node_entry_sizeNode Registry location and capacity
pending_free_start, pending_free_blocks_per_zgPending-free queues
node_journal_start, node_journal_blocksPer-node journal area and size per node
features_compat/_incompat/_ro_compatFeature bitmaps
checksumCRC32C
uuidFilesystem UUID

Zone Group descriptor (one block each)

The hot sector (first 512 bytes, changed by compare-and-write) holds zg_id, owner_node_id (registry row, or 0xFFFF for none), free_data_blocks (the sum of its Block Groups’ free counts), hb_seq and hot_checksum. The cold part, fixed at mkfs time, holds first_bg_id, bg_count and cold_checksum.

Block Group descriptor and region

The descriptor holds bg_id, owner_zg, start_block, length_blocks, the offsets of the header, bitmap and data within the region, data_block_count, the cached free_data_blocks, checksum, and reconcile_seq. reconcile_seq advances whenever a change of Zone Group ownership rewrites the free count from the bitmap, so a late update from the previous owner is detected and dropped.

Each region:

| header (1 block) | data bitmap (dbm blocks) | data blocks |

where dbm = ceil((length_blocks − 1) / 32769). A 1 GiB Block Group (262,144 blocks) has 8 bitmap blocks and 262,135 data blocks.

Node Registry row (128 bytes)

FieldMeaning
node_uuid[16]Stable machine UUID
stateFREE (0) or REGISTERED (1)
hb_seqHeartbeat counter
last_seenUnix seconds of the last heartbeat (backdated on a clean unmount)
label[32]Hostname, informational
checksumCRC32C
fence_epochThe node’s lease. Incremented by whoever takes over its resources; the node fences itself when it changes.
hb_interval_secsThe node’s own heartbeat rate

Per-node journal

Slice node_journal_start + row × node_journal_blocks belongs to registry row row. It holds a flag block (state CLEAN or DIRTY; replay_owner, claimed by compare-and-write) followed by two-block slots. A slot is a header block (magic, txn_id, target_lba, payload_checksum, the target’s checksum before the change, flags such as F_FRESH, header_checksum) followed by a payload block holding the complete new contents of the target.

Pending-free queue entry (16 bytes)

{lba, count, magic}: an extent freed by a node that doesn’t own the Zone Group, waiting for the owner to apply it.

Inode (one per 4 KiB block)

FieldMeaning
ino, mode, uid, gid, nlink, size, blocks, atime, mtime, ctimeStandard
flagsEXTENTS (always), XATTR
extent rootHeader plus 4 inline extents (depth 0) or 4 index entries (depth 1–2)
xattr_blockExternal xattr block, or 0
lock_owner_node, lock_modeWhole-file exclusive lock
ns_lock_ownerPer-inode namespace lock (0xFFFF when free)
read_open_nodes64-bit read-presence mask, one bit per registry row
dirent_seqDirectories: incremented on every directory-block change
generationStamped at allocation; detects inode-number reuse
inode_checksumCRC32C

Extents

extent header   12 bytes: magic, entries, max, depth, generation
extent          16 bytes: logical block (32-bit), length (31-bit + uninitialized flag), physical block
extent index    12 bytes: logical block, physical block of child

The tree is at most depth 2: the inode’s 4 inline slots, then interior blocks, then leaf blocks. Arrays at every depth are append-only and unordered, so growing a file always edits a single block, and readers handle any order. Leaf and interior blocks end with a checksummed {magic, crc} tail. An extent marked uninitialized was preallocated and reads as zeroes until it is written.

Directory entry (266 bytes, fixed size)

inode (u64) + name_len (u16) + file_type (u8) + name[255]. 15 entries fit in a block, and each block ends with a checksummed {magic, crc} tail. Directories use the same extent machinery as files, up to depth 1.

Extended attribute entry (322 bytes)

key[64] + value_len (u16) + value[256], packed into one block per inode (about 12 entries) with a checksummed tail. key holds the full name including its namespace; an empty key marks a free slot.

Feature flags

FlagBitSet by mkfs
INCOMPAT_EXTENTS0yes
INCOMPAT_GLOBAL_ITBL1yes
INCOMPAT_BIT2_UNUSED2no (formerly the SCSI PR backend)
INCOMPAT_NODE_REGISTRY3yes
ROCOMPAT_SPARSE0no

An unknown incompatible bit prevents the mount. An unknown read-only-compatible bit forces a read-only mount. fsck reports unknown bits by name.

Last updated