SchOS/tools/mkfs.c
Nexoniarz 15331d4399 Fix two bugs the Fedora move exposed
Neither is Fedora-specific. A different host and a freshly built
userland simply drove code that Windows never reached.

tools/mkfs.c did not compile at all. The DT_UNKNOWN/DT_DIR/DT_REG
fallbacks sat above <dirent.h>, so on glibc -- which spells them as
enumerators -- the #ifndef asked before the header was read, always
answered "missing", and then the header's own enum expanded to
`enum { 0 = 0, ... }`. Move the fallbacks below the include. The
include cannot move up instead: it has to follow the #undef that ends
the `#define dirent schfs_dirent` block.

getstackpcs() page-faulted. It dereferenced rbp[1] without validating
rbp, and the guard rejected 0xffffffff -- dead code on x86-64, already
caught by the KERNBASE test -- while the value that actually turns up
is all-ones, which is above KERNBASE. rbp[1] then wrapped to 0x7 and
took trap 14. `cp` panicked the kernel every time. Bound rbp from
above and require 8-byte alignment.

That second one masks other bugs: getcallerpcs() runs on every lock
acquisition and again from the trap handler, so a walker that can
fault replaces a real diagnosis with a bogus one.

Baseline on the Fedora laptop is now green: make ports, make,
make schfs-check including the round-trip that had never run, and
bootcheck at 27/27 plus 108 [ok] across the extended suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 19:29:21 +02:00

1252 lines
40 KiB
C

