SchOS/kernel/fs/schfs/vol.c
Nexoniarz d0f7acf9eb Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
2026-08-09 13:47:01 +02:00

299 lines
11 KiB
C

// 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 <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;
}
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);
}