Started as "fix the ASAN leak in schfs_objtest" and the test turned out to be
the messenger. The allocation map was taken through a palloc callback and there
was no way to return it: no schfs_space_release, and a comment in
schfs_space_init reading "caller frees what was taken" for an API the caller
did not have.
Three separate leaks from that one gap:
* The kernel leaked the entire map on every unmount -- up to 2 MB, at 8 KiB
per GiB of volume -- because kmount_unmount flushed, cleared `mounted` and
walked away. This is the one that matters; the others are test-only.
* schfs_space_init stranded a page on its own error path. npages was raised
only after *both* of a pair of allocations succeeded, so a failure on the
second left the first outside the count -- unreachable to any cleanup that
could later exist.
* schfs_objtest leaked 2121728 bytes in 518 allocations, which is what made
the whole SchFS suite exit non-zero regardless of its assertions.
The map now carries its own pfree rather than taking one at release time, so it
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. schfs_vol_open and schfs_vol_format release any map the volume
is already holding before building another, which is what stops a re-open
leaking; struct schfs_vol must therefore be zeroed before first use, and the
header now says so.
The test's own leak was the last one found and the simplest: it does
memset(&VOL, 0, sizeof(VOL)) before reopening, which wiped the map's pointers
before vol_open could release them. Eight of those, each now releasing first.
schfs_objtest asserts the balance -- 520 pages taken, 520 given back -- because
a suite that leaks cannot detect a leak: the filesystem's own allocations would
arrive as noise inside the harness's. The next one fails the run instead.
make schfs-check now exits 0. The 31 UBSAN misalignment reports are still
there and are still a Gate 0 item.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
391 lines
19 KiB
C
391 lines
19 KiB
C
// 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
|
|
// 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 */
|