SchOS/kernel/fs/schfs/vol.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

459 lines
19 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// 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 <string.h>
#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),
void (*pfree)(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.
// A volume that has been opened before still holds the
// previous map, and schfs_space_init memsets it away -- so
// without this every re-open leaks one. schfs_objtest reopens
// the same volume nine times.
//
// Safe on a first open because a struct schfs_vol must be
// zeroed before first use, which is what release reads: a
// zeroed map has no pfree and no pages, so it does nothing.
schfs_space_release(&v->space);
int sr = schfs_space_init(&v->space, v->total_blocks, palloc, pfree);
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),
void (*pfree)(void *))
{
if (total_blocks <= SCHFS_FIRST_BLOCK)
return -3;
// Same reasoning as the release in schfs_vol_open: formatting over a
// volume that was previously open would otherwise drop its map on the
// floor, and the memset below is what makes it unrecoverable.
schfs_space_release(&v->space);
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, pfree);
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);
}