// SCHFS mkfs -- disk image builder.
//
// Aligned with fs.c improvements:
// - Writes atime/mtime/ctime timestamps into every inode.
// - Computes and writes CRC32c checksum into every inode and the superblock,
// so fsinit() and ilock() pass their validation checks on first boot.
// - OVERFLOW_DIR_START / OVERFLOW_DIR_END replace all magic "113 * BSIZE"
// literals; both loops in iappend_init_dir are unified into one helper.
// - alloc_bmap gains a T_EXTENT path that writes proper struct fsv1_extent
// entries (inline extents only; mkfs files are small).
// - ialloc writes a proper struct dinode including the new fields so the
// on-disk image is byte-compatible with what the kernel reads.
// - updatesb writes a CRC32c checksum of the superblock.
// - check_disk_space prints a helpful message before dying.
// - Dead code (cal_block_index_for_dinode, test_xpr) removed.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <time.h>
#include <stdint.h>
// Windows opens files in *text* mode by default, which turns every 0x0A
// written into 0x0D 0x0A. A filesystem image written that way is corrupt in a
// way that is invisible until the kernel tries to mount it, so every open()
// below is explicitly binary. O_BINARY does not exist on Unix, where there is
// no translation to disable.
#ifndef O_BINARY
#define O_BINARY 0
#endif
// bzero/bcopy are pre-POSIX.1-2008 BSD spellings. glibc still declares them,
// mingw-w64 does not, and this file has seventeen calls. They are defined here
// rather than rewritten at each call site on purpose: bcopy takes (src, dst)
// and memmove takes (dst, src), so a mechanical rewrite has seventeen chances
// to silently reverse a copy -- and the result would be a filesystem image
// that builds without complaint and is quietly wrong.
#define bzero(p, n) memset((p), 0, (n))
#define bcopy(src, dst, n) memmove((dst), (src), (n))
#define stat xv6_stat
#define sigset_t xv6_sigset_t
#define dirent schfs_dirent
#include "../include/types.h"
#include "../include/param.h"
#include "../include/fs.h"
#include "../include/stat.h"
#undef dirent
#undef stat
#include <dirent.h>
// After the #undef above, so this gets the host's struct stat and not xv6's.
#include <sys/stat.h>
// struct dirent's d_type is a BSD/glibc extension; mingw-w64 has no such
// field. The loops below already handle DT_UNKNOWN by probing with opendir(),
// which is the portable answer and was written for filesystems that decline to
// report a type -- so on Windows every entry simply takes that path.
//
// This block must stay *below* <dirent.h>, not up with the other compatibility
// shims. glibc spells these as enumerators with matching macros, so asking
// #ifndef before the header is read always says "missing" -- and then the
// header's own `enum { DT_UNKNOWN = 0, ... }` expands to `enum { 0 = 0, ... }`
// and the build dies on a syntax error a long way from the cause. The header
// cannot move up either: it has to follow the `#undef dirent` above, or it
// would define the xv6 struct dirent instead of the host's.
#ifndef DT_UNKNOWN
#define DT_UNKNOWN 0
#define DT_DIR 4
#define DT_REG 8
#endif
#ifdef _WIN32
#define DIRENT_TYPE(dp) DT_UNKNOWN
#else
#define DIRENT_TYPE(dp) ((dp)->d_type)
#endif
#ifndef static_assert
#define static_assert(a, b) do { switch (0) case 0: case (a): ; } while (0)
#endif
#ifndef RW
#define RW 3
#endif
#ifndef HASHRANGE
#define HASHRANGE 16384
#endif
// Overflow bucket bounds -- must match fs.c definitions.
#ifndef OVERFLOW_DIR_START
#define OVERFLOW_DIR_START (113 * BSIZE)
#define OVERFLOW_DIR_END (115 * BSIZE)
#endif
// ---------------------------------------------------------------------------
// CRC32c (Castagnoli) -- must stay byte-for-byte identical to the fs.c copy.
// ---------------------------------------------------------------------------
static uint32_t crc32c_table[256];
static void
crc32c_init(void)
{
uint32_t i, j, crc;
for (i = 0; i < 256; i++) {
crc = i;
for (j = 0; j < 8; j++)
crc = (crc >> 1) ^ (crc & 1 ? 0x82F63B78U : 0);
crc32c_table[i] = crc;
}
}
static uint32_t
crc32c(uint32_t crc, const void *buf, uint32_t len)
{
const uint8_t *p = (const uint8_t *)buf;
crc = ~crc;
while (len--)
crc = (crc >> 8) ^ crc32c_table[(crc ^ *p++) & 0xFF];
return ~crc;
}
static uint32_t
inode_checksum(struct dinode *dip)
{
uint32_t saved = dip->checksum;
dip->checksum = 0;
uint32_t ck = crc32c(0, dip, sizeof(*dip));
dip->checksum = saved;
return ck;
}
// ---------------------------------------------------------------------------
// File system geometry
// ---------------------------------------------------------------------------
#define NINODES 12000
// Image size in blocks. Defaults to FSSIZE (the main root image) but is
// overridable with --size, so a small ramdisk root can be built by the same
// tool without recompiling param.h. Anything derived from it is recomputed in
// main() after argument parsing.
int fssize = FSSIZE;
// Inode count, likewise overridable (--ninodes). It is not a free choice on a
// small image: the inode table is preallocated, so 12000 inodes costs 1501
// blocks whether or not anything uses them. On the 150000-block root image that
// is 1% and irrelevant; on the 6000-block ramdisk root it was a quarter of the
// entire image, spent to describe about 260 files.
int ninodes = NINODES;
int nibitmap;
int nbitmap;
int ninodeblocks;
int nlog = LOGSIZE;
int nmeta;
int nblocks;
// Paths to leave out of the image, relative to the sysroot root and without a
// leading slash (e.g. "usr/share/misc"). Matches a whole path component prefix,
// so naming a directory drops its entire subtree.
//
// This exists because the ramdisk root is *embedded in the kernel binary*, which
// makes what goes into it a licensing question and not only a size one: anything
// staged in sysroot/ when that image is built becomes part of a distributable
// artifact. An exclude list enforces that mechanically, rather than relying on
// someone having remembered to unstage something first.
#define MAXEXCLUDE 32
const char *excludes[MAXEXCLUDE];
int nexcludes;
// strlen(sysroot_path) + 1, so a host path can be turned back into the
// sysroot-relative one the exclude patterns are written against.
size_t sysroot_prefix;
// Is this sysroot-relative path excluded? 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;
}
// Shared by both tree walkers: decide whether to descend into a host path, and
// say so when the answer is no.
//
// Checked before the entry is dispatched rather than inside sysroot_add_dir(),
// so an excluded directory is never opened and never gets an inode. An empty
// directory in the image would still cost one and would wrongly imply the
// content was supposed to be there.
static int
sysroot_skip(const char *child)
{
const char *rel = child + sysroot_prefix;
if (!is_excluded(rel))
return 0;
fprintf(stderr, "mkfs: excluding %s\n", rel);
return 1;
}
int fsfd;
struct superblock sb;
char zeroes[BSIZE];
uint freeinode = 1;
uint freeblock;
// ---------------------------------------------------------------------------
// Forward declarations
// ---------------------------------------------------------------------------
void balloc(int);
void image_init(void);
void image_flush(void);
void wsect(uint, void *);
void winode(uint, struct dinode *);
void rinode(uint, struct dinode *);
void rsect(uint, void *);
uint ialloc(ushort type);
void iappend(uint inum, void *p, int n);
void iappend_init_dir(uint inum, struct schfs_dirent *de);
void setibmap(int);
void updatesb(void);
void die(const char *);
void check_disk_space(void);
static uint alloc_bmap(struct dinode *dp, uint fbn);
static void sysroot_add_dir(uint parent_ino, const char *host_path, const char *name);
static void sysroot_add_file(uint parent_ino, const char *host_path, const char *name);
static void sysroot_populate(uint rootino, const char *sysroot_path);
// ---------------------------------------------------------------------------
// Byte-order helpers (little-endian)
// ---------------------------------------------------------------------------
ushort
xshort(ushort x)
{
ushort y;
uchar *a = (uchar *)&y;
a[0] = x;
a[1] = x >> 8;
return y;
}
uint
xint(uint x)
{
uint y;
uchar *a = (uchar *)&y;
a[0] = x;
a[1] = x >> 8;
a[2] = x >> 16;
a[3] = x >> 24;
return y;
}
// ---------------------------------------------------------------------------
// Content-based hard-link dedup
//
// sysroot/bin holds one full, independent copy of the toybox binary per applet
// name (57 of them, ~822 KB each) because this tool understood neither symlinks
// nor hard links -- about 44 MB of identical bytes in every image. Rather than
// teach it symlinks, dedup by *content*: identical files share a single inode
// and the extra directory entries become real hard links (nlink > 1), which the
// on-disk format already supports.
//
// Keyed on (size, CRC32c of contents); a candidate is re-read and compared
// byte-for-byte before its inode is shared, so a checksum collision cannot
// silently merge two different files.
// ---------------------------------------------------------------------------
struct dedup_ent {
dev_t hdev; // host device+inode: when these match,
ino_t hino; // it is literally the same file
off_t size;
uint32_t crc;
uint inum;
char path[512];
};
static struct dedup_ent dedup_tab[512];
static int dedup_n;
// A file the host already told us is the same file. sysroot/bin holds 236
// names for one toybox binary, and ports/toybox/Makefile makes them real hard
// links -- so this answers for all but the first of them without opening
// anything.
//
// This matters more than it looks. Without it, every one of those 236 names
// was read once to checksum it and then *both* files were re-read a byte at a
// time through getc() to confirm the match: about half a gigabyte of I/O and
// 380 million getc calls, which took 45 seconds of every build across WSL's
// filesystem boundary. The content path below is still there, and still the
// only thing that can dedup two files that merely happen to be identical.
static uint
dedup_lookup_ino(dev_t hdev, ino_t hino)
{
if (hino == 0)
return 0; // host gave us nothing usable
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
return dedup_tab[i].inum;
return 0;
}
// Returns an existing inum holding identical content, or 0 for "not seen".
static uint
dedup_lookup(const char *host_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)
continue;
FILE *a = fopen(dedup_tab[i].path, "rb");
FILE *b = fopen(host_path, "rb");
if (!a || !b) { if (a) fclose(a); if (b) fclose(b); continue; }
// Block-at-a-time, not getc-at-a-time: same answer, ~1000x fewer
// trips through stdio for the multi-megabyte files this actually sees.
char abuf[16384], bbuf[16384];
int same = 1;
for (;;) {
size_t na = fread(abuf, 1, sizeof(abuf), a);
size_t nb = fread(bbuf, 1, sizeof(bbuf), b);
if (na != nb || memcmp(abuf, bbuf, na) != 0) { same = 0; break; }
if (na == 0) break;
}
fclose(a); fclose(b);
if (same)
return dedup_tab[i].inum;
}
return 0;
}
static void
dedup_remember(const char *host_path, dev_t hdev, ino_t hino,
off_t size, uint32_t crc, uint inum)
{
if (dedup_n >= (int)(sizeof(dedup_tab) / sizeof(dedup_tab[0])))
return; // table full: stop deduping, stay correct
struct dedup_ent *e = &dedup_tab[dedup_n++];
e->hdev = hdev;
e->hino = hino;
e->size = size;
e->crc = crc;
e->inum = inum;
snprintf(e->path, sizeof(e->path), "%s", host_path);
}
// Bump nlink on an inode that is already written out, for a dedup hit.
//
// winode() writes the struct verbatim -- it does NOT recompute the checksum,
// so every caller must refresh it after touching any field. Missing this here
// shipped images whose deduped inodes carried a stale CRC32c, and the kernel
// panicked with "ilock: inode checksum mismatch" the first time one was opened.
static void
inode_addlink(uint inum)
{
struct dinode din;
rinode(inum, &din);
din.nlink = xshort(xshort(din.nlink) + 1);
din.checksum = inode_checksum(&din);
winode(inum, &din);
}
// ---------------------------------------------------------------------------
// Sysroot tree walker — mirrors sysroot/ into the FS image
// ---------------------------------------------------------------------------
static void
sysroot_add_file(uint parent_ino, const char *host_path, const char *name)
{
int fd = open(host_path, O_RDONLY | O_BINARY);
if (fd < 0) { perror(host_path); exit(1); }
// Ask the host whether this is a file we have already taken in. A hard
// link is the same inode by definition, so this is exact -- and it costs
// one fstat instead of reading the file twice.
struct stat st;
dev_t hdev = 0;
ino_t hino = 0;
uint ino = 0;
if (fstat(fd, &st) == 0 && st.st_nlink > 1) {
hdev = st.st_dev;
hino = st.st_ino;
ino = dedup_lookup_ino(hdev, hino);
}
if (ino) {
close(fd);
inode_addlink(ino);
} else {
// Checksum the contents, so files that are identical without being
// linked can still share one inode.
char buf[BSIZE];
int n;
off_t fsz = 0;
uint32_t crc = 0;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
crc = crc32c(crc, buf, (uint32_t)n);
fsz += n;
}
ino = dedup_lookup(host_path, fsz, crc);
if (ino) {
// Identical content is already in the image: emit another
// directory entry for that same inode -- a real hard link -- and
// write no data.
close(fd);
inode_addlink(ino);
} else {
ino = ialloc(T_FILE);
if (lseek(fd, 0, SEEK_SET) != 0) die(host_path);
while ((n = read(fd, buf, sizeof(buf))) > 0)
iappend(ino, buf, n);
close(fd);
dedup_remember(host_path, hdev, hino, fsz, crc, ino);
}
}
struct schfs_dirent de;
bzero(&de, sizeof(de));
de.inum = xshort(ino);
strncpy(de.name, name, DIRSIZ);
iappend_init_dir(parent_ino, &de);
}
static void
sysroot_add_dir(uint parent_ino, const char *host_path, const char *name)
{
DIR *d = opendir(host_path);
if (!d) { perror(host_path); exit(1); }
uint dir_ino = ialloc(T_DIR);
struct schfs_dirent de;
bzero(&de, sizeof(de));
de.inum = xshort(dir_ino);
strncpy(de.name, name, DIRSIZ);
iappend_init_dir(parent_ino, &de);
bzero(&de, sizeof(de));
de.inum = xshort(dir_ino);
strcpy(de.name, ".");
iappend(dir_ino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(parent_ino);
strcpy(de.name, "..");
iappend(dir_ino, &de, sizeof(de));
struct dirent *dp;
while ((dp = readdir(d)) != NULL) {
if (dp->d_name[0] == '.')
continue;
char child[4096];
snprintf(child, sizeof(child), "%s/%s", host_path, dp->d_name);
if (sysroot_skip(child))
continue;
unsigned char dtype = DIRENT_TYPE(dp);
if (dtype == DT_UNKNOWN) {
DIR *probe = opendir(child);
if (probe) { closedir(probe); dtype = DT_DIR; }
else dtype = DT_REG;
}
if (dtype == DT_DIR)
sysroot_add_dir(dir_ino, child, dp->d_name);
else if (dtype == DT_REG)
sysroot_add_file(dir_ino, child, dp->d_name);
}
closedir(d);
}
// Merge the contents of sysroot_path/ directly into the root inode.
static void
sysroot_populate(uint rootino, const char *sysroot_path)
{
DIR *d = opendir(sysroot_path);
if (!d) { perror(sysroot_path); exit(1); }
sysroot_prefix = strlen(sysroot_path) + 1;
struct dirent *dp;
while ((dp = readdir(d)) != NULL) {
if (dp->d_name[0] == '.')
continue;
char child[4096];
snprintf(child, sizeof(child), "%s/%s", sysroot_path, dp->d_name);
if (sysroot_skip(child))
continue;
unsigned char dtype = DIRENT_TYPE(dp);
if (dtype == DT_UNKNOWN) {
DIR *probe = opendir(child);
if (probe) { closedir(probe); dtype = DT_DIR; }
else dtype = DT_REG;
}
if (dtype == DT_DIR)
sysroot_add_dir(rootino, child, dp->d_name);
else if (dtype == DT_REG)
sysroot_add_file(rootino, child, dp->d_name);
}
closedir(d);
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int
main(int argc, char *argv[])
{
uint rootino;
struct schfs_dirent de;
static_assert(sizeof(int) == 4, "Integers must be 4 bytes!");
if (argc < 2) {
fprintf(stderr,
"Usage: mkfs fs.img [options] --sysroot <path>\n"
" mkfs fs.img [options] file...\n"
"\n"
" --size <blocks> image size (default FSSIZE = %d)\n"
" --ninodes <n> inode table size (default %d)\n"
" --exclude <path> omit a sysroot-relative path and its subtree;\n"
" may be given more than once\n",
FSSIZE, NINODES);
exit(1);
}
// Options let one tool build both the main root image and the much smaller
// ramdisk root without recompiling param.h.
//
// Parsed by compacting argv in place, because the two modes below still
// identify themselves positionally: argv[2] is either "--sysroot" or the
// first file of the legacy flat list. The previous version of this loop
// handled exactly one option and then `break`, which silently ignored a
// second one -- fine while --size was the only flag, a trap now.
{
int out = 2;
for (int i = 2; i < argc; i++) {
int has_arg = (i + 1 < argc);
if (has_arg && strcmp(argv[i], "--size") == 0) {
fssize = atoi(argv[++i]);
if (fssize < 64) {
fprintf(stderr, "mkfs: --size %d is too small\n", fssize);
exit(1);
}
} else if (has_arg && strcmp(argv[i], "--ninodes") == 0) {
ninodes = atoi(argv[++i]);
if (ninodes < 16) {
fprintf(stderr, "mkfs: --ninodes %d is too small\n", ninodes);
exit(1);
}
} else if (has_arg && strcmp(argv[i], "--exclude") == 0) {
if (nexcludes >= MAXEXCLUDE) {
fprintf(stderr, "mkfs: more than %d --exclude paths\n", MAXEXCLUDE);
exit(1);
}
// Tolerate a leading slash, since "/usr/share/misc" is the
// natural way to write the path even though the patterns are
// matched relative to the sysroot.
const char *p = argv[++i];
excludes[nexcludes++] = (*p == '/') ? p + 1 : p;
} else {
argv[out++] = argv[i];
}
}
argc = out;
}
// Geometry must be computed now, not at static-initialiser time: it depends
// on --size and --ninodes.
nbitmap = fssize / (BSIZE * 8) + 1;
nibitmap = ninodes / (BSIZE * 8) + 1;
ninodeblocks = ninodes / IPB + 1;
assert((BSIZE % sizeof(struct dinode)) == 0);
assert((BSIZE % sizeof(struct schfs_dirent)) == 0);
crc32c_init();
fsfd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0666);
if (fsfd < 0)
die(argv[1]);
nmeta = 2 + nlog + ninodeblocks + nbitmap + nibitmap;
nblocks = fssize - nmeta;
sb.magic = FSMAGIC;
sb.size = xint(fssize);
sb.nblocks = xint(nblocks);
sb.ninodes = xint(ninodes);
sb.nlog = xint(nlog);
sb.logstart = xint(2);
sb.ibmapstart = xint(2 + nlog);
sb.inodestart = xint(2 + nlog + nibitmap);
sb.bmapstart = xint(2 + nlog + nibitmap + ninodeblocks);
printf("nmeta %d blocks %d total %d\n", nmeta, nblocks, fssize);
freeblock = nmeta;
// calloc gives us the zeroed image; there is nothing to write out yet.
image_init();
{
char buf[BSIZE];
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
wsect(1, buf);
}
// Root directory.
rootino = ialloc(T_DIR);
assert(rootino == ROOTINO);
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, ".");
iappend(rootino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, "..");
iappend(rootino, &de, sizeof(de));
if (argc >= 4 && strcmp(argv[2], "--sysroot") == 0) {
// New mode: mirror a sysroot directory tree into the image.
sysroot_populate(rootino, argv[3]);
} else {
// Legacy mode: flat file list → everything goes into /bin.
uint binino = ialloc(T_DIR);
bzero(&de, sizeof(de));
de.inum = xshort(binino);
strcpy(de.name, "bin");
iappend_init_dir(rootino, &de);
bzero(&de, sizeof(de));
de.inum = xshort(binino);
strcpy(de.name, ".");
iappend(binino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, "..");
iappend(binino, &de, sizeof(de));
char buf[BSIZE];
for (int i = 2; i < argc; i++) {
char *shortname = argv[i];
if (strncmp(shortname, "build/user/", 11) == 0)
shortname += 11;
else if (strncmp(shortname, "user/", 5) == 0)
shortname += 5;
char *last_slash = strrchr(shortname, '/');
if (last_slash)
shortname = last_slash + 1;
int fd = open(argv[i], O_RDONLY | O_BINARY);
if (fd < 0)
die(argv[i]);
if (shortname[0] == '_')
shortname++;
uint inum = ialloc(T_FILE);
bzero(&de, sizeof(de));
de.inum = xshort(inum);
strncpy(de.name, shortname, DIRSIZ);
if (strcmp(shortname, "README") == 0)
iappend_init_dir(rootino, &de);
else
iappend_init_dir(binino, &de);
int cc;
while ((cc = read(fd, buf, sizeof(buf))) > 0)
iappend(inum, buf, cc);
close(fd);
}
}
// Finalize: bitmap, inode bitmap, superblock (with checksum).
balloc(freeblock);
setibmap(freeinode);
updatesb();
// Nothing has touched the file until now.
image_flush();
exit(0);
}
// ---------------------------------------------------------------------------
// Disk I/O
// ---------------------------------------------------------------------------
void
check_disk_space(void)
{
if (freeblock >= (uint)fssize) {
// Naming every lever, because the old message said only "increase
// FSSIZE in param.h" -- and the image that actually overflowed was the
// ramdisk root, whose size comes from RAMDISK_BLOCKS in the Makefile.
// FSSIZE was 150000 and entirely innocent, which cost a while to work
// out. A wrong pointer in an error message is worse than none.
fprintf(stderr,
"mkfs: image full -- used %u of %u blocks"
" (%d metadata: %d log, %d inode table, %d bitmaps).\n",
freeblock, fssize, nmeta, nlog, ninodeblocks, nbitmap + nibitmap);
fprintf(stderr,
" Which knob depends on the image:\n"
" the main root image -> FSSIZE in include/param.h\n"
" the ramdisk root -> RAMDISK_BLOCKS in the Makefile\n"
" Before growing it, check whether the content belongs here at all:\n"
" --exclude <path> drops a subtree, and --ninodes shrinks the\n"
" %d-block preallocated inode table (currently %d inodes for %u used).\n",
ninodeblocks, ninodes, freeinode);
exit(1);
}
}
// The image is assembled in memory and written out once, in image_flush().
//
// It used to be built directly on the file: every wsect() was an lseek plus a
// 512-byte write, and building a 150000-block image meant over 300,000
// syscall pairs -- including 150,000 of them just to zero it. That is
// tolerable on a local disk and ruinous across a filesystem boundary; on
// WSL's /mnt/d it made `make` take a hundred seconds *when nothing had
// changed*. Holding the whole image (77 MB at the default size) costs less
// than that by every measure worth caring about.
static unsigned char *image;
void
image_init(void)
{
image = calloc((size_t)fssize, BSIZE);
if (!image) {
fprintf(stderr, "mkfs: out of memory for a %d-block image\n", fssize);
exit(1);
}
}
void
image_flush(void)
{
size_t total = (size_t)fssize * BSIZE;
size_t tail = total;
size_t done = 0;
// Everything above the last allocated block is zero, and a file that ends
// early reads as zeros past its end -- so write only the part that has
// anything in it and set the length with ftruncate. At the default size
// that is under 2 MB written instead of 77, which is the difference
// between a filesystem image being cheap to rebuild and being the slowest
// step in the build.
//
// The last non-zero block is *found* rather than assumed to be `freeblock`
// -- scanning memory costs nothing next to the write, and it does not
// depend on my reading of where the allocator stops being able to place
// things.
while (tail > 0 && image[tail - 1] == 0)
tail--;
tail = ((tail + BSIZE - 1) / BSIZE) * BSIZE; // round up to a whole block
while (done < tail) {
ssize_t n = write(fsfd, image + done, tail - done);
if (n <= 0)
die("write");
done += (size_t)n;
}
if (ftruncate(fsfd, (off_t)total) < 0)
die("ftruncate");
}
void
wsect(uint sec, void *buf)
{
if (sec >= (uint)fssize) {
fprintf(stderr, "mkfs: wsect %u out of range\n", sec);
exit(1);
}
memmove(image + (size_t)sec * BSIZE, buf, BSIZE);
}
void
rsect(uint sec, void *buf)
{
if (sec >= (uint)fssize) {
fprintf(stderr, "mkfs: rsect %u out of range\n", sec);
exit(1);
}
memmove(buf, image + (size_t)sec * BSIZE, BSIZE);
}
void
winode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn = IBLOCK(inum, sb);
rsect(bn, buf);
struct dinode *dip = ((struct dinode *)buf) + (inum % IPB);
*dip = *ip;
wsect(bn, buf);
}
void
rinode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn = IBLOCK(inum, sb);
rsect(bn, buf);
struct dinode *dip = ((struct dinode *)buf) + (inum % IPB);
*ip = *dip;
}
// ---------------------------------------------------------------------------
// Inode allocation
// ---------------------------------------------------------------------------
// Current wall-clock time as a 32-bit Unix timestamp.
static uint32_t
mkfs_now(void)
{
return (uint32_t)time(NULL);
}
uint
ialloc(ushort type)
{
uint inum = freeinode++;
struct dinode din;
bzero(&din, sizeof(din));
din.type = xshort(type);
din.nlink = xshort(1);
din.size = xint(0);
din.rwmode = RW;
din.supermode = 0;
din.showmode = 1;
// Timestamps: all three set to "now" at image creation time.
uint32_t ts = mkfs_now();
din.atime = ts;
din.mtime = ts;
din.ctime = ts;
// Checksum must be computed AFTER all fields are populated.
din.checksum = inode_checksum(&din);
winode(inum, &din);
return inum;
}
// ---------------------------------------------------------------------------
// Block allocation bitmap
// ---------------------------------------------------------------------------
void
balloc(int used)
{
uchar buf[BSIZE];
int bmap_block = 0;
printf("balloc: first %d blocks have been allocated\n", used);
bzero(buf, BSIZE);
for (int i = 0; i < used; i++) {
buf[(i % (BSIZE * 8)) / 8] |= (1 << (i % 8));
if ((i + 1) % (BSIZE * 8) == 0 || i == used - 1) {
wsect(xint(sb.bmapstart) + bmap_block, buf);
bzero(buf, BSIZE);
bmap_block++;
}
}
}
// ---------------------------------------------------------------------------
// alloc_bmap: map logical file block `fbn` to a physical disk block,
// allocating one if needed. Supports classic (direct/indirect) inodes and
// T_EXTENT inodes (inline extent array only -- sufficient for mkfs).
// ---------------------------------------------------------------------------
#define min(a, b) ((a) < (b) ? (a) : (b))
static uint
alloc_bmap_extent(struct dinode *dp, uint fbn)
{
struct fsv1_extent *ext = (struct fsv1_extent *)dp->addrs;
// Search existing inline extents.
int n = 0;
for (n = 0; n < (int)MAX_INLINE_EXTENTS; n++) {
if (ext[n].len == 0)
break;
if (fbn >= ext[n].start_block &&
fbn < ext[n].start_block + ext[n].len)
return ext[n].start_blkaddr + (fbn - ext[n].start_block);
}
// Allocate a new physical block.
check_disk_space();
uint addr = freeblock++;
// Try to extend the previous extent if the blocks are contiguous.
if (n > 0) {
struct fsv1_extent *last = &ext[n - 1];
if (fbn == last->start_block + last->len &&
addr == last->start_blkaddr + last->len) {
last->len++;
return addr;
}
} else {
// Very first block.
ext[0].start_block = 0;
ext[0].start_blkaddr = addr;
ext[0].len = 1;
return addr;
}
// Need a new extent entry.
if (n >= (int)MAX_INLINE_EXTENTS) {
fprintf(stderr,
"alloc_bmap_extent: too many extents for inline array "
"(MAX_INLINE_EXTENTS=%d). "
"Add indirect extent block support if files are this large.\n",
(int)MAX_INLINE_EXTENTS);
exit(1);
}
ext[n].start_block = fbn;
ext[n].start_blkaddr = addr;
ext[n].len = 1;
return addr;
}
static uint
alloc_bmap(struct dinode *dp, uint fbn)
{
// Dispatch to extent path for T_EXTENT inodes.
if (xshort(dp->type) == T_EXTENT)
return alloc_bmap_extent(dp, fbn);
// Classic direct / singly / doubly / triply indirect.
uint indirect[NINDIRECT];
if (fbn < NDIRECT) {
if (xint(dp->addrs[fbn]) == 0) {
check_disk_space();
dp->addrs[fbn] = xint(freeblock++);
}
return xint(dp->addrs[fbn]);
}
fbn -= NDIRECT;
// Singly indirect.
if (fbn < NINDIRECT) {
if (xint(dp->addrs[NDIRECT]) == 0) {
check_disk_space();
dp->addrs[NDIRECT] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT]), (char *)indirect);
if (indirect[fbn] == 0) {
check_disk_space();
indirect[fbn] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT]), (char *)indirect);
}
return xint(indirect[fbn]);
}
fbn -= NINDIRECT;
// Doubly indirect.
if (fbn < NINDIRECT * NINDIRECT) {
if (xint(dp->addrs[NDIRECT + 1]) == 0) {
check_disk_space();
dp->addrs[NDIRECT + 1] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT + 1]), (char *)indirect);
uint idx1 = fbn / NINDIRECT;
if (indirect[idx1] == 0) {
check_disk_space();
indirect[idx1] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT + 1]), (char *)indirect);
}
uint lvl2 = xint(indirect[idx1]);
uint ind2[NINDIRECT];
rsect(lvl2, (char *)ind2);
uint idx2 = fbn % NINDIRECT;
if (ind2[idx2] == 0) {
check_disk_space();
ind2[idx2] = xint(freeblock++);
wsect(lvl2, (char *)ind2);
}
return xint(ind2[idx2]);
}
fbn -= NINDIRECT * NINDIRECT;
// Triply indirect.
if (fbn < NINDIRECT * NINDIRECT * NINDIRECT) {
if (xint(dp->addrs[NDIRECT + 2]) == 0) {
check_disk_space();
dp->addrs[NDIRECT + 2] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT + 2]), (char *)indirect);
uint idx1 = fbn / (NINDIRECT * NINDIRECT);
if (indirect[idx1] == 0) {
check_disk_space();
indirect[idx1] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT + 2]), (char *)indirect);
}
uint lvl2 = xint(indirect[idx1]);
uint ind2[NINDIRECT];
rsect(lvl2, (char *)ind2);
uint idx2 = (fbn / NINDIRECT) % NINDIRECT;
if (ind2[idx2] == 0) {
check_disk_space();
ind2[idx2] = xint(freeblock++);
wsect(lvl2, (char *)ind2);
}
uint lvl3 = xint(ind2[idx2]);
uint ind3[NINDIRECT];
rsect(lvl3, (char *)ind3);
uint idx3 = fbn % NINDIRECT;
if (ind3[idx3] == 0) {
check_disk_space();
ind3[idx3] = xint(freeblock++);
wsect(lvl3, (char *)ind3);
}
return xint(ind3[idx3]);
}
die("alloc_bmap: fbn out of range");
return 0;
}
// ---------------------------------------------------------------------------
// iappend: append `n` bytes from `xp` to inode `inum`.
// ---------------------------------------------------------------------------
void
iappend(uint inum, void *xp, int n)
{
char *p = (char *)xp;
uint fbn, off, n1;
struct dinode din;
char buf[BSIZE];
uint x;
rinode(inum, &din);
off = xint(din.size);
while (n > 0) {
fbn = off / BSIZE;
assert(fbn < MAXFILE);
x = alloc_bmap(&din, fbn);
n1 = min((uint)n, (fbn + 1) * BSIZE - off);
rsect(x, buf);
bcopy(p, buf + off - (fbn * BSIZE), n1);
wsect(x, buf);
n -= n1;
off += n1;
p += n1;
}
din.size = xint(off);
din.mtime = mkfs_now();
din.ctime = mkfs_now();
// Recompute checksum after modifying size/mtime/ctime/addrs.
din.checksum = inode_checksum(&din);
winode(inum, &din);
}
// ---------------------------------------------------------------------------
// Directory HTree helpers
// ---------------------------------------------------------------------------
static unsigned int
BKDRHash(char *str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while (*str)
hash = hash * seed + (*str++);
return (hash % HASHRANGE) + 16;
}
// Ensure that all blocks from 0 up to the block containing `target_off` exist.
// Returns the physical block address of the block containing `target_off`.
static uint
ensure_blocks_up_to(struct dinode *dp, uint target_off)
{
uint target_block = target_off / BSIZE;
uint baddr = 0;
for (uint b = 0; b <= target_block; b++)
baddr = alloc_bmap(dp, b);
uint required = (target_block + 1) * BSIZE;
if (required > xint(dp->size))
dp->size = xint(required);
return baddr;
}
// Search one bucket range [range_start, range_end) for a free dirent slot.
// Returns 1 and writes the entry if found; 0 if the bucket is full.
static int
try_insert_in_range(struct dinode *dp, struct schfs_dirent *de,
uint range_start, uint range_end)
{
char buf[BSIZE];
for (uint off = range_start; off < range_end; off += sizeof(struct schfs_dirent)) {
uint baddr = ensure_blocks_up_to(dp, off);
rsect(baddr, buf);
struct schfs_dirent *slot = (struct schfs_dirent *)(buf + (off % BSIZE));
if (slot->inum == 0) {
bcopy(de, buf + (off % BSIZE), sizeof(struct schfs_dirent));
wsect(baddr, buf);
assert(off % 16 == 0);
return 1;
}
}
return 0;
}
// Insert `de` into the HTree directory `inum`.
// Primary bucket determined by BKDR hash; overflow region used if full.
void
iappend_init_dir(uint inum, struct schfs_dirent *de)
{
struct dinode dir_inode;
rinode(inum, &dir_inode);
unsigned int hash = BKDRHash(de->name);
uint primary_start = (uint)(hash * DIRENT_BUCKET_BYTES);
uint primary_end = (uint)((hash * 16 + 16) * sizeof(struct schfs_dirent));
// Try primary bucket.
if (try_insert_in_range(&dir_inode, de, primary_start, primary_end)) {
dir_inode.mtime = mkfs_now();
dir_inode.ctime = mkfs_now();
dir_inode.checksum = inode_checksum(&dir_inode);
winode(inum, &dir_inode);
return;
}
// Primary bucket full -- fall back to overflow region.
printf("iappend_init_dir: '%s' overflows primary bucket (hash=%u), "
"using overflow region [%u, %u)\n",
de->name, hash, OVERFLOW_DIR_START, OVERFLOW_DIR_END);
if (try_insert_in_range(&dir_inode, de,
OVERFLOW_DIR_START, OVERFLOW_DIR_END)) {
dir_inode.mtime = mkfs_now();
dir_inode.ctime = mkfs_now();
dir_inode.checksum = inode_checksum(&dir_inode);
winode(inum, &dir_inode);
return;
}
fprintf(stderr,
"mkfs: directory full -- '%s' could not be inserted "
"(both primary bucket and overflow region are full).\n"
"Increase OVERFLOW_DIR_END or HASHRANGE.\n",
de->name);
exit(1);
}
// ---------------------------------------------------------------------------
// Inode bitmap
// ---------------------------------------------------------------------------
void
setibmap(int used)
{
uchar buf[BSIZE];
int bmap_block = 0;
printf("setibmap: first %d inodes have been allocated\n", used);
bzero(buf, BSIZE);
for (int i = 0; i < used; i++) {
buf[(i % (BSIZE * 8)) / 8] |= (1 << (i % 8));
if ((i + 1) % (BSIZE * 8) == 0 || i == used - 1) {
wsect(xint(sb.ibmapstart) + bmap_block, buf);
bzero(buf, BSIZE);
bmap_block++;
}
}
}
// ---------------------------------------------------------------------------
// Superblock finalisation (called last)
// ---------------------------------------------------------------------------
void
updatesb(void)
{
uchar buf[BSIZE];
sb.freeinodes = xint(ninodes - freeinode);
sb.freeblocks = xint(fssize - freeblock);
// Compute superblock checksum (same algorithm as fs.c::updatesb).
sb.checksum = 0;
sb.checksum = crc32c(0, &sb, sizeof(sb));
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
wsect(1, buf);
printf("updatesb: %u free blocks, %u free inodes, checksum=0x%08X\n",
xint(sb.freeblocks), xint(sb.freeinodes), sb.checksum);
}
// ---------------------------------------------------------------------------
// Error helper
// ---------------------------------------------------------------------------
void
die(const char *s)
{
perror(s);
exit(1);
}