Nexoniarz is the sole author and SchOS was never published under Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text, every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE, README.md, docs/licensing.md and the site say so. projects/schfs-linux follows the rest of the tree. The GPL gives copyleft: a fork has to ship its source. It also lifts the old "Linux drivers, never" rule, since GPL-2.0-only code combines with GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still has to pick one version, so docs/licensing.md now keeps Apache-2.0 and GPLv3 code out of the kernel to keep Linux code possible there. BSD 4-clause code goes from "avoid" to "no", because the advertising clause is GPL-incompatible. LDLS still ships no .ko. That boundary is now project policy rather than a legal requirement, and ldls.md, devices.md and licensing.md section 6 say so. refmod declares license=GPL, and ldls-check passes (27 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
391 lines
19 KiB
C
391 lines
19 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// 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
|
|
// SCHFS_SPACE_RETAIN_ALL while a snapshot exists -- see schfs_space_free.
|
|
unsigned flags;
|
|
// How to give the pages back. Carried here rather than passed to the
|
|
// release call so that a map can only ever be freed by the allocator that
|
|
// filled it: the kernel's pages come from kalloc and a host tool's from
|
|
// calloc, and the two are not interchangeable.
|
|
void (*pfree)(void *);
|
|
};
|
|
|
|
// A mounted (or under-construction) volume.
|
|
//
|
|
// MUST BE ZEROED before its first use. schfs_vol_open and schfs_vol_format both
|
|
// release any allocation map the volume is already holding before building a
|
|
// new one -- that is what stops a re-open leaking the old map -- and on a first
|
|
// use they are reading whatever the struct was born with. Every volume in the
|
|
// tree is static storage, so this costs nothing; a stack-allocated one would
|
|
// have to be memset first.
|
|
//
|
|
// 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;
|
|
// How many newer uberblocks schfs_vol_open had to reject before it found a
|
|
// root it could actually read. Zero on every healthy mount. Anything else
|
|
// means transactions were lost, which is a recovery the operator has to be
|
|
// told about rather than something to absorb quietly.
|
|
int recovered;
|
|
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];
|
|
|
|
// Which attributes are indexed, as a packed list of (length, name), read
|
|
// off the disk at mount. Cached here rather than looked up because every
|
|
// attribute write asks the question and the answer is almost always no --
|
|
// a tree descent to say so would make every write pay for a feature it is
|
|
// not using. See kernel/fs/schfs/attr.c.
|
|
char idxnames[512];
|
|
unsigned idxnames_len;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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),
|
|
void (*pfree)(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),
|
|
void (*pfree)(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);
|
|
|
|
// Snapshots. Create records the *committed* root; the caller commits after.
|
|
// Lookup fills `out` for a given id. Deleting forgets the record, and the next
|
|
// derivation stops marking what only it reached.
|
|
int schfs_snap_create(struct schfs_vol *v, uint64_t id, const char *name,
|
|
unsigned namelen);
|
|
int schfs_snap_delete(struct schfs_vol *v, uint64_t id);
|
|
int schfs_snap_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t id,
|
|
struct schfs_snap *out);
|
|
|
|
// 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), void (*pfree)(void *));
|
|
|
|
// Hand every page back and zero the map. Safe on a map that was never
|
|
// initialised and safe to call twice, so an error path need not work out how
|
|
// far initialisation got.
|
|
//
|
|
// This exists because there was no way to give the pages back at all. The
|
|
// kernel leaked the whole map on every unmount -- up to 2 MB a time -- and
|
|
// schfs_objtest leaked one map per volume it opened, which ran AddressSanitizer
|
|
// up to 2 MB across 518 allocations and made the whole SchFS suite exit
|
|
// non-zero. A real leak in the filesystem would have arrived as noise inside
|
|
// that.
|
|
void schfs_space_release(struct schfs_space *s);
|
|
|
|
// Return an already-sized map to "empty volume", reusing its pages. Mounting
|
|
// may try several uberblocks, and each candidate root needs its scan to start
|
|
// from a clean map -- but the pages were taken once and must not be taken
|
|
// again, which is the one thing schfs_space_init cannot do twice.
|
|
void schfs_space_reset(struct schfs_space *s, uint64_t nblocks);
|
|
|
|
// 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.
|
|
// A directory entry, with the tree key's hash it was found under. The hash is
|
|
// what a partial listing resumes from -- see schfs_dir_iterate_from.
|
|
typedef int (*schfs_dir_fn)(uint64_t ino, uint16_t type, const char *name,
|
|
unsigned namelen, uint64_t hash, void *arg);
|
|
|
|
int schfs_dir_iterate_from(struct schfs_vol *v, struct schfs_cursor *c,
|
|
uint64_t dir, uint64_t start_hash,
|
|
schfs_dir_fn fn, void *arg);
|
|
|
|
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.
|
|
/* Drops the one-entry inode and extent read caches in obj.c. Every write
|
|
* entry point calls this before touching anything: the caches are only safe
|
|
* because invalidation is blunt, and one that is merely usually right hands
|
|
* back another file's data. */
|
|
void schfs_cache_invalidate(void);
|
|
|
|
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);
|
|
|
|
// Share `src`'s data blocks with `dst` without copying them. Both remain
|
|
// ordinary writable files; the first write to either copies only the run it
|
|
// touches. Costs one metadata record per run, whatever the file's size.
|
|
int schfs_file_reflink(struct schfs_vol *v, uint64_t src, uint64_t dst);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// attr.c -- attributes, the index over them, and the query that uses it
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// One attribute. Returns 1 and fills the buffers if present, 0 if absent, -3 if
|
|
// `out` is too small (the length is still reported), -2 on corruption.
|
|
int schfs_attr_get(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen, void *out, unsigned cap,
|
|
unsigned *valuelen, uint16_t *type);
|
|
|
|
// Every attribute on an object. The callback returns non-zero to stop.
|
|
int schfs_attr_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
|
int (*fn)(const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen,
|
|
uint16_t type, void *arg),
|
|
void *arg);
|
|
|
|
// Set or replace an attribute, keeping any index in step.
|
|
int schfs_attr_set(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen, const void *val, unsigned vallen,
|
|
uint16_t type);
|
|
|
|
// Remove one. Absent is not a failure.
|
|
int schfs_attr_del(struct schfs_vol *v, uint64_t ino, const char *name,
|
|
unsigned namelen);
|
|
|
|
// Start indexing an attribute. Affects values written after this only.
|
|
int schfs_index_create(struct schfs_vol *v, const char *name, unsigned namelen);
|
|
|
|
// Load the indexed-attribute list from the tree into the volume. Called at
|
|
// mount, and after the list changes.
|
|
int schfs_index_reload(struct schfs_vol *v);
|
|
|
|
// Every object whose `name` attribute equals `val`. One tree descent, whatever
|
|
// the size of the filesystem. Returns how many ids were written, or negative.
|
|
int schfs_query_equal(struct schfs_vol *v, const char *name, unsigned namelen,
|
|
const void *val, unsigned vallen, uint64_t *out, int cap);
|
|
|
|
// 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 */
|