// SPDX-License-Identifier: Apache-2.0 // // SchFS volume layer: the uberblock ring. // // A ring of SCHFS_UBER_COUNT slots is written round-robin, each stamped with a // transaction number and a checksum. Mounting reads every slot and adopts the // highest-numbered one that validates. // // That single rule replaces journalling entirely. A commit writes one block; it // either lands whole or it does not. If it does not, its checksum fails, the // previous slot is still the highest valid one, and the volume comes up at the // previous transaction with no replay and no fsck. Rotating through many slots // rather than overwriting one means a run of bad sectors under the current slot // cannot take the volume with it. #include "schfs_int.h" #ifdef SCHFS_HOSTED #include #else #include "types.h" #include "defs.h" #endif // Checksum an uberblock with its own crc field zeroed, the same convention the // tree nodes use. static uint32_t uber_crc(struct schfs_uberblock *u) { uint32_t saved = u->crc; u->crc = 0; uint32_t c = schfs_crc32c(0, u, sizeof(*u)); u->crc = saved; return c; } static int uber_valid(struct schfs_uberblock *u) { if (u->magic != SCHFS_MAGIC) return 0; if (u->version != SCHFS_VERSION) return 0; // A volume made with a different block size would otherwise be read with // every offset wrong, which looks exactly like corruption. if (u->blocksize != SCHFS_BLKSIZE) return 0; if (uber_crc(u) != u->crc) return 0; // Self-consistency: a root inside the ring or past the end of the device is // a corrupt uberblock that happens to checksum, not a usable one. if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks) return 0; return 1; } static void vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot) { v->tree.dev = &v->dev; v->tree.root = u->root; v->tree.root_crc = u->root_crc; v->tree.root_gen = u->root_generation; // The tree's `generation` is the transaction group being *built*, one past // the newest committed one. Setting it to the committed root's generation // instead would tell the tree that every block of the mounted filesystem // belongs to the open transaction, and it would then rewrite the live tree // in place — losing the very thing copy-on-write exists to preserve. v->tree.generation = u->generation + 1; v->total_blocks = u->total_blocks; v->free_blocks = u->free_blocks; v->next_objectid = u->next_objectid; v->root_objectid = u->root_objectid; v->generation = u->generation; v->uber_slot = slot; memmove(v->uuid, u->uuid, sizeof(v->uuid)); memmove(v->label, u->label, sizeof(v->label)); v->label[SCHFS_LABEL_MAX - 1] = '\0'; } int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, struct schfs_ctx *x, void *(*palloc)(void)) { struct schfs_uberblock *u = (struct schfs_uberblock *)x->a; int best_slot = -1; uint64_t best_gen = 0; int io_errors = 0; v->dev = *dev; v->ctx = *x; v->tree.dev = &v->dev; // Remember which slot won, not a copy of it. // // An uberblock is a whole SCHFS_BLKSIZE block, and keeping one as a local // put 4096 bytes on a kernel stack that is 4096 bytes in total — the canary // in kalloc'd stacks caught it as "kstack overflow" on the very first boot. // Re-reading the winner costs one block read, once, at mount. for (int i = 0; i < SCHFS_UBER_COUNT; i++) { if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) { io_errors++; continue; // an unreadable slot is not fatal: try the rest } if (!uber_valid(u)) continue; if (best_slot < 0 || u->generation > best_gen) { best_gen = u->generation; best_slot = i; } } if (best_slot < 0) return io_errors == SCHFS_UBER_COUNT ? -2 : -1; if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0) return -2; if (!uber_valid(u)) return -2; /* changed under us, or a flaky device */ vol_from_uber(v, u, best_slot); v->readonly = 1; // until an allocation map exists, below // The newest uberblock passing its own checksum does not prove the tree it // names is readable — a misdirected write could have landed on the root. // Check now, while there is still the option of falling back, rather than // discovering it on the first lookup. if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0) return -2; { struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a; if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root) return -2; } // Rebuild the allocation map from the tree just adopted. Without a palloc // the volume stays read-only, which is exactly right for a tool that only // inspects an image: it can then never allocate, rather than allocating // from a map that says everything is free. if (palloc) { int sr = schfs_space_init(&v->space, v->total_blocks, palloc); if (sr != 0) return sr; if (schfs_space_scan(v) != 0) return -2; v->free_blocks = v->space.free_blocks; v->readonly = 0; } return 0; } int schfs_vol_commit(struct schfs_vol *v) { struct schfs_uberblock *u = (struct schfs_uberblock *)v->ctx.a; v->generation++; v->uber_slot = (v->uber_slot + 1) % SCHFS_UBER_COUNT; memset(u, 0, sizeof(*u)); u->magic = SCHFS_MAGIC; u->version = SCHFS_VERSION; u->blocksize = SCHFS_BLKSIZE; u->generation = v->generation; u->root = v->tree.root; u->root_crc = v->tree.root_crc; u->root_generation = v->tree.root_gen; u->total_blocks = v->total_blocks; // Blocks this group released count as free *as of this uberblock*: nothing // the root below reaches touches them. They are only still marked in use in // memory because the group being superseded is what a crash would land on // until this very block is written. u->free_blocks = v->space.nblocks ? v->space.free_blocks + v->space.pending : v->free_blocks; u->next_objectid = v->next_objectid; u->root_objectid = v->root_objectid; memmove(u->uuid, v->uuid, sizeof(u->uuid)); memmove(u->label, v->label, sizeof(u->label)); u->crc = uber_crc(u); if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0) return -2; // Order matters here, and it is the whole crash-safety argument in three // lines. Only now that the new uberblock is on the disk is the group this // freed blocks in superseded, so only now may those blocks be handed out // again; releasing them before the write would let a crash come back to a // tree whose blocks had already been reused. if (v->space.nblocks) { schfs_space_commit(&v->space); v->free_blocks = v->space.free_blocks; } // Everything written from here on belongs to the next transaction group, so // the blocks just published become read-only to the tree again and the // copy-on-write rule resumes. Advancing this is the whole reason a commit // is a barrier rather than just another block write. v->tree.generation = v->generation + 1; return 0; } int schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev, uint64_t total_blocks, const char *label, struct schfs_ctx *x, void *(*palloc)(void)) { if (total_blocks <= SCHFS_FIRST_BLOCK) return -3; memset(v, 0, sizeof(*v)); v->dev = *dev; v->ctx = *x; v->tree.dev = &v->dev; v->total_blocks = total_blocks; v->root_objectid = SCHFS_ROOT_OBJECTID; // Nothing is allocated on a fresh volume beyond the boot block and the // ring, so the map starts empty rather than being scanned into existence. { int sr = schfs_space_init(&v->space, total_blocks, palloc); if (sr != 0) return sr; } v->free_blocks = v->space.free_blocks; // The root's object id is consumed by the root itself, so allocation must // start *past* it. Starting at SCHFS_FIRST_OBJECTID instead handed the // first directory created the same id as the root: that directory then // silently became the root, so it contained itself and any tree walk // recursed until the stack ran out. v->next_objectid = SCHFS_ROOT_OBJECTID + 1; v->generation = 0; /* nothing committed yet */ v->tree.generation = 1; /* ...so this is the group being built */ v->uber_slot = -1; v->readonly = 0; if (label) { unsigned i = 0; for (; i < SCHFS_LABEL_MAX - 1 && label[i]; i++) v->label[i] = label[i]; v->label[i] = '\0'; } // A UUID from the block count and a fixed salt. Not random — mkfs is // deliberately reproducible, so two runs over the same tree produce // byte-identical images and a diff means a real change. { uint32_t a = schfs_crc32c(0, &total_blocks, sizeof(total_blocks)); uint32_t b = schfs_crc32c(a, v->label, SCHFS_LABEL_MAX); for (int i = 0; i < 16; i++) v->uuid[i] = (uint8_t)((i & 1 ? a : b) >> ((i % 4) * 8)); } // Zero the whole ring so a stale uberblock from a previous filesystem on // this device cannot outrank ours and be mounted instead. memset(x->a, 0, SCHFS_BLKSIZE); for (int i = 0; i < SCHFS_UBER_COUNT; i++) if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + i, x->a) != 0) return -2; if (schfs_btree_create(&v->tree, &v->dev, x) != 0) return -2; return schfs_vol_commit(v); } /* ------------------------------------------------------------------ */ /* Allocation */ /* */ /* Thin adapters over the space manager, so callers wiring up a */ /* schfs_bdev have something with the right shape to point at. The */ /* readonly check lives here rather than in space.c because "this */ /* volume may not be written" is a property of the mount, not of the */ /* bitmap. */ /* ------------------------------------------------------------------ */ uint64_t schfs_vol_alloc(struct schfs_vol *v) { if (v->readonly) return 0; return schfs_space_alloc(&v->space); } uint64_t schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n) { if (v->readonly) return 0; return schfs_space_alloc_run(&v->space, n); } void schfs_vol_free(struct schfs_vol *v, uint64_t blk) { schfs_space_free(&v->space, blk); }