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.
This commit is contained in:
Nexoniarz 2026-08-09 13:47:01 +02:00
commit d0f7acf9eb
275 changed files with 51417 additions and 0 deletions

1181
kernel/fs/schfs/btree.c Normal file

File diff suppressed because it is too large Load diff

215
kernel/fs/schfs/btree.h Normal file
View file

@ -0,0 +1,215 @@
// SchFS copy-on-write B-tree.
//
// This is the load-bearing piece of the whole filesystem: inodes, directory
// entries, extent maps and indexes are all just records in this one tree, so a
// subtle bug here is a bug in everything. It is therefore written as portable C
// against an abstract block device, and exercised on the host by
// tools/schfs_test.c before it is ever run inside the kernel.
//
// Copy-on-write means no block that is part of the *committed* tree is ever
// modified. An update copies the path from the leaf back up to the root, and
// the caller then publishes the new root. A crash therefore lands on either the
// old tree or the new one, never a mixture — which is why there is no journal
// and no fsck.
//
// "Committed" is the operative word: a block first written during the
// transaction group now being built is not reachable from any published root,
// so it may be rewritten in place. See schfs_btree.generation.
#ifndef SCHFS_BTREE_H
#define SCHFS_BTREE_H
#include "schfs.h"
// Block device the tree lives on. The kernel supplies one backed by the buffer
// cache; the test harness supplies one backed by a plain file or by memory.
struct schfs_bdev {
void *ctx;
int (*bread)(void *ctx, uint64_t blk, void *buf);
int (*bwrite)(void *ctx, uint64_t blk, const void *buf);
// Allocate a block for copy-on-write. Must never return a block that is
// reachable from the last committed root.
uint64_t (*balloc)(void *ctx);
// Release a block that the new tree no longer references. Freeing may only
// take effect at commit, since the old tree must stay readable until then.
void (*bfree)(void *ctx, uint64_t blk);
// Optional: allocate n physically contiguous blocks, returning the first,
// or 0 if that cannot be satisfied. This is what lets a file become one
// extent rather than one per block, and it has to be a separate entry point
// because an allocator with a free list will happily return scattered
// blocks from balloc. NULL means unsupported; callers then fall back to
// allocating singly and accept the extra extent records.
uint64_t (*balloc_run)(void *ctx, uint64_t n);
};
struct schfs_btree {
struct schfs_bdev *dev;
uint64_t root; // block number of the current root
uint32_t root_crc;
uint64_t root_gen; // generation stamped in the root block itself
// The transaction group being built. Every block written by a mutation is
// stamped with this, and a block already carrying it is rewritten in place
// rather than copied — it cannot be part of a committed tree. The volume
// layer advances it on every commit; nothing else may touch it.
uint64_t generation;
};
// ---------------------------------------------------------------------------
// Scratch buffers
//
// Nothing in this file may put a block on the stack. A SchFS block is 4096
// bytes and so is the entire kernel stack (KSTACKSIZE), so even one such local
// overflows it — the kstack canary caught exactly that on the first boot of the
// read path. Mutations need three blocks live at once (the node being edited,
// the sibling a split produces, and a repacking buffer), so callers hand in
// three and the tree borrows them.
//
// The three must not alias, and their contents do not survive across calls.
// ---------------------------------------------------------------------------
struct schfs_ctx {
void *a; // node currently being edited
void *b; // sibling produced by a split
void *c; // repacking scratch for leaves
};
// ---------------------------------------------------------------------------
// Cursor
//
// The read path is driven through a cursor for the same reason: a cursor holds
// exactly one block buffer, supplied by the caller, and remembers the path as
// block numbers, re-reading a parent on the way back up. Walking a tree of any
// depth therefore costs one page rather than one page per level.
// ---------------------------------------------------------------------------
struct schfs_path_ent {
uint64_t blocknr;
uint32_t crc;
int index; // internal: child descended into; leaf: item
};
struct schfs_cursor {
struct schfs_btree *t;
void *blk; // caller-owned, SCHFS_BLKSIZE bytes
int depth; // path[depth] is the leaf
int valid; // 0 once the walk has run off either end
struct schfs_path_ent path[SCHFS_MAX_LEVEL + 1];
};
// `blk` must point at SCHFS_BLKSIZE bytes that outlive the cursor.
void schfs_cursor_init(struct schfs_cursor *c, struct schfs_btree *t, void *blk);
// Position at the first key >= start, or at the very first key if start is
// NULL. Returns 0 if positioned, -1 if the tree holds no such key, -2 on
// corruption.
int schfs_cursor_seek(struct schfs_cursor *c, const struct schfs_key *start);
// Step. Return 0, -1 at the end of the tree, -2 on corruption.
int schfs_cursor_next(struct schfs_cursor *c);
int schfs_cursor_prev(struct schfs_cursor *c);
// Current record. Both pointers are invalidated by the next seek/next/prev.
// Return NULL when the cursor is not positioned.
const struct schfs_key *schfs_cursor_key(struct schfs_cursor *c);
const void *schfs_cursor_val(struct schfs_cursor *c, unsigned *len);
// Ordering over keys: (objectid, type, offset). Returns <0, 0, >0.
int schfs_key_cmp(const struct schfs_key *a, const struct schfs_key *b);
uint32_t schfs_crc32c(uint32_t crc, const void *buf, unsigned len);
// ---------------------------------------------------------------------------
// Node accessors
//
// Plain views over the on-disk layout in schfs.h. Exposed because the space
// manager walks the tree by block number rather than by key, and a second,
// slightly different copy of these three lines is exactly the sort of thing
// that ends up disagreeing with the first.
// ---------------------------------------------------------------------------
#define SCHFS_HDR(b) ((struct schfs_node_hdr *)(b))
#define SCHFS_KPTRS(b) ((struct schfs_kptr *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
#define SCHFS_ITEMS(b) ((struct schfs_item *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
// Read one tree block into `out` and validate it completely: magic, the block's
// self-pointer, its checksum, and — when want_crc is non-zero — that the
// checksum is the one its parent recorded. Returns 0 or -2.
int schfs_node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc,
void *out);
// Create an empty tree (a single empty leaf) and set t->root.
int schfs_btree_create(struct schfs_btree *t, struct schfs_bdev *dev,
struct schfs_ctx *x);
// Look a key up. On success copies at most *len bytes of the value into buf and
// stores the true size in *len. Returns 0 on success, -1 if absent, -2 on a
// corrupt or unreadable block. Uses x->a as its block buffer.
int schfs_btree_lookup(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, void *buf, unsigned *len);
// Same, through a cursor the caller already has positioned somewhere else.
int schfs_btree_lookup_cur(struct schfs_cursor *c, const struct schfs_key *k,
void *buf, unsigned *len);
// Greatest key <= k, which is how an extent covering a file offset is found:
// extents are keyed by the file offset they start at, so the record governing
// byte N is the last one at or before (ino, EXTENT, N). Leaves the cursor
// positioned on it. Returns 0, -1 if every key in the tree is greater, -2 on
// corruption.
int schfs_btree_lookup_le(struct schfs_cursor *c, const struct schfs_key *k);
// Insert or replace. Copies the path, so t->root changes on success.
// Returns 0, -1 if the value is too large to store inline, -2 on I/O or
// corruption, -3 if the device is out of blocks.
int schfs_btree_insert(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, const void *val, unsigned len);
// Remove a key. Emptied nodes are freed and the root is collapsed when it drops
// to a single child, so a tree that is emptied returns to one block.
// Returns 0, -1 if absent, -2 on corruption, -3 out of space.
int schfs_btree_delete(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k);
// Walk records from `start` (inclusive) onward. This is what a directory read
// uses: every entry of directory D is a key in [(D,DIRENT,0), (D,DIRENT,~0)],
// so listing a directory costs the size of that directory rather than the size
// of the filesystem. fn returns non-zero to stop.
//
// WARNING: the `val` pointer handed to fn points *into the cursor's block
// buffer*. Any other tree operation the callback performs may overwrite it —
// including one on a different cursor, if the two were given the same buffer.
// A callback that looks anything up must copy what it needs first. The key is
// already copied out for you; the value is not, because it can be a kilobyte
// and most callbacks consume it immediately.
int schfs_btree_range(struct schfs_cursor *c, const struct schfs_key *start,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// ---------------------------------------------------------------------------
// Host-only facilities
//
// These recurse and keep a block per level, which is fine in a tool and fatal
// in the kernel. Compiling them out of the kernel makes that a link error
// rather than a comment nobody reads.
// ---------------------------------------------------------------------------
#ifdef SCHFS_HOSTED
// Walk every record in key order, calling fn for each. A non-zero return from
// fn stops the walk and is passed back.
int schfs_btree_iterate(struct schfs_btree *t,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// Full structural check: magic, self-pointer, checksum, level agreement, key
// ordering within and across nodes, that values lie inside their block, and
// that every child's first key is exactly the key its parent files it under.
// Returns 0 if the tree is sound, non-zero on the first problem found.
int schfs_btree_check(struct schfs_btree *t);
#endif /* SCHFS_HOSTED */
#endif /* SCHFS_BTREE_H */

623
kernel/fs/schfs/kmount.c Normal file
View file

@ -0,0 +1,623 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS in the kernel: block device, mount, and the transaction bracket.
//
// The filesystem code itself (btree.c, space.c, vol.c, obj.c, objw.c) is the
// same portable C the host tools compile, so nothing here re-implements the
// format. This file is only the three things that cannot be shared: a block
// device backed by the buffer cache, the boot-time mount, and the rule about
// when a group of changes becomes durable.
//
// The running root filesystem is still the v1 code in kernel/fs/fs.c, so a
// SchFS volume that fails to mount costs nothing — the system boots exactly as
// before and says so on the console.
//
// ---------------------------------------------------------------------------
// Transactions
//
// Every entry point below that changes anything is one transaction group: it
// takes the volume lock, does its work, and commits before returning. That
// makes an operation durable the moment it returns, and — because a commit is a
// single uberblock write that either lands or does not — indivisible against a
// crash. There is no journal to replay and nothing to undo, because none of the
// work is reachable until that one block is written.
//
// The cost is one uberblock write per operation. Batching several operations
// into a group would amortise it, and the structure here is deliberately shaped
// so that becomes a change to where commit is called rather than a redesign;
// but a filesystem that loses the last few operations on a crash is a much
// worse default than a slow one.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "schfs_int.h"
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
// filesystem block spans SCHFS_BPB consecutive buffers. Keeping the buffer
// cache at its own block size avoids disturbing the v1 filesystem, the log and
// the IDE driver, all of which assume BSIZE.
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
struct schfs_kdev {
uint dev;
};
static struct schfs_kdev kdev;
static struct schfs_vol kvol;
static int kmounted;
// Block buffers, allocated once at mount.
//
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
// SCHFS_BLKSIZE buffer and the kernel stack is one 4096-byte page (KSTACKSIZE),
// so a single such local overflows it. kalloc returns exactly one page, which
// is exactly one SchFS block, so each of these fits with nothing to spare.
//
// kcurblk and kdatablk serve the read paths; kctx* are the three the volume
// keeps for mutations. They are separate pages because a write path runs a
// cursor of its own inside the volume's buffers while the caller's cursor is
// still positioned in kcurblk.
static char *kcurblk;
static char *kdatablk;
static char *kctxa, *kctxb, *kctxc;
// Those buffers are shared by every caller, so every entry point below holds
// this lock for the whole operation. A sleeplock rather than a spinlock because
// bread() sleeps waiting on the disk.
//
// It also serialises transactions, which is not incidental: a group being built
// rewrites its own blocks in place, so two operations interleaving inside one
// group would each see the other's half-finished tree.
static struct sleeplock schfs_lock;
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
// matches what fs.c does for the v1 filesystem rather than inventing a second,
// differently-wrong answer.
static uint64_t
know(void)
{
return (uint64_t)uptime();
}
// Start and finish one transaction. `changed` says whether anything was
// written, so a read does not pay for an uberblock write.
static void
ktx_begin(void)
{
acquiresleep(&schfs_lock);
}
static int
ktx_end(int changed, int r)
{
if (changed && r >= 0) {
int c = schfs_vol_commit(&kvol);
if (c != 0)
r = c;
}
releasesleep(&schfs_lock);
return r;
}
static int
kd_read(void *ctx, uint64_t blk, void *out)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
// The buffer cache addresses blocks with a uint, which caps the volume long
// before the on-disk format does. Refusing out here beats truncating and
// reading some unrelated sector, which would surface as corruption.
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove((char *)out + i * BSIZE, b->data, BSIZE);
brelse(b);
}
return 0;
}
static int
kd_write(void *ctx, uint64_t blk, const void *in)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
// Straight through the buffer cache, with no log. The xv6 log exists to make
// multi-block updates atomic; copy-on-write makes them atomic by never
// modifying a block the committed tree can reach, so logging every write
// would double the I/O to re-solve a problem the format has already solved.
//
// bread before the overwrite because the cache has no "give me this block
// without reading it" entry point. It costs a read the first time a block is
// touched and nothing afterwards.
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
bwrite(b);
brelse(b);
}
return 0;
}
// Allocation goes through the volume's space manager. These take no ctx of
// their own because a schfs_bdev has one ctx shared by every hook and the block
// I/O above needs it for the device number.
static uint64_t
kd_alloc(void *ctx)
{
(void)ctx;
return schfs_vol_alloc(&kvol);
}
static uint64_t
kd_alloc_run(void *ctx, uint64_t n)
{
(void)ctx;
return schfs_vol_alloc_run(&kvol, n);
}
static void
kd_free(void *ctx, uint64_t b)
{
(void)ctx;
schfs_vol_free(&kvol, b);
}
// Pages for the allocation bitmap: one per 32768 blocks of volume, taken once
// at mount and never given back.
static void *
kd_palloc(void)
{
return kalloc();
}
// Try to mount a SchFS volume from `dev`. Never panics: a disk that is not
// SchFS, or is an unreadable SchFS, simply leaves the volume unmounted.
void
schfs_kmount(uint dev)
{
if (kmounted)
return;
initsleeplock(&schfs_lock, "schfs");
if (kcurblk == 0) kcurblk = kalloc();
if (kdatablk == 0) kdatablk = kalloc();
if (kctxa == 0) kctxa = kalloc();
if (kctxb == 0) kctxb = kalloc();
if (kctxc == 0) kctxc = kalloc();
if (kcurblk == 0 || kdatablk == 0 ||
kctxa == 0 || kctxb == 0 || kctxc == 0) {
cprintf("schfs: no memory for block buffers\n");
return;
}
kdev.dev = dev;
struct schfs_bdev bd;
bd.ctx = &kdev;
bd.bread = kd_read;
bd.bwrite = kd_write;
bd.balloc = kd_alloc;
bd.bfree = kd_free;
bd.balloc_run = kd_alloc_run;
struct schfs_ctx cx;
cx.a = kctxa;
cx.b = kctxb;
cx.c = kctxc;
// The uberblock scan doubles as the probe: no valid uberblock means this is
// simply not a SchFS volume, which is not an error worth reporting loudly.
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
int r = schfs_vol_open(&kvol, &bd, &cx, kd_palloc);
if (r == -1)
return;
if (r == -4) {
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
dev, (int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
return;
}
if (r == -5) {
cprintf("schfs: no memory for the allocation map of device %d\n", dev);
return;
}
if (r != 0) {
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
dev);
return;
}
kmounted = 1;
cprintf("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
kvol.label, dev,
(int)(kvol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
(int)kvol.generation);
}
int
schfs_kmounted(void)
{
return kmounted;
}
// Resolve a path on the mounted volume. Returns the object id, or 0.
uint64_t
schfs_klookup(const char *path)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t r = schfs_path_lookup(&kvol, &c, path);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kstat(uint64_t ino, struct schfs_inode *out)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_inode_get(&kvol, &c, ino, out);
releasesleep(&schfs_lock);
return r;
}
long
schfs_kread(uint64_t ino, uint64_t off, void *buf, unsigned n)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
long r = schfs_file_read(&kvol, &c, ino, off, buf, n, kdatablk);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kreaddir(uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
if (!kmounted)
return -1;
// NOTE: fn must not call back into schfs_k* -- the lock is not recursive and
// the cursor buffer is shared. Callers that need per-entry detail collect the
// entries first, then look them up after the walk.
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_dir_iterate(&kvol, &c, dir, fn, arg);
releasesleep(&schfs_lock);
return r;
}
struct schfs_vol *
schfs_kvol(void)
{
return kmounted ? &kvol : 0;
}
/* ------------------------------------------------------------------ */
/* Write side */
/* */
/* Each of these is one transaction: everything it does is invisible */
/* until it commits, and a crash part way through leaves the volume at */
/* the state before the call. That is the entire failure story — there */
/* is no partial state to detect and nothing to roll back. */
/* ------------------------------------------------------------------ */
int
schfs_kcreate(uint64_t dir, const char *name, unsigned namelen, uint16_t type,
uint32_t mode, uint16_t major, uint16_t minor, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_create(&kvol, dir, name, namelen, type, mode, major, minor,
know(), out);
return ktx_end(1, r);
}
long
schfs_kwrite(uint64_t ino, uint64_t off, const void *buf, unsigned n)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
long r = schfs_file_write(&kvol, ino, off, buf, n, kdatablk);
// ktx_end speaks in error codes, and a write speaks in byte counts. Passing
// the count straight through told it the transaction had failed with status
// 17, which it duly reported -- and a short write is a panic one layer up.
int e = ktx_end(1, r < 0 ? (int)r : 0);
return e == 0 ? r : (long)e;
}
int
schfs_ktruncate(uint64_t ino, uint64_t size)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_file_truncate(&kvol, ino, size, kdatablk);
return ktx_end(1, r);
}
int
schfs_kunlink(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_unlink(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
int
schfs_klink(uint64_t dir, const char *name, unsigned namelen, uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_link(&kvol, dir, name, namelen, ino);
return ktx_end(1, r);
}
int
schfs_krename(uint64_t olddir, const char *oldname, unsigned oldlen,
uint64_t newdir, const char *newname, unsigned newlen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_rename(&kvol, olddir, oldname, oldlen,
newdir, newname, newlen);
return ktx_end(1, r);
}
// Write back an inode record the caller has modified — the one way to change
// mode, ownership or timestamps. The caller reads it with schfs_kstat, edits
// the struct, and hands it back.
int
schfs_ksetattr(uint64_t ino, const struct schfs_inode *in)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_inode_put(&kvol, ino, in);
return ktx_end(1, r);
}
// Flush without changing anything. Nothing needs this yet — every operation
// above commits already — but sync(2) has to mean something.
int
schfs_ksync(void)
{
if (!kmounted || kvol.readonly)
return 0;
ktx_begin();
return ktx_end(1, 0);
}
/* ------------------------------------------------------------------ */
/* Pieces the VFS needs */
/* */
/* The VFS keeps its own link count and decides when an object dies, */
/* so it needs the directory entry and the object as separate */
/* operations rather than the combined create/unlink above. Two layers */
/* counting links independently is exactly how a filesystem loses a */
/* file that something still has open. */
/* ------------------------------------------------------------------ */
// Resolve one name inside a directory. 0 if there is no such entry.
uint64_t
schfs_klookup_at(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t ino = 0;
if (schfs_dir_lookup(&kvol, &c, dir, name, namelen, &ino, 0) != 0)
ino = 0;
releasesleep(&schfs_lock);
return ino;
}
// 1 if the directory holds no entries, 0 if it does, -1 on error.
int
schfs_kdirempty(uint64_t dir)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
int r = schfs_dir_empty(&kvol, dir);
releasesleep(&schfs_lock);
return r == -2 ? -1 : r;
}
// An object with no name and no links yet. Nothing can reach it until the
// caller links it, and nothing reaches the disk until the transaction commits,
// so a caller that gives up in between leaves nothing behind.
int
schfs_kalloc_object(uint16_t type, uint32_t mode, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
uint64_t ino = schfs_alloc_objectid(&kvol);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.mode = mode;
in.nlink = 0;
in.atime = in.mtime = in.ctime = know();
in.gen = kvol.tree.generation;
int r = schfs_inode_put(&kvol, ino, &in);
if (r == 0 && out)
*out = ino;
return ktx_end(1, r);
}
int
schfs_kdiradd(uint64_t dir, const char *name, unsigned namelen, uint64_t ino,
uint16_t type)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_add(&kvol, dir, name, namelen, ino, type);
return ktx_end(1, r);
}
// Remove one directory entry, leaving the object alone. The VFS decrements the
// link count itself and calls schfs_kfree when it reaches zero.
int
schfs_kdirdel(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_del(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
// Release an object's data, checksums and inode record. Called from iput once
// the last name is gone and nothing has it open.
int
schfs_kfree(uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_object_free(&kvol, ino);
return ktx_end(1, r);
}
uint64_t
schfs_kroot(void)
{
return kmounted ? kvol.root_objectid : 0;
}
/* ------------------------------------------------------------------ */
/* /proc/schfs */
/* */
/* Phase 2 has no VFS integration -- SchFS files are not reachable */
/* through open(2) yet -- so this is how the mount is observable from */
/* userland at all. It reports the volume and lists the root directory, */
/* which is enough to confirm from inside a running Schunix that the */
/* kernel really did parse an image mkschfs wrote. */
/* ------------------------------------------------------------------ */
static void
kp_puts(char *buf, int cap, int *len, const char *s)
{
while (*s && *len < cap - 1)
buf[(*len)++] = *s++;
}
static void
kp_putu(char *buf, int cap, int *len, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { if (*len < cap - 1) buf[(*len)++] = '0'; return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) { if (*len < cap - 1) buf[(*len)++] = tmp[i]; }
}
struct kp_ctx {
char *buf;
int cap;
int *len;
int count;
};
static int
kp_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
struct kp_ctx *c = arg;
c->count++;
// Only the first handful, and no per-entry stat: schfs_kreaddir holds the
// lock across the walk, so calling schfs_kstat from here would deadlock.
if (c->count > 16)
return 0;
kp_puts(c->buf, c->cap, c->len, " ");
kp_puts(c->buf, c->cap, c->len,
type == SCHFS_T_DIR ? "dir " :
type == SCHFS_T_SYMLINK ? "symlink " :
type == SCHFS_T_DEV ? "dev " : "file ");
for (unsigned i = 0; i < namelen && *c->len < c->cap - 1; i++)
c->buf[(*c->len)++] = name[i];
kp_puts(c->buf, c->cap, c->len, " #");
kp_putu(c->buf, c->cap, c->len, ino);
kp_puts(c->buf, c->cap, c->len, "\n");
return 0;
}
int
schfs_kinfo(char *buf, int cap)
{
int len = 0;
if (!kmounted) {
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
return len;
}
kp_puts(buf, cap, &len, "label: ");
kp_puts(buf, cap, &len, kvol.label);
kp_puts(buf, cap, &len, "\nblocksize: ");
kp_putu(buf, cap, &len, SCHFS_BLKSIZE);
kp_puts(buf, cap, &len, "\ntotal_blocks: ");
kp_putu(buf, cap, &len, kvol.total_blocks);
kp_puts(buf, cap, &len, "\nfree_blocks: ");
kp_putu(buf, cap, &len, kvol.free_blocks);
kp_puts(buf, cap, &len, "\ngeneration: ");
kp_putu(buf, cap, &len, kvol.generation);
kp_puts(buf, cap, &len, "\ntree_root: ");
kp_putu(buf, cap, &len, kvol.tree.root);
kp_puts(buf, cap, &len, "\nroot_objectid: ");
kp_putu(buf, cap, &len, kvol.root_objectid);
kp_puts(buf, cap, &len, "\n\nroot directory:\n");
struct kp_ctx c = { buf, cap, &len, 0 };
if (schfs_kreaddir(kvol.root_objectid, kp_entry, &c) != 0) {
kp_puts(buf, cap, &len, " (directory read failed)\n");
return len;
}
if (c.count > 16) {
kp_puts(buf, cap, &len, " ... ");
kp_putu(buf, cap, &len, (uint64)(c.count - 16));
kp_puts(buf, cap, &len, " more\n");
}
return len;
}

441
kernel/fs/schfs/obj.c Normal file
View file

@ -0,0 +1,441 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, read side: inodes, directories and file data.
//
// Everything here is a record in the one B-tree, which is the point of the
// btrfs-style design — there is no inode table, no directory block format and
// no indirect-block scheme to get wrong, only key types:
//
// (ino, INODE, 0) struct schfs_inode
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
// (ino, EXTENT, fileblk) struct schfs_extent
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
//
// Kernel code reaches all of this through a caller-supplied cursor, so no
// function here puts a 4096-byte block on the stack.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
// Point a cursor at the volume's current tree, keeping the caller's block
// buffer. Called before every operation rather than once at open, because the
// tree root moves on every commit under copy-on-write: a cursor that cached the
// old root would keep reading a stale — though perfectly valid — filesystem.
static void
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
{
c->t = &v->tree;
c->depth = 0;
c->valid = 0;
}
void
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset)
{
memset(k, 0, sizeof(*k));
k->objectid = objectid;
k->type = type;
k->offset = offset;
}
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
// FNV does that, is a dozen lines, and needs no table — worth more here than
// the marginally better distribution of something larger.
//
// Collisions are handled by the packed dirent list rather than assumed away;
// see struct schfs_dirent_rec.
// Test-only: narrows the hash so collisions become common.
//
// With a full 64-bit hash, two names in one directory colliding is so unlikely
// that the packed-list code which handles it would never run — and untested
// code that only executes on rare inputs is exactly the code that loses a file
// years later. Setting this to a small mask makes every directory a pile of
// collisions, so the round-trip test exercises the path properly. Always ~0
// in the kernel and in real images.
uint64_t schfs_hash_mask = ~0ULL;
uint64_t
schfs_namehash(const char *name, unsigned len)
{
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
for (unsigned i = 0; i < len; i++) {
h ^= (uint8_t)name[i];
h *= 1099511628211ULL; /* FNV prime */
}
// Key ordering is by this value, so a directory's entries come back in an
// order that has nothing to do with their names. That is fine for readdir
// (POSIX promises no order) and is what makes lookup one descent.
return h & schfs_hash_mask;
}
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
int
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
cur_bind(c, v);
unsigned len = sizeof(*out);
int r = schfs_btree_lookup_cur(c, &k, out, &len);
if (r != 0)
return r;
// A short record means an image written by an older or broken tool. Reject
// it rather than acting on a half-filled struct.
if (len != sizeof(*out))
return -2;
return 0;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
// non-zero. Returns fn's value, or -2 if the list is malformed.
static int
dirent_walk(const void *val, unsigned len,
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
void *arg)
{
unsigned off = 0;
while (off < len) {
if (len - off < sizeof(struct schfs_dirent_rec))
return -2;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)((const char *)val + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
return -2;
int r = fn(d, (const char *)d + sizeof(*d), arg);
if (r)
return r;
off += step;
}
return 0;
}
struct find_ctx {
const char *name;
unsigned namelen;
uint64_t ino;
uint16_t type;
};
static int
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
{
struct find_ctx *f = (struct find_ctx *)arg;
if (d->namelen != f->namelen)
return 0;
if (memcmp(nm, f->name, f->namelen) != 0)
return 0;
f->ino = d->objectid;
f->type = d->type;
return 1; /* found; stop */
}
int
schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
cur_bind(c, v);
int r = schfs_cursor_seek(c, &k);
if (r == -2)
return -2;
if (r != 0)
return -1;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got || schfs_key_cmp(got, &k) != 0)
return -1;
unsigned len;
const void *val = schfs_cursor_val(c, &len);
if (!val)
return -2;
struct find_ctx f;
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
r = dirent_walk(val, len, find_one, &f);
if (r == -2)
return -2;
if (r != 1)
return -1; /* hash matched, name did not */
if (out_ino) *out_ino = f.ino;
if (out_type) *out_type = f.type;
return 0;
}
struct iter_ctx {
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
void *arg;
struct schfs_cursor *cur;
uint64_t dir;
int stop;
};
// Hand each entry of one packed dirent list to the caller.
//
// The list lives in the cursor's block buffer, and almost every readdir caller
// immediately looks each entry up — to stat it, to recurse into it — through a
// cursor that may well share that buffer. Walking the list in place meant the
// name and every record after it turned to garbage the moment the callback
// touched the filesystem, which is how an extracted tree once ended up with
// directories reparented under their own children.
//
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
// path that also runs under procread's 2 KB buffer. So only the current entry
// is copied, and the item is re-fetched from the cursor each time round:
// schfs_cursor_val re-reads the block, which restores whatever the callback
// disturbed.
static int
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
{
struct iter_ctx *ic = (struct iter_ctx *)arg;
(void)val;
// The range scan runs until something stops it, so stopping at the first
// key that is no longer this directory's is what bounds the walk.
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
return 1;
if (len > SCHFS_MAX_INLINE)
return 1;
unsigned off = 0;
while (off < len) {
unsigned now;
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
if (!v || now != len) {
ic->stop = -2;
return 1;
}
if (len - off < sizeof(struct schfs_dirent_rec)) {
ic->stop = -2;
return 1;
}
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v + off);
unsigned namelen = d->namelen;
unsigned step = SCHFS_DIRENT_LEN(namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
ic->stop = -2;
return 1;
}
char name[SCHFS_NAME_MAX];
uint64_t oid = d->objectid;
uint16_t type = d->type;
memmove(name, (const char *)d + sizeof(*d), namelen);
int r = ic->fn(oid, type, name, namelen, ic->arg);
if (r) {
ic->stop = r;
return 1;
}
off += step;
}
return 0;
}
int
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct iter_ctx ic;
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
cur_bind(c, v);
int r = schfs_btree_range(c, &start, iter_item, &ic);
if (r == -2)
return -2;
return ic.stop == 1 ? 0 : ic.stop;
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
// corruption. Extents are keyed by the file block they start at, so the record
// governing block B is the last one at or before (ino, EXTENT, B).
static int
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
cur_bind(c, v);
int r = schfs_btree_lookup_le(c, &k);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got)
return 0;
// lookup_le can land on some other object's records entirely; a sparse file
// whose first extent starts past this block looks the same way.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
unsigned len;
const struct schfs_extent *e =
(const struct schfs_extent *)schfs_cursor_val(c, &len);
if (!e || len != sizeof(*e))
return -2;
if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */
*out = e->disk_block + (fblk - got->offset);
return 1;
}
// Verify a data block against its checksum record. Tree nodes carry their
// checksum in the parent pointer; data blocks have no parent, so the check must
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
// no record exists — an image may legitimately predate checksums for a block,
// and refusing to read data over a *missing* checksum would turn an upgrade
// into a total loss.
static int
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t want;
unsigned len = sizeof(want);
cur_bind(c, v);
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
if (r == -1)
return 0;
if (r != 0 || len != sizeof(want))
return -2;
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
return -2;
return 0;
}
long
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
if (off >= in.size)
return 0;
if (off + n > in.size)
n = (unsigned)(in.size - off);
char *dst = (char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
uint64_t disk = 0;
int m = extent_map(v, c, ino, fblk, &disk);
if (m == -2)
return -2;
if (m == 0) {
memset(dst + done, 0, want); /* hole reads as zeros */
} else {
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
return -2;
if (csum_check(v, c, disk, dblk) != 0)
return -2;
memmove(dst + done, (char *)dblk + inblk, want);
}
done += want;
}
return (long)done;
}
/* ------------------------------------------------------------------ */
/* Path resolution */
/* ------------------------------------------------------------------ */
uint64_t
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
{
uint64_t cur = v->root_objectid;
while (*path == '/')
path++;
while (*path) {
const char *s = path;
while (*path && *path != '/')
path++;
unsigned len = (unsigned)(path - s);
while (*path == '/')
path++;
if (len == 1 && s[0] == '.')
continue;
if (len == 2 && s[0] == '.' && s[1] == '.') {
// ".." is synthetic: directories store a parent id rather than an
// entry, so walking up is an inode read, not a lookup.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
cur = in.parent ? in.parent : v->root_objectid;
continue;
}
// Every component but the last must be a directory. Checking here is
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
if (in.type != SCHFS_T_DIR)
return 0;
uint64_t next;
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
return 0;
cur = next;
}
return cur;
}

