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

@ -121,21 +121,20 @@ linked into the kernel binary for a `ramroot` build.
--- ---
## The suite is currently red ## One known failure left
`make schfs-check` **exits non-zero at baseline**, and has for some time. Two `make schfs-check` passes. UndefinedBehaviorSanitizer still reports **31
separate causes, both in the harness rather than the filesystem, both itemised misaligned reads** from casting pointers into tree leaves where records are not
in [../plan/1.0.md](../plan/1.0.md): 8-aligned — itemised in [../plan/1.0.md](../plan/1.0.md), and printed on stderr
without failing the run.
- AddressSanitizer reports 2121728 bytes leaked in 518 allocations, all from the The leak half of this is closed, and how it closed is the useful part: the test
test's own page allocator, which never frees. leaked 518 pages because the allocation map had no release path *at all*, which
- UndefinedBehaviorSanitizer reports 31 misaligned reads, from casting pointers meant the kernel leaked the same map on every unmount. **A suite that leaks
into tree leaves where records are not 8-aligned. cannot detect a leak** — the filesystem's own allocations would have arrived as
noise inside the harness's. `schfs_objtest` now asserts that every page it
Worth knowing before you run it, and worth fixing rather than living with: the handed out came back, so the next one fails the suite rather than joining the
assertions still pass, so it is tempting to read the `[ok]` lines and ignore the background.
exit code — which is exactly how a suite stops being a check and becomes a
decoration.
## What is not tested ## What is not tested

View file

@ -146,18 +146,23 @@ it, and both predate the current work — confirmed by stashing and re-running.
They are here rather than filed as nice-to-haves because a suite that is red for They are here rather than filed as nice-to-haves because a suite that is red for
known reasons stops being read, and then it is red for unknown reasons too. known reasons stops being read, and then it is red for unknown reasons too.
- [ ] **`make schfs-check` fails at baseline.** AddressSanitizer reports - [x] ~~**`make schfs-check` fails at baseline.**~~ Fixed, and it was not only a
**2121728 bytes leaked in 518 allocations**, every one of them from test bug. The allocation map was taken through a `palloc` callback and
`md_palloc` (`tools/schfs_objtest.c:84`) — the test's own page allocator, there was **no way to give it back** — no `schfs_space_release`, and a
which hands out pages for the allocation map and never frees them. The comment in `schfs_space_init` reading *"caller frees what was taken"* for
target exits non-zero, so the *whole* SchFS suite is failing whatever the an API the caller did not have.
assertions say.
The cost is not the leaked memory, it is that **the leak check is now So the kernel leaked the whole map on **every unmount**, up to 2 MB a
useless**: a real leak in the filesystem would arrive as noise inside time, and `schfs_space_init`'s own error path stranded a page it had
2 MB of expected noise. Fixing it means the test freeing what it already taken because `npages` was only raised after *both* allocations
allocated, not suppressing the report — a suppression keeps the number succeeded. The test's 518 leaked pages were the symptom that pointed at
quiet and keeps the detector off. it.
The map now carries its own `pfree` so it can only be freed by the
allocator that filled it, `schfs_vol_open`/`schfs_vol_format` release any
map the volume already holds, and unmount releases. `schfs_objtest`
asserts the balance — 520 pages taken, 520 given back — so the next leak
fails the suite instead of being absorbed by it.
- [ ] **31 UndefinedBehaviorSanitizer errors**, all one root cause: records are - [ ] **31 UndefinedBehaviorSanitizer errors**, all one root cause: records are
read straight out of a tree leaf by casting a pointer into the block, and read straight out of a tree leaf by casting a pointer into the block, and

View file

