// SchFS — on-disk format. // // Design rationale lives in docs/architecture/schfs.md. In short: copy-on-write // with the checksum of a block stored in the pointer that leads to it (ZFS), // one B-tree reused for every kind of record (btrfs), and attribute indexes as // just another record type (BeFS). // // This header is shared by the kernel and by host tools (tools/mkfs, // tools/schfs_test), so it must stay free of kernel-only types. #ifndef SCHFS_H #define SCHFS_H #include #define SCHFS_MAGIC 0x53636846u // "SchF" #define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod" // 4096 to match modern media and make extents worth having. The old format // used 1024, which is also why its directories were so wasteful. #define SCHFS_BLKSIZE 4096 // Deepest tree we will walk without recursing. 8 levels of a ~84-way internal // node is far more than any plausible volume needs. #define SCHFS_MAX_LEVEL 8 // --------------------------------------------------------------------------- // Keys // // Every record in the filesystem is addressed by one 24-byte key, sorted by // (objectid, type, offset). This is the whole reason there is only one tree: // inodes, directory entries, extent maps and indexes are just different `type` // values in the same keyspace. // --------------------------------------------------------------------------- #define SCHFS_KEY_INODE 1 // (ino, INODE, 0) -> struct schfs_inode #define SCHFS_KEY_DIRENT 2 // (dirino,DIRENT, namehash) -> packed dirent list #define SCHFS_KEY_EXTENT 3 // (ino, EXTENT, fileblk) -> struct schfs_extent #define SCHFS_KEY_ATTR 4 // (ino, ATTR, namehash) -> attribute value #define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid #define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c #define SCHFS_KEY_SNAP 7 // (SNAP, SNAP, id) -> struct schfs_snap // Data-block checksums live under one reserved object id, exactly as btrfs // keeps a checksum tree. Tree nodes carry their checksum in their parent // pointer, but file *data* blocks have no parent to hold it — without these // records "end-to-end checksums" would only cover metadata, which is the part // least likely to rot. #define SCHFS_CSUM_OBJECTID 1ULL // Snapshots live under their own reserved objectid, as ordinary records in the // live tree. A snapshot is nothing more than a tree root that is kept: the // B-tree is copy-on-write, so the blocks a root reaches are already immutable // as long as nothing frees them. Keeping one costs a record. #define SCHFS_SNAP_OBJECTID 2ULL // Object ids below this are reserved; the root directory is the first real one. #define SCHFS_FIRST_OBJECTID 16ULL #define SCHFS_ROOT_OBJECTID 16ULL struct schfs_key { uint64_t objectid; uint64_t offset; uint8_t type; uint8_t pad[7]; }; /* 24 bytes */ // --------------------------------------------------------------------------- // Node header — identical for internal nodes and leaves. // // `blocknr` is a self-pointer: a block that was written to the wrong place // still checksums correctly, so the only way to catch a misdirected write is // for the block to say where it thinks it lives. // --------------------------------------------------------------------------- struct schfs_node_hdr { uint32_t magic; // SCHFS_NODE_MAGIC uint32_t crc; // CRC32c of the whole block, this field zeroed uint64_t blocknr; // where this block believes it lives uint64_t generation; // transaction that wrote it uint16_t level; // 0 = leaf, >0 = internal uint16_t nritems; uint32_t pad; }; /* 32 bytes */ // Internal node entry: a key and the child it leads to, plus that child's // checksum and generation. Storing the checksum *here* rather than in the // child is what makes the tree self-validating. struct schfs_kptr { struct schfs_key key; uint64_t blocknr; uint64_t generation; uint32_t crc; uint32_t pad; }; /* 48 bytes */ // Leaf entry. Items grow forward from the end of the header; their data grows // backward from the end of the block, exactly like btrfs. `offset` is measured // from the start of the block. struct schfs_item { struct schfs_key key; uint32_t offset; uint32_t size; }; /* 32 bytes */ #define SCHFS_NODE_SPACE (SCHFS_BLKSIZE - (int)sizeof(struct schfs_node_hdr)) #define SCHFS_MAX_KPTR (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_kptr)) #define SCHFS_MAX_ITEMS (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_item)) // Largest value that can live inline in a leaf. Anything bigger becomes an // extent record pointing at data blocks. #define SCHFS_MAX_INLINE (SCHFS_NODE_SPACE / 4) // --------------------------------------------------------------------------- // Uberblock // // A ring of these is written round-robin. Mounting means reading them all and // taking the highest generation that checksums — that is the entire crash // recovery story, in place of a journal replay. // --------------------------------------------------------------------------- #define SCHFS_UBER_COUNT 128 #define SCHFS_UBER_START 1 // block 0 stays free for a boot sector // First block available for the tree and for file data: everything below is // the boot block and the uberblock ring. #define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT) #define SCHFS_VERSION 1 #define SCHFS_LABEL_MAX 32 // A retained tree root. // // The root a snapshot names is a *previous* version of the live tree, and it is // reached exactly as the live one is -- same node format, same checksums. What // makes it survive is schfs_space_scan walking it too, so its blocks are never // handed out again. // // The record is stored in the live tree, which means creating one rewrites the // path to it under copy-on-write and produces a new live root. That is correct // and is why the snapshot names the root as it was *before* the record was // added: a snapshot that included its own creation would have to refer to a // block that does not exist yet. #define SCHFS_SNAP_NAME_MAX 64 // Set on a volume that has at least one snapshot. See schfs_space_free. #define SCHFS_SPACE_RETAIN_ALL 1 struct schfs_snap { uint64_t root; // block number of the retained tree root uint64_t root_generation; uint32_t root_crc; // checksum of that root block uint32_t pad0; uint64_t created; // generation the snapshot was taken at char name[SCHFS_SNAP_NAME_MAX]; }; struct schfs_uberblock { uint32_t magic; // SCHFS_MAGIC uint32_t crc; // CRC32c of this struct, this field zeroed uint32_t version; // SCHFS_VERSION uint32_t blocksize; // SCHFS_BLKSIZE; a reader must refuse a mismatch uint64_t generation; // transaction number; highest valid one wins uint64_t root; // block number of the tree root uint32_t root_crc; // checksum of that root block uint32_t pad0; uint64_t root_generation; uint64_t total_blocks; // Informational only. Which blocks are actually free is derived from the // tree at mount rather than stored, so this is what the volume believed at // commit time — useful for df before anything is mounted, never trusted. uint64_t free_blocks; uint64_t reserved0; uint64_t next_objectid; uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID uint8_t uuid[16]; char label[SCHFS_LABEL_MAX]; uint8_t pad[SCHFS_BLKSIZE - 136]; }; // --------------------------------------------------------------------------- // Records // --------------------------------------------------------------------------- #define SCHFS_T_FILE 1 #define SCHFS_T_DIR 2 #define SCHFS_T_DEV 3 #define SCHFS_T_SYMLINK 4 struct schfs_inode { uint64_t size; uint64_t blocks; uint64_t atime, mtime, ctime; // Containing directory. Meaningful for directories only, where it is what // ".." resolves to — neither "." nor ".." is stored on disk, so a directory // costs nothing until it has real entries. For files with several hard // links "parent" is simply the first directory that linked them and must // not be trusted. uint64_t parent; uint32_t mode; // real POSIX mode bits, at last uint32_t uid, gid; uint32_t nlink; uint16_t type; // SCHFS_T_* uint16_t rdev_major, rdev_minor; uint16_t pad; uint32_t flags; // SCHFS_I_* — none defined yet uint32_t gen; // transaction that last changed this inode; // what an incremental send/snapshot diff needs }; // An extent maps a run of file blocks onto a run of disk blocks. refs is what // makes snapshots and reflink copies nearly free: sharing a range is a refcount // bump rather than a data copy. struct schfs_extent { uint64_t disk_block; uint64_t nblocks; // Transaction group that allocated this run. A run stamped with the group // currently being built cannot be reached from any committed root, so its // blocks may be overwritten in place — which is what stops a program that // writes a hundred bytes at a time from copying a whole 4 KB block, and // then a whole tree path, on every call. // // A full 64 bits, matching the tree's generation counter, because a 32-bit // one would wrap after four billion commits and a wrapped comparison says // "same group" about a block belonging to a committed tree — the one // mistake this field exists to prevent. Only ever trusted together with // refs == 1; a shared run belongs to somebody else as well. uint64_t gen; uint32_t refs; uint32_t pad; }; // Directory entry payload. The key's offset field holds the name hash, so a // lookup is one tree descent rather than a directory scan. // // Two names in one directory can hash the same, and the key would then be // identical — the second insert would silently replace the first. So the value // is a *packed list*: one schfs_dirent_rec plus its name bytes, repeated. A // 64-bit hash makes lists longer than one vanishingly rare, but "vanishingly // rare" is not "impossible", and a filesystem that loses a file under a hash // collision is not one to build on. struct schfs_dirent_rec { uint64_t objectid; uint16_t type; uint16_t namelen; uint32_t pad; /* name bytes follow, namelen of them, not NUL-terminated */ }; #define SCHFS_NAME_MAX 255 // Step from one packed dirent to the next. Records are 8-byte aligned so the // uint64 objectid of the next one is naturally aligned. #define SCHFS_DIRENT_LEN(namelen) \ ((unsigned)((sizeof(struct schfs_dirent_rec) + (namelen) + 7) & ~7u)) // --------------------------------------------------------------------------- // Compile-time layout checks. // // Every one of these is a number that ends up baked into images on disk. A // silent change to any of them — adding a field, changing a type, a compiler // padding a struct differently — makes every existing image unreadable in a way // that looks like corruption rather than like a mistake. Failing the build is // enormously cheaper than debugging that. // --------------------------------------------------------------------------- #define SCHFS_ASSERT_SIZE(t, n) \ typedef char schfs_sizecheck_##t[(sizeof(struct t) == (n)) ? 1 : -1] SCHFS_ASSERT_SIZE(schfs_key, 24); SCHFS_ASSERT_SIZE(schfs_node_hdr, 32); SCHFS_ASSERT_SIZE(schfs_kptr, 48); SCHFS_ASSERT_SIZE(schfs_item, 32); SCHFS_ASSERT_SIZE(schfs_uberblock, SCHFS_BLKSIZE); SCHFS_ASSERT_SIZE(schfs_inode, 80); SCHFS_ASSERT_SIZE(schfs_extent, 32); SCHFS_ASSERT_SIZE(schfs_dirent_rec, 16); #endif /* SCHFS_H */