932
kernel/fs/schfs/objw.c Normal file
View file

@ -0,0 +1,932 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, write side: create, link, unlink, rename, write,
// truncate.
//
// The same portable C as the read side, compiled both hosted (mkschfs) and
// freestanding (the kernel mount), so an image the tools produce and one the
// kernel produces come from a single implementation rather than two that agree
// by luck. Everything below is expressed as records in the one B-tree; there is
// no inode table, no directory block format and no bitmap to keep in step.
//
// Two rules run through the whole file.
//
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
// is the entire kernel stack, so every function borrows the volume's three
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
// its buffer — which is safe only because no path below holds a cursor across a
// tree mutation, and every one of them copies what it needs out first.
//
// Nothing reachable from a committed root is ever overwritten. For tree blocks
// the B-tree enforces that; for *data* blocks it is enforced here, by
// schfs_extent.gen. A run allocated during the group being built may be written
// in place, because no published root can see it. Anything older is copied to a
// fresh block and the extent record split around it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
uint64_t
schfs_alloc_objectid(struct schfs_vol *v)
{
return v->next_objectid++;
}
int
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
}
// Read an inode using the volume's own scratch block. Every mutation below is a
// read-modify-write of one of these, and each one that took a cursor buffer
// from its caller was one more place to get the aliasing rules wrong.
static int
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
return schfs_inode_get(v, &c, ino, out);
}
/* ------------------------------------------------------------------ */
/* Directory entries */
/* */
/* One key per name hash, holding a packed list of records, because two */
/* names in a directory can hash the same and the second insert would */
/* otherwise silently replace the first. */
/* ------------------------------------------------------------------ */
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
// 0 if there is no such key, or a negative error.
static int
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, struct schfs_key *k)
{
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
unsigned len = sizeof(v->dirbuf);
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
if (r == -2)
return -2;
if (r != 0)
return 0;
if (len > sizeof(v->dirbuf))
return -2; /* stored value larger than can exist */
return (int)len;
}
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
static int
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
{
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(list + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
if (step == 0 || off + step > len)
return -2;
if (d->namelen == namelen &&
memcmp(list + off + sizeof(*d), name, namelen) == 0)
return (int)off;
off += step;
}
return -1;
}
int
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
// Reject a duplicate outright. Without this, adding a name twice produces a
// directory with two entries that lookup can never tell apart.
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at >= 0)
return -1;
unsigned add = SCHFS_DIRENT_LEN(namelen);
if (len + add > sizeof(v->dirbuf))
return -4; /* collision list will not fit in one item */
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
memset(d, 0, add);
d->objectid = child;
d->type = type;
d->namelen = (uint16_t)namelen;
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
}
int
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
if (len == 0)
return -1;
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at < 0)
return -1;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v->dirbuf + at);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
len -= step;
// An emptied list means the key itself goes, not a zero-length value: a key
// that exists with nothing in it would make the directory scan stop on a
// record readdir cannot describe.
if (len == 0)
return schfs_btree_delete(&v->tree, &v->ctx, &k);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
}
int
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_cursor_seek(&c, &start);
if (r == -2)
return -2;
if (r != 0)
return 1; /* nothing at or after: empty */
const struct schfs_key *got = schfs_cursor_key(&c);
if (!got)
return 1;
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
}
/* ------------------------------------------------------------------ */
/* Data-block checksums */
/* */
/* Tree nodes carry their checksum in their parent's pointer. Data */
/* blocks have no parent, so theirs live under one reserved object id, */
/* exactly as btrfs keeps a checksum tree. */
/* ------------------------------------------------------------------ */
static int
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
}
static int
csum_del(struct schfs_vol *v, uint64_t dblk)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r; /* absent is not a failure */
}
/* ------------------------------------------------------------------ */
/* Extents */
/* ------------------------------------------------------------------ */
static int
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
}
static int
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
// The extent covering file block `fblk`, if any. Extents are keyed by the file
// block they start at, so the record governing block B is the last one at or
// before (ino, EXTENT, B). Returns 1 and fills *start and *e, 0 for a hole, -2
// on corruption.
static int
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
uint64_t *start, struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
// lookup_le happily lands on some other object's records; so does a sparse
// file whose first extent starts past this block.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
if (fblk >= got->offset + found->nblocks)
return 0;
*start = got->offset;
*e = *found;
return 1;
}
// The last extent record belonging to `ino`, or 0 if it has none.
static int
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
*start = got->offset;
*e = *found;
return 1;
}
// Record a newly allocated single block, merging it into the preceding extent
// when the two are adjacent both in the file and on the disk.
//
// This is what keeps extents worth having. Sequential writes get consecutive
// blocks from the allocator, so without merging a one-megabyte file carries 256
// extent records instead of one, and the tree ends up mostly describing where
// the data is rather than holding it.
static int
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
if (fblk > 0) {
uint64_t pstart;
struct schfs_extent p;
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
if (m == -2)
return -2;
if (m == 1 &&
pstart + p.nblocks == fblk &&
p.disk_block + p.nblocks == e->disk_block &&
p.refs == e->refs && p.gen == e->gen) {
p.nblocks++;
return extent_put(v, ino, pstart, &p);
}
}
return extent_put(v, ino, fblk, e);
}
// Release a run of data blocks and the checksum records that describe them.
// Shared runs are left alone: another file still points at them, and proper
// refcounting needs an extent tree that nothing creates yet.
static int
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
uint64_t first, uint64_t n)
{
if (e->refs != 1)
return 0;
for (uint64_t i = 0; i < n; i++) {
int r = csum_del(v, first + i);
if (r != 0)
return r;
v->dev.bfree(v->dev.ctx, first + i);
}
return 0;
}
// Drop everything from file block `from` onward. Used by truncate and by the
// deletion of an object.
static int
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
uint64_t from)
{
for (;;) {
uint64_t start;
struct schfs_extent e;
int m = extent_last(v, ino, &start, &e);
if (m == -2)
return -2;
if (m == 0)
return 0; /* no extents left */
if (start + e.nblocks <= from)
return 0; /* nothing beyond the cut */
if (start >= from) {
int r = extent_release(v, &e, e.disk_block, e.nblocks);
if (r != 0)
return r;
r = extent_del(v, ino, start);
if (r != 0)
return r;
if (in->blocks >= e.nblocks)
in->blocks -= e.nblocks;
continue; /* there may be earlier ones too */
}
// The cut falls inside this run: keep its head, release its tail.
uint64_t keep = from - start;
uint64_t tail = e.nblocks - keep;
int r = extent_release(v, &e, e.disk_block + keep, tail);
if (r != 0)
return r;
e.nblocks = keep;
r = extent_put(v, ino, start, &e);
if (r != 0)
return r;
if (in->blocks >= tail)
in->blocks -= tail;
return 0;
}
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
int
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
{
if (nblocks == 0)
return 0;
// Take the whole run before touching the tree.
//
// This is what makes extents worth having when the final size is already
// known. Allocating a data block, then inserting its extent record -- which
// itself allocates blocks for the copy-on-write path -- then allocating the
// next data block interleaves metadata with data, so consecutive file
// blocks are never physically adjacent. Grabbing the run up front keeps it
// contiguous and a whole file becomes one record.
uint64_t first;
if (v->dev.balloc_run) {
first = v->dev.balloc_run(v->dev.ctx, nblocks);
if (first == 0)
return -3;
} else {
first = v->dev.balloc(v->dev.ctx);
if (first == 0)
return -3;
uint64_t prev = first;
for (uint64_t i = 1; i < nblocks; i++) {
uint64_t b = v->dev.balloc(v->dev.ctx);
if (b == 0)
return -3;
if (b != prev + 1)
return -2; /* allocator is not handing out a run */
prev = b;
}
}
struct schfs_extent e;
memset(&e, 0, sizeof(e));
e.disk_block = first;
e.nblocks = nblocks;
e.refs = 1;
e.gen = v->tree.generation;
// Deliberately does not touch the inode. Re-reading and rewriting it here
// costs two extra tree inserts per file, and every insert copies its whole
// path. The caller knows the block count before it writes the inode the
// first time, so it sets in.blocks there instead.
return extent_put(v, ino, 0, &e);
}
// Where the next write to file block `fblk` should land.
struct wblk {
uint64_t disk; // block to write
uint64_t src; // block holding the current contents, 0 for a hole
int fresh; // a block that did not exist before
};
static int
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
struct wblk *w)
{
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 0) {
uint64_t d = v->dev.balloc(v->dev.ctx);
if (d == 0)
return -3;
struct schfs_extent ne;
memset(&ne, 0, sizeof(ne));
ne.disk_block = d;
ne.nblocks = 1;
ne.refs = 1;
ne.gen = v->tree.generation;
int r = extent_grow(v, ino, fblk, &ne);
if (r != 0)
return r;
w->disk = d;
w->src = 0;
w->fresh = 1;
return 0;
}
uint64_t old = e.disk_block + (fblk - start);
// Allocated by the group being built, and owned outright: no published root
// can reach it, so it may simply be overwritten.
if (e.gen == v->tree.generation && e.refs == 1) {
w->disk = old;
w->src = old;
w->fresh = 0;
return 0;
}
// Otherwise copy this one block out of the run, and split the record around
// it: a head covering what came before, the new block, and a tail covering
// what came after. No deletion is needed — when the block being replaced is
// the run's first, the middle record simply overwrites the original key.
uint64_t nd = v->dev.balloc(v->dev.ctx);
if (nd == 0)
return -3;
if (fblk > start) {
struct schfs_extent head = e;
head.nblocks = fblk - start;
int r = extent_put(v, ino, start, &head);
if (r != 0)
return r;
}
{
struct schfs_extent mid;
memset(&mid, 0, sizeof(mid));
mid.disk_block = nd;
mid.nblocks = 1;
mid.refs = 1;
mid.gen = v->tree.generation;
int r = extent_put(v, ino, fblk, &mid);
if (r != 0)
return r;
}
if (fblk + 1 < start + e.nblocks) {
struct schfs_extent tail = e;
tail.disk_block = e.disk_block + (fblk + 1 - start);
tail.nblocks = start + e.nblocks - (fblk + 1);
int r = extent_put(v, ino, fblk + 1, &tail);
if (r != 0)
return r;
}
// The block just replaced rejoins the free map at the next commit, not now:
// until then the committed tree still points at it and a crash lands there.
int r = extent_release(v, &e, old, 1);
if (r != 0)
return r;
w->disk = nd;
w->src = old;
w->fresh = 0;
return 0;
}
long
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
const char *src = (const char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t at = off + done;
uint64_t fblk = at / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
// A whole-block write needs no read: every byte is about to be
// replaced. A partial one has to start from whatever is there, or from
// zeros where the file had a hole.
if (inblk != 0 || want != SCHFS_BLKSIZE) {
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
}
memmove((char *)dblk + inblk, src + done, want);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
// The checksum is rewritten every time the block changes, not just when
// it is allocated, or a topped-up tail block fails its own check on the
// very next read.
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
done += want;
}
if (off + n > in.size)
in.size = off + n;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
return (long)done;
}
int
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r == -1 ? -2 : r;
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
if (got < 0)
return (int)got;
return got == (long)n ? 0 : -2;
}
int
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (size < in.size) {
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
r = extents_drop_from(v, ino, &in, keep);
if (r != 0)
return r;
// Whole blocks past the new end are gone; the block the new size falls
// *inside* has to have its tail zeroed. It is tempting not to bother,
// since a read stops at in.size and can never see those bytes — but
// truncating down and then growing again is required to read back
// zeros, and without this it reads back whatever the file used to hold.
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
if (tail != 0) {
uint64_t fblk = size / SCHFS_BLKSIZE;
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 1) { /* a hole is already zeros */
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
}
}
}
// Growing needs nothing at all: the range with no extent records reads back
// as zeros, which is a sparse file for free.
in.size = size;
in.gen = v->tree.generation;
return schfs_inode_put(v, ino, &in);
}
// Release an object entirely: its data, its checksums and its inode record.
int
schfs_object_free(struct schfs_vol *v, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r == -1)
return 0; /* already gone */
if (r != 0)
return r;
r = extents_drop_from(v, ino, &in, 0);
if (r != 0)
return r;
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
/* ------------------------------------------------------------------ */
/* Namespace */
/* ------------------------------------------------------------------ */
// A directory's link count is 2 (itself and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back at it. Neither
// entry is stored, but the count still has to match what POSIX tools expect or
// "find -type d" prunes real directories and du walks the tree wrongly.
static int
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
{
struct schfs_inode in;
int r = inode_load(v, dir, &in);
if (r != 0)
return r;
if (delta < 0 && in.nlink < (uint32_t)(-delta))
return -2;
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
return schfs_inode_put(v, dir, &in);
}
int
schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode parent;
int r = inode_load(v, dir, &parent);
if (r != 0)
return r;
if (parent.type != SCHFS_T_DIR)
return -1;
uint64_t ino = schfs_alloc_objectid(v);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.parent = dir;
in.mode = mode;
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
in.rdev_major = rdev_major;
in.rdev_minor = rdev_minor;
in.atime = in.mtime = in.ctime = now;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
// The entry goes in last, so a failure leaves an unreferenced object rather
// than a name pointing at nothing. Neither survives a crash — none of this
// is visible until the transaction commits — but an unreachable inode is
// the harmless one of the two if a later step in the same group fails.
r = schfs_dir_add(v, dir, name, namelen, ino, type);
if (r != 0)
return r;
if (type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, +1);
if (r != 0)
return r;
}
if (out_ino)
*out_ino = ino;
return 0;
}
int
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
// Directories are not hard-linkable. Allowing it makes cycles possible, and
// a cycle turns every recursive walk into an infinite one.
if (in.type == SCHFS_T_DIR)
return -1;
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
if (r != 0)
return r;
in.nlink++;
in.ctime = in.mtime;
return schfs_inode_put(v, ino, &in);
}
int
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
if (r != 0)
return r;
}
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
int e = schfs_dir_empty(v, ino);
if (e == -2)
return -2;
if (e == 0)
return -5; /* directory not empty */
}
r = schfs_dir_del(v, dir, name, namelen);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, -1);
if (r != 0)
return r;
return schfs_object_free(v, ino);
}
if (in.nlink > 1) {
in.nlink--;
return schfs_inode_put(v, ino, &in);
}
return schfs_object_free(v, ino);
}
int
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
if (r != 0)
return r;
}
// Renaming a directory into its own subtree would detach that subtree from
// the root and leave it reachable only from itself. Walking up from the new
// parent is the cheap way to catch it, since every directory records its
// own parent.
if (type == SCHFS_T_DIR && newdir != olddir) {
uint64_t up = newdir;
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
if (up == ino)
return -1;
struct schfs_inode t;
if (inode_load(v, up, &t) != 0)
break;
if (up == v->root_objectid)
break;
up = t.parent;
}
}
// A target that already exists is replaced, which means removing it first —
// and refusing when that would silently discard a non-empty directory.
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
uint64_t victim;
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
if (victim == ino)
return 0; /* renaming a name onto itself */
int r = schfs_unlink(v, newdir, newname, newlen);
if (r != 0)
return r;
}
}
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
if (r != 0)
return r;
r = schfs_dir_del(v, olddir, oldname, oldlen);
if (r != 0)
return r;
if (newdir != olddir) {
if (type == SCHFS_T_DIR) {
// ".." is synthesised from the inode's parent field, so moving a
// directory means rewriting that field and moving the link its ".."
// contributes to each parent's count.
struct schfs_inode in;
r = inode_load(v, ino, &in);
if (r != 0)
return r;
in.parent = newdir;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
r = parent_nlink_adjust(v, olddir, -1);
if (r != 0)
return r;
r = parent_nlink_adjust(v, newdir, +1);
if (r != 0)
return r;
}
}
return 0;
}

