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>
459 lines
19 KiB
C
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);
|
|
}
|