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

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

441 lines
14 KiB
C

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