277
kernel/fs/schfs/schfs_int.h Normal file
View file

@ -0,0 +1,277 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS internals: the volume (uberblock ring, block allocation) and the object
// layer (inodes, directories, file data) that sit on top of the B-tree.
//
// Split three ways so the kernel links only what it needs:
//
// vol.c open / format / commit — the uberblock ring
// obj.c read paths — kernel needs these in phase 2
// objw.c write paths — host tools only, until phase 3
//
// Everything here is portable C compiled both hosted (mkfs, fsck) and freestanding
// (kernel), so a bug is found on the host, where a test takes a second rather
// than a boot.
#ifndef SCHFS_INT_H
#define SCHFS_INT_H
#include "btree.h"
// ---------------------------------------------------------------------------
// space.c — the allocation bitmap
//
// One bit per block, held in RAM and derived from the committed tree at mount
// rather than stored on disk. See space.c for why.
//
// The bitmap is a vector of pages because the kernel allocates a page at a time
// and gives no promise that two are adjacent. That vector is fixed-size, and
// its length is therefore the hard ceiling on how large a volume this build can
// mount: SCHFS_SPACE_PAGES pages x SCHFS_SPACE_BITS_PER_PAGE blocks each.
// ---------------------------------------------------------------------------
#define SCHFS_SPACE_BITS_PER_PAGE ((uint64_t)SCHFS_BLKSIZE * 8) /* 32768 */
#define SCHFS_SPACE_PAGES 512
#define SCHFS_MAX_BLOCKS \
(SCHFS_SPACE_BITS_PER_PAGE * (uint64_t)SCHFS_SPACE_PAGES) /* 64 GiB */
struct schfs_space {
uint64_t *inuse[SCHFS_SPACE_PAGES]; // 1 = allocated
uint64_t *pend [SCHFS_SPACE_PAGES]; // 1 = freed, but not yet reusable
int npages;
uint64_t nblocks;
uint64_t free_blocks;
uint64_t pending; // bits set in pend[], counted
uint64_t hint; // rotating search start
};
// A mounted (or under-construction) volume.
//
// NOT copyable: tree.dev points at this struct's own dev field, so a copy
// leaves the tree talking to whichever struct it was built in. Open into the
// storage it will live in.
struct schfs_vol {
struct schfs_bdev dev;
struct schfs_btree tree;
// The three block buffers every mutation borrows, handed in at open or
// format and owned by the caller for the life of the volume. Nothing in
// SchFS may put a SCHFS_BLKSIZE buffer on the stack: a block is 4096 bytes
// and so is the whole kernel stack.
//
// The object layer also uses ctx.a as a cursor buffer, which is safe on the
// one condition that no cursor is still live when a tree mutation runs.
// Every write path here reads what it needs out of the cursor first.
struct schfs_ctx ctx;
struct schfs_space space;
uint64_t total_blocks;
uint64_t free_blocks;
uint64_t next_objectid;
uint64_t root_objectid;
uint64_t generation; // last *committed* transaction group
int uber_slot; // ring slot the last commit used
int readonly;
char label[SCHFS_LABEL_MAX];
uint8_t uuid[16];
// Staging for a directory's packed dirent list. Deliberately not one of the
// ctx buffers: a split reuses ctx.b and ctx.c while the value being
// inserted is still being read from, so the value needs storage of its own.
char dirbuf[SCHFS_MAX_INLINE];
};
// ---------------------------------------------------------------------------
// vol.c
// ---------------------------------------------------------------------------
// Scan the whole uberblock ring and adopt the highest generation that both
// checksums and points at a readable root. This *is* the crash recovery story:
// there is no journal to replay, because a torn update simply leaves an older
// slot as the newest valid one. Returns 0, -1 if no valid uberblock exists
// (not a SchFS volume), -2 on I/O error.
//
// `x` supplies the volume's three scratch blocks; they are kept for its whole
// lifetime, so they must outlive it and must not alias one another. `palloc`
// yields pages for the allocation bitmap and may be NULL for a read-only mount,
// which then cannot allocate at all.
int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
struct schfs_ctx *x, void *(*palloc)(void));
// Lay down an empty volume: one empty tree and one uberblock.
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));
// Publish the current tree by writing the next slot in the ring. Until this
// returns, everything written since the last commit is invisible and a crash
// discards it — which is exactly what makes the format crash-safe.
int schfs_vol_commit(struct schfs_vol *v);
// Block allocation. Returns 0 when the volume is full.
//
// Not themselves schfs_bdev callbacks: a bdev has one ctx shared by all of its
// hooks, and the caller's block I/O needs that slot for its own state (a file
// handle, a device number). Callers wrap these in a one-line adapter instead.
uint64_t schfs_vol_alloc(struct schfs_vol *v);
uint64_t schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n);
void schfs_vol_free(struct schfs_vol *v, uint64_t blk);
// ---------------------------------------------------------------------------
// space.c
// ---------------------------------------------------------------------------
// Build an empty map covering `nblocks`, with everything below
// SCHFS_FIRST_BLOCK already reserved. `palloc` yields SCHFS_BLKSIZE-sized
// pages; the map keeps them for the volume's lifetime. Returns 0, -3 if the
// volume is too small to hold anything, -4 if it is larger than this build can
// map (see SCHFS_MAX_BLOCKS), -5 if palloc ran out.
int schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void));
// Record a run as allocated. Idempotent, because an extent range may be shared
// by several files.
void schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n);
uint64_t schfs_space_alloc(struct schfs_space *s);
uint64_t schfs_space_alloc_run(struct schfs_space *s, uint64_t n);
// Release a block — but only for the *next* transaction group. Until the
// commit that supersedes it, the last committed tree still points here and a
// crash would land on it.
void schfs_space_free(struct schfs_space *s, uint64_t blk);
// Fold this group's deferred frees back into the free map. Called by
// schfs_vol_commit once the new uberblock is on the disk, never before.
void schfs_space_commit(struct schfs_space *s);
// Walk the committed tree and mark every block it reaches. This is what makes
// the map correct without storing it. Returns 0 or -2 on an unreadable tree.
int schfs_space_scan(struct schfs_vol *v);
// ---------------------------------------------------------------------------
// obj.c — read side
// ---------------------------------------------------------------------------
uint64_t schfs_namehash(const char *name, unsigned len);
// Test-only knob; see obj.c. Narrowing this forces directory-hash collisions
// so the packed-dirent path actually runs. Never changed outside tools/.
extern uint64_t schfs_hash_mask;
void schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset);
// Fetch an inode record. Returns 0, -1 if there is no such object, -2 corrupt.
int schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out);
// Resolve one name inside a directory. Returns 0 and fills *out_ino/*out_type,
// -1 if absent, -2 corrupt.
int schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type);
// Iterate a directory's real entries in hash order. "." and ".." are not
// stored and are not reported; callers that need them synthesise them.
// A non-zero return from fn stops the walk and is passed back.
int schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t ino, uint16_t type, const char *name,
unsigned namelen, void *arg),
void *arg);
// Read from a file. Short reads at end-of-file are normal; returns the byte
// count, or -1 on a missing object, -2 on corruption or a failed data checksum.
// `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks.
long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk);
// Walk an absolute path. Returns the object id, or 0 if any component is
// missing or is not a directory.
uint64_t schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c,
const char *path);
// ---------------------------------------------------------------------------
// objw.c — write side
//
// Shared error codes throughout: 0 success, -1 absent or not permitted,
// -2 corruption or I/O failure, -3 out of space, -4 a directory hash-collision
// list that will not fit in one item, -5 directory not empty.
//
// None of it is visible to a reader until schfs_vol_commit publishes the tree,
// which is what lets a failure part way through a multi-record operation be
// abandoned rather than unwound.
// ---------------------------------------------------------------------------
uint64_t schfs_alloc_objectid(struct schfs_vol *v);
int schfs_inode_put(struct schfs_vol *v, uint64_t ino,
const struct schfs_inode *in);
// Add one entry, appending to the packed list if another name already hashes
// the same. -1 if the name is already present.
int schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type);
// Remove one entry. -1 if there is no such name.
int schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// 1 if the directory holds no entries, 0 if it holds some, -2 on corruption.
int schfs_dir_empty(struct schfs_vol *v, uint64_t dir);
// Claim a contiguous run of data blocks for a file whose final size is already
// known, and record the whole run as ONE extent. Worth calling before a bulk
// write: without it, data and tree blocks interleave, and only the merging in
// schfs_file_write keeps the extent count down.
int schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks);
// Write at an arbitrary offset, allocating blocks, extent records and data
// checksums, copying any block that a committed tree can still reach, and
// splitting the extent record around it. Writing past the end extends the file;
// a gap left behind stays a hole and reads back as zeros. Returns the byte
// count written, or a negative error.
long schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk);
// Append at the current end of file. A convenience over schfs_file_write, kept
// because image builders write nothing else.
int schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk);
// Set a file's length, releasing whole blocks past the new end and zeroing what
// is left of the block the new end falls inside — truncating down and growing
// again has to read back zeros. Growing alone costs nothing: a range with no
// extent records is a hole. `dblk` is a caller-owned SCHFS_BLKSIZE buffer.
int schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk);
// Release an object's data, checksums and inode record. The caller must already
// have removed every name pointing at it.
int schfs_object_free(struct schfs_vol *v, uint64_t ino);
// Create an object and the entry naming it. `now` is the timestamp to stamp all
// three times with — SchFS has no clock of its own.
int schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino);
// A second name for an existing object. -1 for a directory: hard-linking one
// makes cycles possible, and a cycle turns every recursive walk infinite.
int schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino);
// Remove a name, and the object with it once the last name is gone. Handles
// directories too, refusing with -5 if the directory still has entries.
int schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// Move a name, replacing the target if one exists. Refuses to move a directory
// into its own subtree, which would detach that subtree from the root.
int schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen);
#endif /* SCHFS_INT_H */

