// 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; } // What this uberblock is, which is not the same question as whether it is good. // // This used to be one predicate returning "valid" or not, and a version // mismatch answered the same as a failed checksum. That conflates two states // the caller has to treat in opposite ways: // // A damaged uberblock means TRY THE NEXT ONE. That is what the ring is for, // and falling back to an older generation is exactly right -- it is crash // recovery, and the older root is a consistent filesystem. // // An uberblock this driver is too old to understand means STOP. Falling back // mounts a *stale* filesystem: every commit made since the volume was // upgraded is silently discarded, and the mount succeeds, so nobody finds // out. That is data loss reported as success, which is the worst outcome // available here. // // The order of the checks is the order of the questions. Is it ours (magic); // is it intact (crc); can we read it (version, features); does it make sense // (root in range). Compatibility is asked before sanity on purpose -- the // fields a sanity check reads are only meaningfully placed if the format is // one we know. enum uber_state { UBER_BAD = 0, // not SchFS, or damaged -- keep looking UBER_OK, // ours, and fully usable UBER_RO, // ours, sound, but carries ro_compat bits we lack UBER_TOO_NEW, // ours, sound, and beyond this driver -- stop the scan }; static enum uber_state uber_check(struct schfs_uberblock *u) { if (u->magic != SCHFS_MAGIC) return UBER_BAD; if (uber_crc(u) != u->crc) return UBER_BAD; // From here the block is certainly ours and certainly landed intact, so // anything wrong with it is a statement about the *format*, not the media. if (u->version > SCHFS_VERSION) return UBER_TOO_NEW; // A different block size is not damage either: every offset would be read // wrong, which is precisely what an incompatible format means. if (u->blocksize != SCHFS_BLKSIZE) return UBER_TOO_NEW; if (u->feature_incompat & ~SCHFS_FEAT_INCOMPAT_SUPP) return UBER_TOO_NEW; // 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 UBER_BAD; // Read-only is a real mount, so it is decided last -- after everything that // would have refused outright. if (u->feature_ro_compat & ~SCHFS_FEAT_RO_COMPAT_SUPP) return UBER_RO; // feature_compat is deliberately not consulted. Unknown bits there are // unknown on purpose; that is what the class means. return UBER_OK; } 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 io_errors = 0; int seen = 0; // valid uberblocks found, at any generation uint64_t ceiling = ~0ULL; // consider only generations below this int spaced = 0; // the allocation map is built and owned uint64_t too_new = 0; // newest generation this driver cannot read int ro_forced = 0; // the adopted root carries ro_compat bits v->dev = *dev; v->ctx = *x; v->tree.dev = &v->dev; v->recovered = 0; // Walk the ring newest-first, and keep walking. // // This used to take the highest valid generation, check its tree, and give // up if that failed. But an uberblock passing its own checksum only proves // the uberblock landed -- not the tree it names. That is exactly the state // a power cut used to leave behind before commit had a barrier, and it is // still what a misdirected write or a bad sector under the root produces. // // A ring of SCHFS_UBER_COUNT slots exists precisely so there is somewhere // to fall back *to*. Refusing the mount while 127 older, entirely intact // roots sat on the disk was throwing away the only thing the ring is for. for (;;) { int best_slot = -1; uint64_t best_gen = 0; for (int i = 0; i < SCHFS_UBER_COUNT; i++) { if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) { if (ceiling == ~0ULL) io_errors++; // count each slot once, on the first sweep continue; // an unreadable slot is not fatal: try the rest } { enum uber_state st = uber_check(u); if (st == UBER_TOO_NEW) { // Recorded, not mounted, and not skipped past. Counted on // the first sweep only, like io_errors: later sweeps walk // the same slots with a lower ceiling. if (ceiling == ~0ULL && u->generation > too_new) too_new = u->generation; continue; } if (st == UBER_BAD) continue; } if (u->generation >= ceiling) continue; // already tried, and rejected if (best_slot < 0 || u->generation > best_gen) { best_gen = u->generation; best_slot = i; } } if (ceiling == ~0ULL) seen = best_slot >= 0; if (best_slot < 0) break; // nothing older left to try // Refuse rather than mount something older than a root we could not // read. The candidate below is a perfectly good filesystem; it is just // not the *current* one, and mounting it would discard every commit // made since the volume was written by a newer driver -- silently, and // with the mount reporting success. if (too_new > best_gen) return -3; ceiling = best_gen; // next round considers strictly older ones // Re-read the winner rather than keeping 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 costs one block read per candidate. if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0) goto next; { enum uber_state st = uber_check(u); if (st == UBER_TOO_NEW) return -3; /* rewritten under us by a newer driver */ if (st == UBER_BAD) goto next; /* changed under us, or a flaky device */ ro_forced = (st == UBER_RO); } vol_from_uber(v, u, best_slot); v->readonly = 1; // until an allocation map exists, below if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0) goto next; { struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a; if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root) goto next; } // 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. // // The scan is also the deepest check this root gets: it visits every // metadata block and verifies each against the checksum in its parent. // A root that survives it is not merely present, it is whole -- so a // scan failure is a reason to try the next candidate, not to give up. if (palloc) { if (!spaced) { // Sizing failures are the caller's problem, not this root's: // a volume too large to map or a machine out of pages will // fail identically on every slot in the ring. int sr = schfs_space_init(&v->space, v->total_blocks, palloc); if (sr != 0) return sr; spaced = 1; } else { schfs_space_reset(&v->space, v->total_blocks); } if (schfs_space_scan(v) != 0) goto next; v->free_blocks = v->space.free_blocks; // A volume carrying ro_compat bits we do not implement stays // read-only even though the map is now good: writing would leave // whatever structure those bits describe stale, and stale is worse // than absent because nothing would notice. v->readonly = ro_forced; } // Which attributes are indexed, so attr.c can answer without a lookup // on every write. A volume with no indexes -- every volume until // somebody asks for one -- ends up with an empty list. schfs_index_reload(v); return 0; next: v->recovered++; // this candidate was newer, and unusable continue; } // Nothing usable. Distinguish "not a SchFS volume" from "a SchFS volume // that cannot be read", because the first is an ordinary answer to a probe // and the second is a damaged disk somebody needs to hear about. if (too_new) return -3; if (io_errors == SCHFS_UBER_COUNT) return -2; return seen ? -2 : -1; } 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); // The barrier the whole design rests on. Every block the new root reaches // has been handed to bwrite by now, but "handed to bwrite" is not "on the // medium": the kernel defers tree writes into the buffer cache, and the // drive under it has a write cache of its own. Both are free to reorder. // // So: drain everything, *then* write the block that makes it live. A crash // before this point leaves the previous uberblock newest and nothing is // lost; a crash after it finds a tree that is fully present. Without the // barrier there is a third outcome -- a newest uberblock naming a tree that // never landed -- and it is unrecoverable, because the checksum on the // uberblock is perfectly valid. if (v->dev.bflush && v->dev.bflush(v->dev.ctx) != 0) return -2; if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0) return -2; // And the uberblock itself, before this function claims the transaction is // durable. Everything below acts on that claim: the blocks this group freed // are handed back to the allocator, which is only safe once the group that // still referenced them can no longer be what a crash comes back to. if (v->dev.bflush && v->dev.bflush(v->dev.ctx) != 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); }