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
749 lines
27 KiB
C
749 lines
27 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// mkschfs — build a SchFS image from a host directory tree.
|
|
//
|
|
// This is the tool that makes the on-disk format real: everything the kernel
|
|
// mounts in phase 2 is produced here. It deliberately shares the actual
|
|
// filesystem code (btree.c, vol.c, obj.c, objw.c) rather than reimplementing
|
|
// the layout, so a disagreement between writer and reader is impossible by
|
|
// construction — there is only one implementation, exercised on the host where
|
|
// a mistake costs a second instead of a boot.
|
|
//
|
|
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]
|
|
//
|
|
// Two behaviours worth knowing about:
|
|
//
|
|
// * Identical file contents are stored once and hard-linked. sysroot/bin
|
|
// holds a full copy of the toybox binary per applet, so this is the
|
|
// difference between a 190 MB image and a 6 MB one.
|
|
// * Output is reproducible. Directory entries are sorted, the UUID is
|
|
// derived rather than random, and timestamps come from the source files,
|
|
// so building the same tree twice gives byte-identical images and a diff
|
|
// means something actually changed.
|
|
|
|
#define _FILE_OFFSET_BITS 64
|
|
#define SCHFS_HOSTED 1
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
/* major()/minor() for device nodes. glibc puts them here; mingw has no such
|
|
header and no device numbers at all, because Windows has no mknod. On that
|
|
host the macros are stubbed: a device node cannot exist in the source tree
|
|
being imaged, so the values are never anything but zero, and stubbing is
|
|
honest where pretending to read st_rdev would not be. */
|
|
#ifdef __MINGW32__
|
|
#define major(x) 0
|
|
#define minor(x) 0
|
|
/* Symlinks, likewise. Windows has no lstat/readlink and mingw exposes neither,
|
|
so on this host a symlink cannot be encountered while walking the tree --
|
|
lstat degrades to stat, nothing is ever S_ISLNK, and readlink can only be
|
|
reached through a branch that is now unreachable. Verified rather than
|
|
assumed: `find sysroot -type l` returns nothing.
|
|
|
|
If symlinks are ever staged (procfs wants /proc/<pid>/exe to be one), this
|
|
host stops being able to build a faithful image and the stub must become a
|
|
real Win32 reparse-point read. Failing loudly then is the point of the
|
|
abort() below. */
|
|
#define lstat(p, b) stat((p), (b))
|
|
#define S_ISLNK(m) 0
|
|
static long readlink(const char *p, char *b, unsigned long n)
|
|
{ (void)p; (void)b; (void)n; return -1; }
|
|
#elif defined(__linux__)
|
|
/* glibc and musl both put major()/minor() here, and glibc warns and then
|
|
errors if you take them from <sys/types.h> instead. Linux only: macOS and
|
|
the BSDs have no <sys/sysmacros.h> at all and define both in <sys/types.h>,
|
|
which is already included above -- including it unconditionally is a build
|
|
failure on every non-Linux Unix. */
|
|
#include <sys/sysmacros.h>
|
|
#endif
|
|
|
|
#include "../kernel/fs/schfs/btree.c"
|
|
#include "../kernel/fs/schfs/space.c"
|
|
#include "../kernel/fs/schfs/vol.c"
|
|
#include "../kernel/fs/schfs/obj.c"
|
|
#include "../kernel/fs/schfs/objw.c"
|
|
#include "../kernel/fs/schfs/attr.c"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* File-backed block device */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
struct imgdev {
|
|
FILE *f;
|
|
struct schfs_vol *vol;
|
|
uint64_t nblocks;
|
|
long reads, writes;
|
|
};
|
|
|
|
static struct imgdev IMG;
|
|
|
|
static int
|
|
img_read(void *ctx, uint64_t b, void *buf)
|
|
{
|
|
struct imgdev *d = ctx;
|
|
if (b >= d->nblocks)
|
|
return -1;
|
|
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
|
|
return -1;
|
|
if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
|
|
return -1;
|
|
d->reads++;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
img_write(void *ctx, uint64_t b, const void *buf)
|
|
{
|
|
struct imgdev *d = ctx;
|
|
if (b >= d->nblocks)
|
|
return -1;
|
|
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
|
|
return -1;
|
|
if (fwrite(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
|
|
return -1;
|
|
d->writes++;
|
|
return 0;
|
|
}
|
|
|
|
// Make what has been written actually reach the file.
|
|
//
|
|
// The kernel's version of this hook drains the buffer cache and then tells the
|
|
// drive to commit its own cache; here the equivalent stages are stdio's buffer
|
|
// and the operating system's page cache. The image mkschfs is building is
|
|
// normally consumed by something else the moment it exits, so this is mostly
|
|
// belt and braces -- but the commit path calls it, and a hook that lies about
|
|
// having flushed is exactly the bug this whole change set is about.
|
|
static int
|
|
img_flush(void *ctx)
|
|
{
|
|
struct imgdev *d = ctx;
|
|
if (fflush(d->f) != 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Allocation */
|
|
/* */
|
|
/* Straight through to the volume's space manager. An earlier version */
|
|
/* kept a build-time free list of its own, because the bump allocator it */
|
|
/* replaced could only ever take back the block it handed out last and */
|
|
/* a build otherwise leaked one root-to-leaf path per insert. Two things */
|
|
/* made that unnecessary: the space manager reuses any freed block, and */
|
|
/* the tree now rewrites its own nodes in place within a transaction */
|
|
/* group, so a build barely frees anything in the first place. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static uint64_t
|
|
img_alloc(void *ctx)
|
|
{
|
|
return schfs_vol_alloc(((struct imgdev *)ctx)->vol);
|
|
}
|
|
|
|
static uint64_t
|
|
img_alloc_run(void *ctx, uint64_t n)
|
|
{
|
|
return schfs_vol_alloc_run(((struct imgdev *)ctx)->vol, n);
|
|
}
|
|
|
|
static void
|
|
img_free(void *ctx, uint64_t b)
|
|
{
|
|
schfs_vol_free(((struct imgdev *)ctx)->vol, b);
|
|
}
|
|
|
|
// Pages for the allocation bitmap. calloc rather than malloc so a bug that
|
|
// forgets to initialise one shows up as "everything is free" consistently
|
|
// rather than as whatever the heap happened to contain.
|
|
static void *
|
|
img_palloc(void)
|
|
{
|
|
return calloc(1, SCHFS_BLKSIZE);
|
|
}
|
|
|
|
static void
|
|
img_pfree(void *p)
|
|
{
|
|
free(p);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Content dedup */
|
|
/* */
|
|
/* sysroot/bin contains one independent ~800 KB copy of toybox per */
|
|
/* applet name. Storing each separately is the difference between an */
|
|
/* image that fits in RAM and one that does not, so identical content */
|
|
/* becomes one object with several names -- a plain hard link, which */
|
|
/* the format already supports through nlink. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
struct dedup_ent {
|
|
char *path;
|
|
dev_t hdev; /* host device+inode: when these match, */
|
|
ino_t hino; /* it is literally the same file */
|
|
off_t size;
|
|
uint32_t crc;
|
|
uint64_t ino;
|
|
};
|
|
|
|
static struct dedup_ent *dedup_tab;
|
|
static int dedup_n, dedup_cap;
|
|
|
|
/* A file the host already told us is the same file.
|
|
*
|
|
* ports/toybox/Makefile hard-links its 236 applet names to one binary, so this
|
|
* settles all but the first of them without reading anything: the content path
|
|
* below reads the candidate to CRC it and then re-reads both files to confirm,
|
|
* which is half a gigabyte of I/O for an answer fstat already has. */
|
|
static uint64_t
|
|
dedup_lookup_ino(dev_t hdev, ino_t hino)
|
|
{
|
|
if (hino == 0)
|
|
return 0;
|
|
for (int i = 0; i < dedup_n; i++)
|
|
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
|
|
return dedup_tab[i].ino;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
files_identical(const char *a, const char *b)
|
|
{
|
|
FILE *fa = fopen(a, "rb"), *fb = fopen(b, "rb");
|
|
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return 0; }
|
|
int same = 1;
|
|
for (;;) {
|
|
char ba[8192], bb[8192];
|
|
size_t na = fread(ba, 1, sizeof(ba), fa);
|
|
size_t nb = fread(bb, 1, sizeof(bb), fb);
|
|
if (na != nb || memcmp(ba, bb, na) != 0) { same = 0; break; }
|
|
if (na == 0) break;
|
|
}
|
|
fclose(fa); fclose(fb);
|
|
return same;
|
|
}
|
|
|
|
// Size and CRC agreeing is not proof, so a candidate is confirmed byte by byte.
|
|
// Silently hard-linking two files that merely collide would corrupt the image
|
|
// in a way nothing downstream could detect.
|
|
static uint64_t
|
|
dedup_lookup(const char *path, off_t size, uint32_t crc)
|
|
{
|
|
for (int i = 0; i < dedup_n; i++)
|
|
if (dedup_tab[i].size == size && dedup_tab[i].crc == crc &&
|
|
files_identical(dedup_tab[i].path, path))
|
|
return dedup_tab[i].ino;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
dedup_remember(const char *path, const struct stat *st,
|
|
off_t size, uint32_t crc, uint64_t ino)
|
|
{
|
|
dev_t dedup_hdev = st->st_nlink > 1 ? st->st_dev : 0;
|
|
ino_t dedup_hino = st->st_nlink > 1 ? st->st_ino : 0;
|
|
|
|
if (dedup_n == dedup_cap) {
|
|
dedup_cap = dedup_cap ? dedup_cap * 2 : 256;
|
|
dedup_tab = realloc(dedup_tab, (size_t)dedup_cap * sizeof(*dedup_tab));
|
|
if (!dedup_tab) { perror("realloc"); exit(1); }
|
|
}
|
|
dedup_tab[dedup_n].path = strdup(path);
|
|
dedup_tab[dedup_n].hdev = dedup_hdev;
|
|
dedup_tab[dedup_n].hino = dedup_hino;
|
|
dedup_tab[dedup_n].size = size;
|
|
dedup_tab[dedup_n].crc = crc;
|
|
dedup_tab[dedup_n].ino = ino;
|
|
dedup_n++;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static struct schfs_vol VOL;
|
|
static char DATABLK[SCHFS_BLKSIZE];
|
|
static char CURBLK[SCHFS_BLKSIZE];
|
|
|
|
// The volume's three scratch blocks. Statics rather than locals because the
|
|
// same code runs in the kernel, where a 4096-byte local is the whole stack —
|
|
// keeping the shapes identical is what makes the host the place bugs are found.
|
|
static char CTX_A[SCHFS_BLKSIZE];
|
|
static char CTX_B[SCHFS_BLKSIZE];
|
|
static char CTX_C[SCHFS_BLKSIZE];
|
|
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
|
|
|
|
static long nfiles, ndirs, nlinks, nsymlinks, ndevs;
|
|
static long long nbytes, ndeduped;
|
|
|
|
static void
|
|
die(const char *what, int r)
|
|
{
|
|
fprintf(stderr, "mkschfs: %s failed (%d)%s\n", what, r,
|
|
r == -3 ? " -- image is full, try a larger --size" : "");
|
|
exit(1);
|
|
}
|
|
|
|
static void
|
|
inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
|
|
uint64_t parent)
|
|
{
|
|
memset(in, 0, sizeof(*in));
|
|
in->type = type;
|
|
in->parent = parent;
|
|
in->nlink = 1;
|
|
in->mode = st ? (uint32_t)(st->st_mode & 07777)
|
|
: (type == SCHFS_T_DIR ? 0755u : 0644u);
|
|
in->uid = 0;
|
|
in->gid = 0;
|
|
if (st) {
|
|
// All three come from mtime, deliberately.
|
|
//
|
|
// Copying the host's atime would make the output depend on when the
|
|
// source files were last *read* -- and mkschfs reads every one of them,
|
|
// so building the same tree twice produced two different images. ctime
|
|
// is similar: it tracks host inode changes that mean nothing inside the
|
|
// image. mtime is the only one that describes the content, which is the
|
|
// only thing being copied.
|
|
in->atime = (uint64_t)st->st_mtime;
|
|
in->mtime = (uint64_t)st->st_mtime;
|
|
in->ctime = (uint64_t)st->st_mtime;
|
|
}
|
|
}
|
|
|
|
static void
|
|
bump_nlink(uint64_t ino)
|
|
{
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
|
struct schfs_inode in;
|
|
int r = schfs_inode_get(&VOL, &c, ino, &in);
|
|
if (r != 0)
|
|
die("re-reading an inode for a hard link", r);
|
|
in.nlink++;
|
|
// Under v1 this was the bug that shipped broken images: nlink was bumped
|
|
// but the inode's checksum was not recomputed, so every deduped file failed
|
|
// its own integrity check on first open. Here the checksum belongs to the
|
|
// tree node and is recomputed by the insert, so it cannot get out of step.
|
|
r = schfs_inode_put(&VOL, ino, &in);
|
|
if (r != 0)
|
|
die("writing back a hard link count", r);
|
|
}
|
|
|
|
static uint64_t
|
|
add_file(uint64_t parent, const char *host, const char *name,
|
|
const struct stat *st)
|
|
{
|
|
// A hard link is the same inode by definition, so ask the host before
|
|
// reading anything.
|
|
if (st->st_nlink > 1) {
|
|
uint64_t hit = dedup_lookup_ino(st->st_dev, st->st_ino);
|
|
if (hit) {
|
|
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
|
hit, SCHFS_T_FILE);
|
|
if (r != 0)
|
|
die("linking a deduplicated file", r);
|
|
bump_nlink(hit);
|
|
nlinks++;
|
|
ndeduped += st->st_size;
|
|
return hit;
|
|
}
|
|
}
|
|
|
|
// CRC the content: it is both the dedup key and a cheap way to avoid
|
|
// reading candidates that cannot possibly match.
|
|
FILE *f = fopen(host, "rb");
|
|
if (!f) { perror(host); exit(1); }
|
|
uint32_t crc = 0;
|
|
for (;;) {
|
|
char buf[8192];
|
|
size_t n = fread(buf, 1, sizeof(buf), f);
|
|
if (n == 0) break;
|
|
crc = schfs_crc32c(crc, buf, (unsigned)n);
|
|
}
|
|
|
|
uint64_t existing = dedup_lookup(host, st->st_size, crc);
|
|
if (existing) {
|
|
fclose(f);
|
|
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
|
existing, SCHFS_T_FILE);
|
|
if (r != 0)
|
|
die("linking a deduplicated file", r);
|
|
bump_nlink(existing);
|
|
nlinks++;
|
|
ndeduped += st->st_size;
|
|
return existing;
|
|
}
|
|
|
|
// The final size is known before a byte is written, so the block count goes
|
|
// into the inode on its first and only write, and the whole run of data
|
|
// blocks is claimed up front. That is what collapses a multi-megabyte file
|
|
// into a single extent record instead of one per 4 KB block.
|
|
uint64_t need = ((uint64_t)st->st_size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
|
|
|
uint64_t ino = schfs_alloc_objectid(&VOL);
|
|
struct schfs_inode in;
|
|
inode_init(&in, st, SCHFS_T_FILE, parent);
|
|
in.blocks = need;
|
|
int r = schfs_inode_put(&VOL, ino, &in);
|
|
if (r != 0) die("writing a file inode", r);
|
|
|
|
r = schfs_file_reserve(&VOL, ino, need);
|
|
if (r != 0) die("reserving data blocks", r);
|
|
|
|
rewind(f);
|
|
for (;;) {
|
|
// Large chunks on purpose: each append call re-reads and rewrites the
|
|
// inode, and every tree write copies its path, so appending a big file
|
|
// 8 KB at a time spends more space on metadata churn than on data.
|
|
static char buf[256 * 1024];
|
|
size_t n = fread(buf, 1, sizeof(buf), f);
|
|
if (n == 0) break;
|
|
r = schfs_file_append(&VOL, ino, buf, (unsigned)n, DATABLK);
|
|
if (r != 0) die("writing file data", r);
|
|
nbytes += (long long)n;
|
|
}
|
|
fclose(f);
|
|
|
|
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
|
ino, SCHFS_T_FILE);
|
|
if (r != 0) die("adding a directory entry", r);
|
|
|
|
dedup_remember(host, st, st->st_size, crc, ino);
|
|
nfiles++;
|
|
return ino;
|
|
}
|
|
|
|
static void
|
|
add_symlink(uint64_t parent, const char *host, const char *name,
|
|
const struct stat *st)
|
|
{
|
|
char target[4096];
|
|
ssize_t n = readlink(host, target, sizeof(target) - 1);
|
|
if (n < 0) { perror(host); exit(1); }
|
|
target[n] = '\0';
|
|
|
|
uint64_t need = ((uint64_t)n + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
|
uint64_t ino = schfs_alloc_objectid(&VOL);
|
|
struct schfs_inode in;
|
|
inode_init(&in, st, SCHFS_T_SYMLINK, parent);
|
|
in.blocks = need;
|
|
int r = schfs_inode_put(&VOL, ino, &in);
|
|
if (r != 0) die("writing a symlink inode", r);
|
|
|
|
// The target is stored as the object's data, exactly like a file's
|
|
// contents, so nothing special is needed to read it back.
|
|
r = schfs_file_reserve(&VOL, ino, need);
|
|
if (r != 0) die("reserving a symlink block", r);
|
|
r = schfs_file_append(&VOL, ino, target, (unsigned)n, DATABLK);
|
|
if (r != 0) die("writing a symlink target", r);
|
|
|
|
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
|
ino, SCHFS_T_SYMLINK);
|
|
if (r != 0) die("adding a symlink entry", r);
|
|
nsymlinks++;
|
|
}
|
|
|
|
static void
|
|
add_dev(uint64_t parent, const char *name, const struct stat *st)
|
|
{
|
|
uint64_t ino = schfs_alloc_objectid(&VOL);
|
|
struct schfs_inode in;
|
|
inode_init(&in, st, SCHFS_T_DEV, parent);
|
|
in.rdev_major = (uint16_t)major(st->st_rdev);
|
|
in.rdev_minor = (uint16_t)minor(st->st_rdev);
|
|
int r = schfs_inode_put(&VOL, ino, &in);
|
|
if (r != 0) die("writing a device inode", r);
|
|
|
|
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
|
ino, SCHFS_T_DEV);
|
|
if (r != 0) die("adding a device entry", r);
|
|
ndevs++;
|
|
}
|
|
|
|
/* Sorted directory listing, so images are reproducible. */
|
|
struct namelist { char **v; int n, cap; };
|
|
|
|
static void
|
|
nl_add(struct namelist *l, const char *s)
|
|
{
|
|
if (l->n == l->cap) {
|
|
l->cap = l->cap ? l->cap * 2 : 64;
|
|
l->v = realloc(l->v, (size_t)l->cap * sizeof(*l->v));
|
|
if (!l->v) { perror("realloc"); exit(1); }
|
|
}
|
|
l->v[l->n++] = strdup(s);
|
|
}
|
|
|
|
static int
|
|
nl_cmp(const void *a, const void *b)
|
|
{
|
|
return strcmp(*(char *const *)a, *(char *const *)b);
|
|
}
|
|
|
|
static void add_tree(uint64_t dirino, const char *hostdir);
|
|
|
|
// Paths kept out of the image, sysroot-relative.
|
|
//
|
|
// Same rule and same spelling as mkfs.c, because the two tools build the same
|
|
// tree and a path excluded from one must be excluded from the other. This is a
|
|
// licensing mechanism before it is a size one: the ramdisk image is linked into
|
|
// the kernel binary, so anything staged in sysroot/ when it is built becomes
|
|
// part of a distributable artifact.
|
|
#define MAXEXCLUDE 32
|
|
static const char *excludes[MAXEXCLUDE];
|
|
static int nexcludes;
|
|
// strlen(sysroot) + 1, so a host path can be turned back into the
|
|
// sysroot-relative one the exclude patterns are written against.
|
|
static size_t sysroot_prefix;
|
|
|
|
// Matches exactly, or as a directory prefix so that "usr/share/misc" also drops
|
|
// "usr/share/misc/big.dat".
|
|
static int
|
|
is_excluded(const char *relpath)
|
|
{
|
|
for (int i = 0; i < nexcludes; i++) {
|
|
size_t n = strlen(excludes[i]);
|
|
if (strncmp(relpath, excludes[i], n) == 0 &&
|
|
(relpath[n] == '\0' || relpath[n] == '/'))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Returns 1 if the entry was a directory, so the caller can total up the link
|
|
// count without walking the directory a second time.
|
|
static int
|
|
add_entry(uint64_t dirino, const char *hostdir, const char *name)
|
|
{
|
|
char path[8192];
|
|
snprintf(path, sizeof(path), "%s/%s", hostdir, name);
|
|
|
|
// Checked before the entry is dispatched, so an excluded directory is never
|
|
// opened and never gets an inode -- an empty directory left behind would
|
|
// still describe what was removed.
|
|
if (nexcludes && strlen(path) > sysroot_prefix &&
|
|
is_excluded(path + sysroot_prefix)) {
|
|
fprintf(stderr, "mkschfs: excluding %s\n", path + sysroot_prefix);
|
|
return 0;
|
|
}
|
|
|
|
struct stat st;
|
|
// lstat, not stat: a symlink must be stored as a symlink rather than
|
|
// silently turned into a second copy of whatever it points at.
|
|
if (lstat(path, &st) != 0) { perror(path); exit(1); }
|
|
|
|
if (strlen(name) > SCHFS_NAME_MAX) {
|
|
fprintf(stderr, "mkschfs: name too long (max %d): %s\n",
|
|
SCHFS_NAME_MAX, path);
|
|
exit(1);
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
uint64_t ino = schfs_alloc_objectid(&VOL);
|
|
struct schfs_inode in;
|
|
inode_init(&in, &st, SCHFS_T_DIR, dirino);
|
|
in.nlink = 2; /* itself and its own "." */
|
|
int r = schfs_inode_put(&VOL, ino, &in);
|
|
if (r != 0) die("writing a directory inode", r);
|
|
r = schfs_dir_add(&VOL, dirino, name, (unsigned)strlen(name),
|
|
ino, SCHFS_T_DIR);
|
|
if (r != 0) die("adding a subdirectory entry", r);
|
|
ndirs++;
|
|
add_tree(ino, path);
|
|
return 1;
|
|
}
|
|
if (S_ISLNK(st.st_mode))
|
|
add_symlink(dirino, path, name, &st);
|
|
else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
|
|
add_dev(dirino, name, &st);
|
|
else if (S_ISREG(st.st_mode))
|
|
add_file(dirino, path, name, &st);
|
|
else
|
|
fprintf(stderr, "mkschfs: skipping %s (unsupported file type)\n", path);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
add_tree(uint64_t dirino, const char *hostdir)
|
|
{
|
|
DIR *d = opendir(hostdir);
|
|
if (!d) { perror(hostdir); exit(1); }
|
|
|
|
struct namelist l = { 0, 0, 0 };
|
|
struct dirent *e;
|
|
while ((e = readdir(d)) != NULL) {
|
|
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
|
continue;
|
|
nl_add(&l, e->d_name);
|
|
}
|
|
closedir(d);
|
|
|
|
qsort(l.v, l.n, sizeof(*l.v), nl_cmp);
|
|
long subdirs = 0;
|
|
for (int i = 0; i < l.n; i++) {
|
|
subdirs += add_entry(dirino, hostdir, l.v[i]);
|
|
free(l.v[i]);
|
|
}
|
|
free(l.v);
|
|
|
|
// A directory's link count is 2 (itself, and its own ".") plus one per
|
|
// subdirectory, because each subdirectory's ".." points back here. ".." is
|
|
// synthetic in SchFS, but the count still has to match what POSIX tools
|
|
// expect, or "find -type d" prunes real directories and du walks the tree
|
|
// wrongly. Written last because the count is not known until the children
|
|
// have been added.
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
|
struct schfs_inode in;
|
|
if (schfs_inode_get(&VOL, &c, dirino, &in) != 0)
|
|
die("re-reading a directory inode", -2);
|
|
in.nlink = (uint32_t)(2 + subdirs);
|
|
if (schfs_inode_put(&VOL, dirino, &in) != 0)
|
|
die("writing a directory link count", -2);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
const char *out = NULL, *sysroot = NULL, *label = "schos";
|
|
// Take a snapshot of the finished tree. Mostly for testing the snapshot
|
|
// paths end to end -- an image that ships with one is a perfectly ordinary
|
|
// thing to want, and it is one call.
|
|
const char *snapname = NULL;
|
|
uint64_t nblocks = 6000;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--sysroot") == 0 && i + 1 < argc)
|
|
sysroot = argv[++i];
|
|
else if (strcmp(argv[i], "--size") == 0 && i + 1 < argc)
|
|
nblocks = strtoull(argv[++i], NULL, 0);
|
|
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
|
|
label = argv[++i];
|
|
else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc)
|
|
snapname = argv[++i];
|
|
else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {
|
|
if (nexcludes >= MAXEXCLUDE) {
|
|
fprintf(stderr, "mkschfs: too many --exclude paths (max %d)\n",
|
|
MAXEXCLUDE);
|
|
return 1;
|
|
}
|
|
excludes[nexcludes++] = argv[++i];
|
|
}
|
|
else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) {
|
|
// Testing only: shrink the name hash so collisions are the norm
|
|
// rather than a once-in-a-lifetime event. schfs_fsck takes the
|
|
// same flag and must be given the same value to read the image.
|
|
int bits = atoi(argv[++i]);
|
|
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
|
|
}
|
|
else if (argv[i][0] == '-') {
|
|
fprintf(stderr, "mkschfs: unknown option %s\n", argv[i]);
|
|
return 1;
|
|
} else if (!out)
|
|
out = argv[i];
|
|
else {
|
|
fprintf(stderr, "mkschfs: too many arguments\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (!out || !sysroot) {
|
|
fprintf(stderr,
|
|
"usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n"
|
|
" [--exclude PATH]... [--snapshot NAME]\n"
|
|
" BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n"
|
|
" --exclude omits a sysroot-relative path and its subtree\n",
|
|
SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024)));
|
|
return 1;
|
|
}
|
|
if (nblocks <= SCHFS_FIRST_BLOCK) {
|
|
fprintf(stderr, "mkschfs: --size must exceed %d blocks (boot block "
|
|
"plus the uberblock ring)\n", SCHFS_FIRST_BLOCK);
|
|
return 1;
|
|
}
|
|
|
|
FILE *f = fopen(out, "w+b");
|
|
if (!f) { perror(out); return 1; }
|
|
// Pre-extend so every block is readable. The bump allocator hands out
|
|
// blocks in order, but the tree reads a block back the moment it is
|
|
// written, and a short file would fail that read rather than the write.
|
|
if (ftruncate(fileno(f), (off_t)nblocks * SCHFS_BLKSIZE) != 0) {
|
|
perror("ftruncate");
|
|
return 1;
|
|
}
|
|
|
|
IMG.f = f;
|
|
IMG.vol = &VOL;
|
|
IMG.nblocks = nblocks;
|
|
|
|
struct schfs_bdev bd;
|
|
memset(&bd, 0, sizeof(bd)); /* optional hooks must read as absent */
|
|
bd.ctx = &IMG;
|
|
bd.bread = img_read;
|
|
bd.bwrite = img_write;
|
|
bd.balloc = img_alloc;
|
|
bd.bfree = img_free;
|
|
bd.balloc_run = img_alloc_run;
|
|
bd.bflush = img_flush;
|
|
|
|
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc, img_pfree);
|
|
if (r != 0) die("formatting", r);
|
|
|
|
// The root directory is an object like any other; nothing points at it
|
|
// except the uberblock's root_objectid.
|
|
struct schfs_inode root;
|
|
struct stat rst;
|
|
int have_rst = (lstat(sysroot, &rst) == 0);
|
|
inode_init(&root, have_rst ? &rst : NULL, SCHFS_T_DIR, 0);
|
|
root.mode = 0755;
|
|
root.nlink = 2;
|
|
r = schfs_inode_put(&VOL, VOL.root_objectid, &root);
|
|
if (r != 0) die("writing the root inode", r);
|
|
ndirs++;
|
|
|
|
// add_entry() builds host paths as "<sysroot>/<name>", so this is where the
|
|
// sysroot-relative part of such a path begins.
|
|
sysroot_prefix = strlen(sysroot) + 1;
|
|
|
|
add_tree(VOL.root_objectid, sysroot);
|
|
|
|
// Publish. Everything above this line is invisible to a reader; if the tool
|
|
// died half way through, the image would simply have no valid uberblock and
|
|
// be rejected as "not a SchFS volume" rather than mounted half-built.
|
|
r = schfs_vol_commit(&VOL);
|
|
if (r != 0) die("committing the uberblock", r);
|
|
|
|
// A snapshot of the finished tree, if one was asked for. After the commit
|
|
// above, because a snapshot names a *committed* root -- taking it before
|
|
// would record a root that only exists in memory.
|
|
if (snapname) {
|
|
unsigned n = (unsigned)strlen(snapname);
|
|
r = schfs_snap_create(&VOL, 1, snapname, n);
|
|
if (r != 0) die("creating the snapshot", r);
|
|
r = schfs_vol_commit(&VOL);
|
|
if (r != 0) die("committing the snapshot", r);
|
|
}
|
|
|
|
if (fflush(f) != 0 || fclose(f) != 0) { perror(out); return 1; }
|
|
|
|
uint64_t used = nblocks - VOL.free_blocks - SCHFS_FIRST_BLOCK;
|
|
printf("mkschfs: %s\n", out);
|
|
printf(" %ld dirs, %ld files, %ld hard links, %ld symlinks, %ld devices\n",
|
|
ndirs, nfiles, nlinks, nsymlinks, ndevs);
|
|
printf(" %lld bytes of data written, %lld bytes saved by dedup\n",
|
|
nbytes, ndeduped);
|
|
printf(" %llu of %llu blocks used (%llu KB of %llu KB), generation %llu\n",
|
|
(unsigned long long)used, (unsigned long long)nblocks,
|
|
(unsigned long long)used * SCHFS_BLKSIZE / 1024,
|
|
(unsigned long long)nblocks * SCHFS_BLKSIZE / 1024,
|
|
(unsigned long long)VOL.generation);
|
|
return 0;
|
|
}
|