314
kernel/fs/schfs/space.c Normal file
View file

@ -0,0 +1,314 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS space manager: which blocks are in use.
//
// There is no free-space map on disk, and that is the interesting decision
// here. The committed tree already records exactly which blocks are live —
// every tree node is named by its parent's pointer, every data block by an
// extent record — so the map is *derived* at mount by walking the tree once,
// held in RAM, and never written back.
//
// The alternative, persisting it, buys a fast mount and costs the one problem
// copy-on-write is otherwise free of: the map has to be updated atomically with
// the tree it describes, and writing the map needs blocks, which changes the
// map. ZFS spends real complexity on that recursion and btrfs resolves it with
// a fixpoint loop. Deriving the map instead cannot disagree with the filesystem
// no matter what a crash interrupts, because it is computed from it — there is
// no second source of truth to get out of step.
//
// What it costs is a metadata scan at mount, proportional to the number of tree
// blocks rather than to the size of the volume. Nothing is written, so it is
// also safe on a read-only mount. If mount time ever becomes the thing that
// hurts, the map can be cached on disk later purely as a hint, validated
// against the uberblock's generation and discarded when it does not match — an
// addition, not a format change.
//
// ---------------------------------------------------------------------------
// Deferred frees
//
// A block released during the transaction group being built is still reachable
// from the last *committed* root, which is where a crash lands. Handing it
// straight back would let the filesystem overwrite the very copy it is supposed
// to fall back to. So frees go into a second bitmap and only rejoin the free
// map when the commit that supersedes them lands. One transaction of delay, no
// more: once group N is published, group N-1's tree is no longer anybody's
// fallback.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Bit vectors, paged */
/* */
/* The bitmap has to be one flat array to be indexable, and the kernel */
/* hands out memory a page at a time with no promise that two pages */
/* are adjacent. So it is a vector of page pointers instead: one page */
/* covers SCHFS_SPACE_BITS_PER_PAGE blocks, and the page vector itself */
/* is a fixed-size array, which is what puts a hard ceiling on volume */
/* size. See docs/architecture/schfs.md for the resulting limits. */
/* ------------------------------------------------------------------ */
static int
bit_test(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return (int)((pages[pg][off / 64] >> (off % 64)) & 1);
}
static void
bit_set(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
pages[pg][off / 64] |= 1ULL << (off % 64);
}
// The whole 64-block word containing b, so a scan can skip a fully used region
// sixty-four blocks at a time instead of one. On a nearly full volume that is
// the difference between an allocation costing microseconds and milliseconds.
static uint64_t
bit_word(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return pages[pg][off / 64];
}
static int
popcount64(uint64_t v)
{
int n = 0;
while (v) { v &= v - 1; n++; }
return n;
}
/* ------------------------------------------------------------------ */
/* Setup */
/* ------------------------------------------------------------------ */
int
schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void))
{
memset(s, 0, sizeof(*s));
if (nblocks <= SCHFS_FIRST_BLOCK)
return -3;
if (nblocks > SCHFS_MAX_BLOCKS)
return -4; /* larger than this build can map */
uint64_t npages = (nblocks + SCHFS_SPACE_BITS_PER_PAGE - 1)
/ SCHFS_SPACE_BITS_PER_PAGE;
for (uint64_t i = 0; i < npages; i++) {
void *p = palloc();
if (p == 0)
return -5; /* caller frees what was taken */
memset(p, 0, SCHFS_BLKSIZE);
s->inuse[i] = (uint64_t *)p;
p = palloc();
if (p == 0)
return -5;
memset(p, 0, SCHFS_BLKSIZE);
s->pend[i] = (uint64_t *)p;
s->npages = (int)(i + 1);
}
s->nblocks = nblocks;
s->free_blocks = nblocks - SCHFS_FIRST_BLOCK;
s->hint = SCHFS_FIRST_BLOCK;
// The boot block and the uberblock ring are never allocatable, and neither
// is anything past the end of the volume: the last bitmap page covers more
// blocks than exist, and a stray allocation out there would be written to a
// sector the device does not have.
for (uint64_t b = 0; b < SCHFS_FIRST_BLOCK; b++)
bit_set(s->inuse, b);
for (uint64_t b = nblocks; b < (uint64_t)npages * SCHFS_SPACE_BITS_PER_PAGE; b++)
bit_set(s->inuse, b);
return 0;
}
void
schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n)
{
for (uint64_t i = 0; i < n; i++) {
uint64_t b = blk + i;
if (b < SCHFS_FIRST_BLOCK || b >= s->nblocks)
continue;
if (bit_test(s->inuse, b))
continue; /* already counted; extents may be shared */
bit_set(s->inuse, b);
s->free_blocks--;
}
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* ------------------------------------------------------------------ */
uint64_t
schfs_space_alloc_run(struct schfs_space *s, uint64_t n)
{
if (n == 0 || n > s->free_blocks)
return 0;
// Two sweeps: from the rotating hint to the end of the volume, then from
// the start back to it. Starting every search at block 0 would make the
// front of the volume the only place anything is ever written and turn
// allocation into a linear scan of everything already used.
for (int pass = 0; pass < 2; pass++) {
uint64_t lo = pass == 0 ? s->hint : SCHFS_FIRST_BLOCK;
uint64_t hi = pass == 0 ? s->nblocks : s->hint;
uint64_t run = 0, first = 0;
for (uint64_t b = lo; b < hi; ) {
if (b % 64 == 0 && bit_word(s->inuse, b) == ~0ULL) {
run = 0;
b += 64;
continue;
}
if (bit_test(s->inuse, b)) {
run = 0;
b++;
continue;
}
if (run == 0)
first = b;
run++;
b++;
if (run == n) {
for (uint64_t i = 0; i < n; i++)
bit_set(s->inuse, first + i);
s->free_blocks -= n;
s->hint = first + n < s->nblocks ? first + n : SCHFS_FIRST_BLOCK;
return first;
}
}
}
return 0;
}
uint64_t
schfs_space_alloc(struct schfs_space *s)
{
return schfs_space_alloc_run(s, 1);
}
void
schfs_space_free(struct schfs_space *s, uint64_t blk)
{
if (blk < SCHFS_FIRST_BLOCK || blk >= s->nblocks)
return;
if (!bit_test(s->inuse, blk))
return; /* never allocated, or freed twice */
if (bit_test(s->pend, blk))
return; /* already queued this group */
bit_set(s->pend, blk);
s->pending++;
}
void
schfs_space_commit(struct schfs_space *s)
{
uint64_t words = SCHFS_SPACE_BITS_PER_PAGE / 64;
for (int p = 0; p < s->npages; p++) {
uint64_t *pend = s->pend[p];
uint64_t *inuse = s->inuse[p];
for (uint64_t w = 0; w < words; w++) {
if (pend[w] == 0)
continue;
s->free_blocks += popcount64(pend[w] & inuse[w]);
inuse[w] &= ~pend[w];
pend[w] = 0;
}
}
s->pending = 0;
}
/* ------------------------------------------------------------------ */
/* Deriving the map from the committed tree */
/* ------------------------------------------------------------------ */
// Every extent record in a leaf names a run of data blocks. A range shared by
// several files (a reflink, a snapshot) appears more than once, which is why
// schfs_space_mark is idempotent rather than counting.
static void
mark_leaf_extents(struct schfs_space *s, const void *blk)
{
const struct schfs_item *it = SCHFS_ITEMS(blk);
int n = SCHFS_HDR(blk)->nritems;
for (int i = 0; i < n; i++) {
if (it[i].key.type != SCHFS_KEY_EXTENT)
continue;
if (it[i].size != sizeof(struct schfs_extent))
continue;
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
continue;
const struct schfs_extent *e =
(const struct schfs_extent *)((const char *)blk + it[i].offset);
schfs_space_mark(s, e->disk_block, e->nblocks);
}
}
// Walk the whole tree, marking every block it reaches.
//
// Iterative, with one block buffer and a path of block numbers, for the same
// reason everything else here is: a recursive walk keeps a block per level and
// the kernel stack is one block in total. Climbing re-reads the parent, so an
// internal node is read once per child — mount-time cost that the buffer cache
// largely absorbs, in exchange for bounded memory.
int
schfs_space_scan(struct schfs_vol *v)
{
struct schfs_space *s = &v->space;
void *blk = v->ctx.a;
struct {
uint64_t nr;
uint32_t crc;
int idx; /* next child to descend into */
} path[SCHFS_MAX_LEVEL + 1];
int d = 0;
path[0].nr = v->tree.root;
path[0].crc = v->tree.root_crc;
path[0].idx = 0;
schfs_space_mark(s, v->tree.root, 1);
for (;;) {
if (schfs_node_read(&v->dev, path[d].nr, path[d].crc, blk) != 0)
return -2;
if (SCHFS_HDR(blk)->level == 0) {
mark_leaf_extents(s, blk);
d--;
} else if (path[d].idx < SCHFS_HDR(blk)->nritems) {
struct schfs_kptr *kp = &SCHFS_KPTRS(blk)[path[d].idx];
path[d].idx++;
if (d + 1 > SCHFS_MAX_LEVEL)
return -2;
d++;
path[d].nr = kp->blocknr;
path[d].crc = kp->crc;
path[d].idx = 0;
schfs_space_mark(s, path[d].nr, 1);
continue;
} else {
d--; /* every child visited */
}
if (d < 0)
return 0;
}
}

414
kernel/fs/schfs/vfs.c Normal file
View file

@ -0,0 +1,414 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS behind the VFS: the fs_ops table that makes SchFS files reachable
// through open(2), read(2), write(2) and the rest.
//
// Everything below is translation, not filesystem logic. The filesystem lives
// in btree.c/space.c/vol.c/obj.c/objw.c and is driven through the schfs_k*
// entry points in kmount.c, each of which is one transaction. This file's whole
// job is turning a `struct inode` into an object id and back.
//
// ---------------------------------------------------------------------------
// The two models, and where they disagree
//
// The VFS inherited xv6's shape: allocate an inode, fill it in, then link a
// name to it. SchFS has no free-standing inode — an object exists because a
// record with its id exists — so schfs_ialloc writes an inode record with
// nlink 0 and no name at all. Nothing can reach it until dirlink adds an entry.
// If the caller dies in between, the record is unreferenced but harmless, and
// it never reaches the disk anyway: nothing is durable until the transaction
// that created it commits.
//
// The other disagreement is `nlink`. The VFS adjusts it itself (create sets it
// to 1, link bumps it, unlink drops it) and then calls iupdate. So the object
// layer's own link accounting is deliberately bypassed here: schfs_dir_add and
// schfs_dir_del are used rather than schfs_link and schfs_unlink, and iput's
// ifree hook is what releases an object once the VFS has decided it has no
// names left. Two pieces of code counting links independently is precisely how
// a filesystem loses a file that is still referenced.
#include "types.h"
#include "x86.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "spinlock.h"
#include "proc.h" /* `proc` — copyin/copyout need the current pgdir */
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "file.h"
#include "schfs_int.h"
// The VFS talks in T_*; the disk format uses SCHFS_T_*. Neither is going to
// change to suit the other, so the mapping lives here, once, in both
// directions rather than as an assumption that the numbers happen to line up.
static short
type_to_vfs(uint16_t t)
{
switch (t) {
case SCHFS_T_DIR: return T_DIR;
case SCHFS_T_DEV: return T_DEV;
case SCHFS_T_SYMLINK: return T_SYMLINK;
default: return T_FILE;
}
}
static uint16_t
type_from_vfs(short t)
{
switch (t) {
case T_DIR: return SCHFS_T_DIR;
case T_DEV: return SCHFS_T_DEV;
case T_SYMLINK: return SCHFS_T_SYMLINK;
default: return SCHFS_T_FILE;
}
}
/* ------------------------------------------------------------------ */
/* Inode load / store / free */
/* ------------------------------------------------------------------ */
static void
schfs_vfs_iload(struct inode *ip)
{
struct schfs_inode in;
if (schfs_kstat(ip->inum, &in) != 0) {
// An unreadable inode on the root filesystem is a panic in v1 because the
// system cannot continue without it. Here it is a mounted volume that may
// simply be damaged, so report an empty file and let the caller fail
// normally rather than taking the machine down with it.
ip->type = T_FILE;
ip->nlink = 0;
ip->size = 0;
ip->valid = 1;
return;
}
ip->type = type_to_vfs(in.type);
ip->major = (short)in.rdev_major;
ip->minor = (short)in.rdev_minor;
ip->nlink = (short)in.nlink;
ip->size = in.size;
ip->rwmode = RW;
ip->supermode = 0;
ip->showmode = 1;
ip->atime = (uint)in.atime;
ip->mtime = (uint)in.mtime;
ip->ctime = (uint)in.ctime;
memset(ip->addrs, 0, sizeof(ip->addrs));
ip->valid = 1;
}
static void
schfs_vfs_istore(struct inode *ip)
{
struct schfs_inode in;
// Read-modify-write rather than build-from-scratch: the on-disk record holds
// fields the VFS has no slot for (uid, gid, the real mode bits, the
// generation), and rebuilding it here would quietly zero every one of them.
if (schfs_kstat(ip->inum, &in) != 0) {
memset(&in, 0, sizeof(in));
in.mode = ip->type == T_DIR ? 0755 : 0644;
}
in.type = type_from_vfs(ip->type);
in.rdev_major = (uint16_t)ip->major;
in.rdev_minor = (uint16_t)ip->minor;
in.nlink = (uint32_t)ip->nlink;
in.size = ip->size;
in.atime = ip->atime;
in.mtime = ip->mtime;
in.ctime = ip->ctime;
schfs_ksetattr(ip->inum, &in);
}
static void
schfs_vfs_ifree(struct inode *ip)
{
schfs_kfree(ip->inum);
}
/* ------------------------------------------------------------------ */
/* Data */
/* ------------------------------------------------------------------ */
// Reads and writes are staged through a bounce buffer rather than going
// straight to user memory, because the filesystem takes a sleeplock and walks
// the tree while a copyout could fault. One page at a time keeps that buffer
// off the 4096-byte kernel stack -- it is kalloc'd -- and bounds how long the
// volume lock is held.
#define BOUNCE 2048
static int
schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
char *tmp = kalloc();
if (tmp == 0)
return -1;
uint done = 0;
while (done < n) {
uint want = n - done;
if (want > BOUNCE)
want = BOUNCE;
long got = schfs_kread(ip->inum, off + done, tmp, want);
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
if (got == 0) break; /* end of file */
if (user_dst) {
if (copyout(myproc()->pgdir, dst + done, tmp, (uint)got) < 0)
break;
} else {
memmove((char *)(dst + done), tmp, (uint)got);
}
done += (uint)got;
if ((uint)got < want)
break; /* short read: end of file */
}
kfree(tmp);
return (int)done;
}
static int
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
char *tmp = kalloc();
if (tmp == 0)
return -1;
uint done = 0;
while (done < n) {
uint want = n - done;
if (want > BOUNCE)
want = BOUNCE;
if (user_src) {
if (copyin(myproc()->pgdir, (addr_t)(src + done), tmp, (uint64)want) < 0)
break;
} else {
memmove(tmp, (char *)(src + done), want);
}
long put = schfs_kwrite(ip->inum, off + done, tmp, want);
if (put <= 0) break;
done += (uint)put;
if ((uint)put < want) break;
}
kfree(tmp);
if (off + done > ip->size)
ip->size = off + done;
return (int)done;
}
static void
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
{
if (schfs_ktruncate(ip->inum, newsize) == 0)
ip->size = newsize;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
static struct inode *
schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
// "." and ".." are not stored: a directory costs nothing until it has real
// entries. Both are answered from what the VFS already knows, except ".."
// at the mount root, which the mount layer redirects before we get here.
if (name[0] == '.' && name[1] == '\0') {
if (poff) *poff = 0;
return iget(dp->dev, dp->inum);
}
unsigned namelen = 0;
while (name[namelen]) namelen++;
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
struct schfs_inode in;
if (schfs_kstat(dp->inum, &in) != 0)
return 0;
if (poff) *poff = 0;
return iget(dp->dev, in.parent ? in.parent : dp->inum);
}
uint64 ino = schfs_klookup_at(dp->inum, name, namelen);
if (ino == 0)
return 0;
// poff is v1's byte offset into the directory file. SchFS finds an entry by
// name, so there is nothing meaningful to hand back; dirunlink below takes
// the name and ignores it.
if (poff) *poff = 0;
return iget(dp->dev, ino);
}
static int
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
{
unsigned namelen = 0;
while (name[namelen]) namelen++;
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode in;
if (schfs_kstat(inum, &in) != 0)
return -1;
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
if (r != 0)
return -1;
// A subdirectory's ".." is synthesised from its parent field, so moving or
// creating one has to set that field or the new directory reports the wrong
// parent -- and every ".." walk out of it lands somewhere else entirely.
if (in.type == SCHFS_T_DIR && in.parent != dp->inum) {
in.parent = dp->inum;
schfs_ksetattr(inum, &in);
}
return 0;
}
static int
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
{
(void)off;
unsigned namelen = 0;
while (name[namelen]) namelen++;
return schfs_kunlink(dp->inum, name, namelen) == 0 ? 0 : -1;
}
static int
schfs_vfs_dirempty(struct inode *dp)
{
return schfs_kdirempty(dp->inum);
}
static struct inode *
schfs_vfs_ialloc(uint dev, short type)
{
uint64 ino = 0;
if (schfs_kalloc_object(type_from_vfs(type), type == T_DIR ? 0755 : 0644,
&ino) != 0)
return 0;
return iget(dev, ino);
}
/* ------------------------------------------------------------------ */
/* getdents64 */
/* ------------------------------------------------------------------ */
// One record's worth of state for the callback below.
//
// Entries come back in name-hash order, which has nothing to do with their
// names -- POSIX promises no order, and hash order is what makes a lookup one
// tree descent instead of a scan. The position handed back to userland is
// therefore an *index*: entry N of the walk. Resuming means walking to N again,
// which readdir already does linearly, so a full listing stays linear overall
// only because the common case is resuming exactly where the last call stopped.
struct gd_ctx {
char *out;
int cap;
int written;
uint64 skip; // entries to step over before emitting
uint64 index; // entries seen so far
int full;
};
static int
gd_entry(uint64 ino, unsigned short type, const char *name, unsigned namelen,
void *arg)
{
struct gd_ctx *g = arg;
if (g->index++ < g->skip)
return 0;
if (g->full)
return 1;
if (namelen > 255)
namelen = 255;
int reclen = (8 + 8 + 2 + 1 + (int)namelen + 1 + 7) & ~7;
if (g->written + reclen > g->cap) {
g->full = 1;
return 1; /* stop; the caller will come back */
}
char *k = g->out + g->written;
uint64 *p64 = (uint64 *)k;
p64[0] = ino;
p64[1] = g->index; /* resume position: entries consumed */
*(uint16 *)(k + 16) = (uint16)reclen;
*(uint8 *)(k + 18) = type == SCHFS_T_DIR ? 4 /* DT_DIR */
: type == SCHFS_T_DEV ? 2 /* DT_CHR */
: type == SCHFS_T_SYMLINK ? 10 /* DT_LNK */
: 8; /* DT_REG */
memmove(k + 19, name, namelen);
memset(k + 19 + namelen, 0, reclen - 19 - namelen);
g->written += reclen;
return 0;
}
static int
schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
{
struct gd_ctx g;
g.out = out;
g.cap = cap;
g.written = 0;
g.skip = *poff;
g.index = 0;
g.full = 0;
// "." and ".." are synthesised: they are not records, so a walk would never
// produce them, and every readdir caller expects both.
static const char *dots[2] = { ".", ".." };
for (int i = 0; i < 2; i++) {
if (g.index++ < g.skip)
continue;
int reclen = (8 + 8 + 2 + 1 + (i + 1) + 1 + 7) & ~7;
if (g.written + reclen > cap)
return g.written;
char *k = out + g.written;
uint64 *p64 = (uint64 *)k;
p64[0] = dp->inum; /* close enough for ".."; nothing reads it */
p64[1] = g.index;
*(uint16 *)(k + 16) = (uint16)reclen;
*(uint8 *)(k + 18) = 4; /* DT_DIR */
memmove(k + 19, dots[i], i + 1);
memset(k + 19 + i + 1, 0, reclen - 19 - i - 1);
g.written += reclen;
}
if (schfs_kreaddir(dp->inum, gd_entry, &g) != 0 && g.written == 0)
return -1;
*poff = g.index;
return g.written;
}
/* ------------------------------------------------------------------ */
const struct fs_ops schfs_ops = {
.name = "schfs",
.iload = schfs_vfs_iload,
.istore = schfs_vfs_istore,
.ifree = schfs_vfs_ifree,
.read = schfs_vfs_read,
.write = schfs_vfs_write,
.truncate = schfs_vfs_truncate,
.dirlookup = schfs_vfs_dirlookup,
.dirlink = schfs_vfs_dirlink,
.dirunlink = schfs_vfs_dirunlink,
.dirempty = schfs_vfs_dirempty,
.ialloc = schfs_vfs_ialloc,
.getdents = schfs_vfs_getdents,
};

299
kernel/fs/schfs/vol.c Normal file
View file

@ -0,0 +1,299 @@
// 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);
}