@ -447,6 +447,15 @@ kd_palloc(void)
return kalloc(); return kalloc();
} }
// The other half of kd_palloc, and the reason unmount stopped leaking: the
// allocation map is up to SCHFS_SPACE_PAGES pages of kalloc'd memory, and
// nothing ever gave it back. A mount/unmount cycle leaked the lot.
static void
kd_pfree(void *p)
{
kfree((char *)p);
}
// Mount the SchFS volume on block device `bdev` into slot `slot`. // Mount the SchFS volume on block device `bdev` into slot `slot`.
// //
// Never panics: a disk that is not SchFS, or is an unreadable SchFS, simply // Never panics: a disk that is not SchFS, or is an unreadable SchFS, simply
@ -495,7 +504,7 @@ kmount_slot(int slot, uint bdev)
// Opening with a page allocator also builds the allocation bitmap, by walking // Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount // the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted. // that cannot disagree with the filesystem it just adopted.
int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc); int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc, kd_pfree);
if (r == -1) if (r == -1)
return -EINVAL; /* not a SchFS volume at all */ return -EINVAL; /* not a SchFS volume at all */
if (r == -4) { if (r == -4) {
@ -594,6 +603,10 @@ schfs_kumount(uint dev)
acquiresleep(&m->lock); acquiresleep(&m->lock);
bflush(m->kdev.dev); bflush(m->kdev.dev);
// Give the allocation map back. It is rebuilt from the tree on every mount,
// so there is nothing in it worth keeping -- and at 8 KiB per GiB of volume,
// keeping it was the largest thing this kernel leaked.
schfs_space_release(&m->vol.space);
m->mounted = 0; m->mounted = 0;
releasesleep(&m->lock); releasesleep(&m->lock);
return 0; return 0;

View file

@ -45,10 +45,22 @@ struct schfs_space {
uint64_t hint; // rotating search start uint64_t hint; // rotating search start
// SCHFS_SPACE_RETAIN_ALL while a snapshot exists -- see schfs_space_free. // SCHFS_SPACE_RETAIN_ALL while a snapshot exists -- see schfs_space_free.
unsigned flags; 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. // 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 // 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 // leaves the tree talking to whichever struct it was built in. Open into the
// storage it will live in. // 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, // yields pages for the allocation bitmap and may be NULL for a read-only mount,
// which then cannot allocate at all. // which then cannot allocate at all.
int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, 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. // Lay down an empty volume: one empty tree and one uberblock.
int schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev, int schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label, 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 // 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 // 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 // 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. // map (see SCHFS_MAX_BLOCKS), -5 if palloc ran out.
int schfs_space_init(struct schfs_space *s, uint64_t nblocks, 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 // 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 // may try several uberblocks, and each candidate root needs its scan to start

View file

@ -126,11 +126,26 @@ schfs_space_reset(struct schfs_space *s, uint64_t nblocks)
bit_set(s->inuse, b); bit_set(s->inuse, b);
} }
void
schfs_space_release(struct schfs_space *s)
{
if (s->pfree) {
for (int i = 0; i < s->npages; i++) {
if (s->inuse[i]) s->pfree(s->inuse[i]);
if (s->pend[i]) s->pfree(s->pend[i]);
}
}
// Zeroed rather than merely emptied, so calling this twice is harmless and
// a released map cannot be mistaken for a sized one.
memset(s, 0, sizeof(*s));
}
int int
schfs_space_init(struct schfs_space *s, uint64_t nblocks, schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void)) void *(*palloc)(void), void (*pfree)(void *))
{ {
memset(s, 0, sizeof(*s)); memset(s, 0, sizeof(*s));
s->pfree = pfree;
if (nblocks <= SCHFS_FIRST_BLOCK) if (nblocks <= SCHFS_FIRST_BLOCK)
return -3; return -3;
@ -141,16 +156,26 @@ schfs_space_init(struct schfs_space *s, uint64_t nblocks,
/ SCHFS_SPACE_BITS_PER_PAGE; / SCHFS_SPACE_BITS_PER_PAGE;
for (uint64_t i = 0; i < npages; i++) { for (uint64_t i = 0; i < npages; i++) {
// npages is raised as soon as inuse[i] is ours, not after both pages
// are. It used to be raised after, which meant a failure on the second
// allocation left the first one owned by nobody: outside the count, so
// unreachable to any cleanup, and never freed. The comment here said
// "caller frees what was taken" and there was nothing the caller could
// call.
void *p = palloc(); void *p = palloc();
if (p == 0) if (p == 0) {
return -5; /* caller frees what was taken */ schfs_space_release(s);
return -5;
}
s->inuse[i] = (uint64_t *)p; s->inuse[i] = (uint64_t *)p;
s->npages = (int)(i + 1);
p = palloc(); p = palloc();
if (p == 0) if (p == 0) {
schfs_space_release(s);
return -5; return -5;
}
s->pend[i] = (uint64_t *)p; s->pend[i] = (uint64_t *)p;
s->npages = (int)(i + 1);
} }
schfs_space_reset(s, nblocks); schfs_space_reset(s, nblocks);

View file

@ -124,7 +124,8 @@ vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot)
int int
schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, 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 *))
{ {
struct schfs_uberblock *u = (struct schfs_uberblock *)x->a; struct schfs_uberblock *u = (struct schfs_uberblock *)x->a;
int io_errors = 0; int io_errors = 0;
@ -238,7 +239,16 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
// Sizing failures are the caller's problem, not this root's: // Sizing failures are the caller's problem, not this root's:
// a volume too large to map or a machine out of pages will // a volume too large to map or a machine out of pages will
// fail identically on every slot in the ring. // fail identically on every slot in the ring.
int sr = schfs_space_init(&v->space, v->total_blocks, palloc); // A volume that has been opened before still holds the
// previous map, and schfs_space_init memsets it away -- so
// without this every re-open leaks one. schfs_objtest reopens
// the same volume nine times.
//
// Safe on a first open because a struct schfs_vol must be
// zeroed before first use, which is what release reads: a
// zeroed map has no pfree and no pages, so it does nothing.
schfs_space_release(&v->space);
int sr = schfs_space_init(&v->space, v->total_blocks, palloc, pfree);
if (sr != 0) if (sr != 0)
return sr; return sr;
spaced = 1; spaced = 1;
@ -350,11 +360,16 @@ schfs_vol_commit(struct schfs_vol *v)
int int
schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev, schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label, uint64_t total_blocks, const char *label,
struct schfs_ctx *x, void *(*palloc)(void)) struct schfs_ctx *x, void *(*palloc)(void),
void (*pfree)(void *))
{ {
if (total_blocks <= SCHFS_FIRST_BLOCK) if (total_blocks <= SCHFS_FIRST_BLOCK)
return -3; return -3;
// Same reasoning as the release in schfs_vol_open: formatting over a
// volume that was previously open would otherwise drop its map on the
// floor, and the memset below is what makes it unrecoverable.
schfs_space_release(&v->space);
memset(v, 0, sizeof(*v)); memset(v, 0, sizeof(*v));
v->dev = *dev; v->dev = *dev;
v->ctx = *x; v->ctx = *x;
@ -365,7 +380,7 @@ schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
// Nothing is allocated on a fresh volume beyond the boot block and the // Nothing is allocated on a fresh volume beyond the boot block and the
// ring, so the map starts empty rather than being scanned into existence. // ring, so the map starts empty rather than being scanned into existence.
{ {
int sr = schfs_space_init(&v->space, total_blocks, palloc); int sr = schfs_space_init(&v->space, total_blocks, palloc, pfree);
if (sr != 0) if (sr != 0)
return sr; return sr;
} }

View file

@ -168,6 +168,12 @@ img_palloc(void)
return calloc(1, SCHFS_BLKSIZE); return calloc(1, SCHFS_BLKSIZE);
} }
static void
img_pfree(void *p)
{
free(p);
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Content dedup */ /* Content dedup */
/* */ /* */
@ -688,7 +694,7 @@ main(int argc, char **argv)
bd.balloc_run = img_alloc_run; bd.balloc_run = img_alloc_run;
bd.bflush = img_flush; bd.bflush = img_flush;
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc); int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc, img_pfree);
if (r != 0) die("formatting", r); if (r != 0) die("formatting", r);
// The root directory is an object like any other; nothing points at it // The root directory is an object like any other; nothing points at it

