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:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
441
kernel/fs/schfs/obj.c
Normal file
441
kernel/fs/schfs/obj.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue