schfs: the allocation map can be given back, so unmount stops leaking it

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
This commit is contained in:
Nexoniarz 2026-09-12 17:49:57 +02:00
commit 25530e7eed
9 changed files with 175 additions and 57 deletions

View file

@ -45,10 +45,22 @@ struct schfs_space {
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.
@ -112,12 +124,14 @@ struct schfs_vol {
// 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));
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));
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
@ -152,7 +166,19 @@ void schfs_vol_free(struct schfs_vol *v, uint64_t blk);
// 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 *(*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