View file

@ -113,6 +113,7 @@ static void img_nofree(void *ctx, uint64_t b) { (void)ctx; (void)b; }
// Pages for the allocation map --check derives. Nothing is written back; this // Pages for the allocation map --check derives. Nothing is written back; this
// is a check on the map, not a use of it. // is a check on the map, not a use of it.
static void *img_palloc(void) { return calloc(1, SCHFS_BLKSIZE); } static void *img_palloc(void) { return calloc(1, SCHFS_BLKSIZE); }
static void img_pfree(void *p) { free(p); }
static struct schfs_vol VOL; static struct schfs_vol VOL;
static char CURBLK[SCHFS_BLKSIZE]; static char CURBLK[SCHFS_BLKSIZE];
@ -367,7 +368,7 @@ do_check(void)
// stored, so nothing else would ever notice if the scan and the writer // stored, so nothing else would ever notice if the scan and the writer
// disagreed — right up until a mounted volume handed out a block that was // disagreed — right up until a mounted volume handed out a block that was
// still in use. // still in use.
if (schfs_space_init(&VOL.space, VOL.total_blocks, img_palloc) != 0) { if (schfs_space_init(&VOL.space, VOL.total_blocks, img_palloc, img_pfree) != 0) {
fprintf(stderr, " FAIL: could not build an allocation map\n"); fprintf(stderr, " FAIL: could not build an allocation map\n");
return 1; return 1;
} }
@ -785,7 +786,7 @@ main(int argc, char **argv)
// No page allocator: this tool never writes, and a volume without an // No page allocator: this tool never writes, and a volume without an
// allocation map cannot allocate even by accident. // allocation map cannot allocate even by accident.
int r = schfs_vol_open(&VOL, &bd, &CTX, NULL); int r = schfs_vol_open(&VOL, &bd, &CTX, NULL, NULL);
if (r == -1) { if (r == -1) {
fprintf(stderr, "schfs_fsck: %s: no valid uberblock -- not a SchFS " fprintf(stderr, "schfs_fsck: %s: no valid uberblock -- not a SchFS "
"volume, or never successfully written\n", imgpath); "volume, or never successfully written\n", imgpath);

View file

@ -81,7 +81,13 @@ md_write(void *ctx, uint64_t b, const void *buf)
static uint64_t md_alloc(void *ctx) { (void)ctx; return schfs_vol_alloc(&VOL); } static uint64_t md_alloc(void *ctx) { (void)ctx; return schfs_vol_alloc(&VOL); }
static uint64_t md_run(void *ctx, uint64_t n) { (void)ctx; return schfs_vol_alloc_run(&VOL, n); } static uint64_t md_run(void *ctx, uint64_t n) { (void)ctx; return schfs_vol_alloc_run(&VOL, n); }
static void md_free(void *ctx, uint64_t b) { (void)ctx; schfs_vol_free(&VOL, b); } static void md_free(void *ctx, uint64_t b) { (void)ctx; schfs_vol_free(&VOL, b); }
static void *md_palloc(void) { return calloc(1, SCHFS_BLKSIZE); } static long md_pages_taken, md_pages_given;
static void *md_palloc(void) { md_pages_taken++; return calloc(1, SCHFS_BLKSIZE); }
// The matching half. Without it every volume this test opened leaked its whole
// allocation map -- 2121728 bytes across 518 allocations by the end, which made
// the target exit non-zero and drowned any real leak the filesystem might have
// had in expected noise.
static void md_pfree(void *p) { md_pages_given++; free(p); }
static void static void
mkbdev(struct schfs_bdev *bd) mkbdev(struct schfs_bdev *bd)
@ -221,8 +227,9 @@ remount(void)
// Straight back into VOL, not into a local that is then copied over it: a // Straight back into VOL, not into a local that is then copied over it: a
// schfs_vol points at its own dev field, so a copy of one leaves the tree // schfs_vol points at its own dev field, so a copy of one leaves the tree
// talking to whichever struct it was built in. // talking to whichever struct it was built in.
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
if (schfs_vol_open(&VOL, &bd2, &CTX, md_palloc) != 0) { if (schfs_vol_open(&VOL, &bd2, &CTX, md_palloc, md_pfree) != 0) {
printf(" FAIL: remount\n"); printf(" FAIL: remount\n");
failures++; failures++;
return ~0ULL; return ~0ULL;
@ -273,7 +280,7 @@ main(void)
printf("SchFS object-layer test\n\n"); printf("SchFS object-layer test\n\n");
if (schfs_vol_format(&VOL, &bd, NBLOCKS, "objtest", &CTX, md_palloc) != 0) { if (schfs_vol_format(&VOL, &bd, NBLOCKS, "objtest", &CTX, md_palloc, md_pfree) != 0) {
fprintf(stderr, "FAIL: format\n"); fprintf(stderr, "FAIL: format\n");
return 1; return 1;
} }
@ -917,8 +924,9 @@ main(void)
struct schfs_bdev bd3; struct schfs_bdev bd3;
mkbdev(&bd3); mkbdev(&bd3);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
int r = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc); int r = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree);
ok(r == 0, "the volume mounts after a crash mid-transaction"); ok(r == 0, "the volume mounts after a crash mid-transaction");
ok(VOL.generation == good_gen, ok(VOL.generation == good_gen,
"it comes back at the last committed generation, not a partial one"); "it comes back at the last committed generation, not a partial one");
@ -964,9 +972,10 @@ main(void)
for (long cut = 1; cut < 120; cut++) { for (long cut = 1; cut < 120; cut++) {
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE); memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) { if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree) != 0) {
bad++; break; bad++; break;
} }
@ -994,9 +1003,10 @@ main(void)
// Whatever the disk looks like now, all three of these must // Whatever the disk looks like now, all three of these must
// hold: it mounts, it is a whole tree, and the data committed // hold: it mounts, it is a whole tree, and the data committed
// before the crash is exactly as it was. // before the crash is exactly as it was.
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) { if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree) != 0) {
printf(" cut %ld: does not mount\n", cut); printf(" cut %ld: does not mount\n", cut);
bad++; continue; bad++; continue;
} }
@ -1040,9 +1050,10 @@ main(void)
// the root. Before the fallback existed the mount simply refused, with // the root. Before the fallback existed the mount simply refused, with
// 127 intact older roots sitting on the disk untouched. // 127 intact older roots sitting on the disk untouched.
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE); memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) { if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree) != 0) {
printf(" FAIL: reopen for the misdirected-write case\n"); printf(" FAIL: reopen for the misdirected-write case\n");
failures++; failures++;
} else { } else {
@ -1062,9 +1073,10 @@ main(void)
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
memset(DISK + roots[i] * SCHFS_BLKSIZE, 0xA5, SCHFS_BLKSIZE); memset(DISK + roots[i] * SCHFS_BLKSIZE, 0xA5, SCHFS_BLKSIZE);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
int rr = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc); int rr = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree);
ok(rr == 0, "a volume whose two newest roots are rubbish still mounts"); ok(rr == 0, "a volume whose two newest roots are rubbish still mounts");
// Strictly older, not "exactly two older". Copy-on-write shares // Strictly older, not "exactly two older". Copy-on-write shares
// every unchanged node between generations, so a block that is part // every unchanged node between generations, so a block that is part
@ -1088,19 +1100,35 @@ main(void)
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE); memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
memset(DISK + SCHFS_UBER_START * SCHFS_BLKSIZE, 0, memset(DISK + SCHFS_UBER_START * SCHFS_BLKSIZE, 0,
SCHFS_UBER_COUNT * SCHFS_BLKSIZE); SCHFS_UBER_COUNT * SCHFS_BLKSIZE);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
ok(schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0, ok(schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree) != 0,
"a volume with no valid uberblock at all is refused, not invented"); "a volume with no valid uberblock at all is refused, not invented");
// Put the good image back so anything after this runs on a sane volume. // Put the good image back so anything after this runs on a sane volume.
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE); memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
schfs_space_release(&VOL.space); /* before the memset strands it */
memset(&VOL, 0, sizeof(VOL)); memset(&VOL, 0, sizeof(VOL));
mkbdev(&bd3); mkbdev(&bd3);
schfs_vol_open(&VOL, &bd3, &CTX, md_palloc); schfs_vol_open(&VOL, &bd3, &CTX, md_palloc, md_pfree);
free(saved); free(saved);
} }
// The last volume opened is still holding its map. Nothing else will take
// it, and a test that leaks cannot detect a leak: the filesystem's own
// allocations would arrive as noise inside this one.
schfs_space_release(&VOL.space);
if (md_pages_taken != md_pages_given) {
printf(" [FAIL] %ld allocation-map pages taken, %ld given back\n",
md_pages_taken, md_pages_given);
failures++;
} else {
printf(" [ok] every allocation-map page was given back (%ld)\n",
md_pages_taken);
}
printf("\n%s\n", failures ? "FAILED" : "all checks passed"); printf("\n%s\n", failures ? "FAILED" : "all checks passed");
return failures ? 1 : 0; return failures ? 1 : 0;
} }