From 7e88cea151dc908eb4a7cb6a895372c1732380af Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 22 Sep 2026 01:24:34 +0200 Subject: [PATCH] root: find the filesystem instead of being told at build time, and fix dmesg `#define ROOTDEV 1` -- IDE disk 1 -- so a kernel could only ever root on the disk it was *built* expecting. Booting the same image from a USB stick was impossible by construction, and that was the one thing standing between this kernel and its own acceptance test, which begins "boot on real hardware from a USB stick". rootdev_mount() probes. schfs_kmount() was already written for exactly this -- its own comment says "the uberblock scan doubles as the probe: no valid uberblock means this is simply not a SchFS volume, which is not an error worth reporting loudly" -- and a failed probe leaves the slot unmounted and reusable, which is what makes a loop over candidates legal at all. So the change is one function and one call site; the only real use of ROOTDEV was `schfs_kmount()`. Order is preference, not confidence: a RAMDISK_ROOT build wins outright, then USB, then IDE 1, then IDE 0. USB before the fixed disks because a stick that is plugged in was plugged in on purpose. iderw() panics on an absent drive rather than failing, so each candidate is checked with ide_disk_present() / usb_storage_present() first -- which is what those predicates are for. Verified with **no IDE disk attached at all**: boots, mounts the stick, writes a file, and the file is still there on the next boot. [ 0.000021] root: USB disk (device 3) And when nothing matches, it names what it looked at, because "no root filesystem" with no list is the least useful thing a kernel can say at the one moment somebody needs to know which disk it expected: root: no SchFS volume on any candidate device. Looked at: device 3, USB disk: not present device 1, IDE disk 1: not present device 0, IDE disk 0: present, but no SchFS on it **That message is also how dmesg turned out to be broken**, because it did not appear in `dmesg` and should have. `dmesg` printed *nothing at all*, on a system whose boot banner says "or dmesg once you are in" and whose entire quiet-boot design is "route the detail into the kernel log and say how to read it". The log was there the whole time -- `cat /proc/kmsg` has always worked -- but the one command anybody would reach for could not see it. Two separate faults, each alone enough: - **sys_syslog() was `return 0`.** dmesg does not read /proc/kmsg; it calls klogctl(10) for the buffer size and klogctl(3) to read. 0 to the first one means "the log is zero bytes", and a stub that answers 0 is indistinguishable from a machine that logged nothing. Implemented for the actions that mean something here; the rest return -EINVAL rather than 0, because a console log level this kernel does not have is not a request that succeeded. - **With that fixed, dmesg was still empty.** It parses every line with `sscanf(msg, "<%u>[%llu.%llu] %n", ...)` and silently drops anything that does not match. The ring's `[ 0.00] text` already matches everything after the priority -- scanf skips the leading spaces -- so the only missing thing was the `<6>`. klog_render_syslog() adds it on the way out rather than storing it: the ring is also what a person reads with `cat /proc/kmsg` and what PSOGM points at after a crash, and an ABI detail belongs at the boundary that has the ABI. Also: the first version of the no-root diagnostic printed %-11s device 1596400 because cprintf() has no field-width specifier -- it prints "%-11s" literally and then reads the next argument for the %d. Plain %s now, with the note. Co-Authored-By: Claude Opus 5 --- docs/architecture/boot.md | 65 +++++++++++++++++++++++++-- docs/plan/1.0.md | 12 +++-- include/defs.h | 1 + kernel/core/klog.c | 92 +++++++++++++++++++++++++++++++++++++++ kernel/proc/proc.c | 90 +++++++++++++++++++++++++++++++++----- kernel/sys/sys_compat.c | 68 ++++++++++++++++++++++++++++- 6 files changed, 307 insertions(+), 21 deletions(-) diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index c4185e6..63f9854 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -168,6 +168,44 @@ It does **not** mount the root filesystem, and cannot: mounting reads blocks, `forkret()`, the first time any process is scheduled — guarded by a `static int first`. +### Which device the root is on is decided there, not at build time + +`ROOTDEV` used to be `#define ROOTDEV 1` — IDE disk 1 — so a kernel could only +ever root on the disk it was *built* expecting. Booting the same image from a +USB stick was impossible by construction, and that was the one thing standing +between this kernel and its own acceptance test, which begins "boot on real +hardware from a USB stick". + +`rootdev_mount()` probes instead. `schfs_kmount()` was already written to be +used this way — *"the uberblock scan doubles as the probe: no valid uberblock +means this is simply not a SchFS volume, which is not an error worth reporting +loudly"* — and a failed probe leaves the slot unmounted and reusable, which is +what makes a loop over candidates legal at all. + +| order | device | | +|---|---|---| +| 1 | `RAMDISK_DEV` | only in a `RAMDISK_ROOT` build, where it is an explicit choice | +| 2 | `USBDISK_DEV` | a stick that is plugged in was plugged in on purpose | +| 3 | IDE 1 | what the old constant named | +| 4 | IDE 0 | usually the ESP, so usually not SchFS | + +`iderw()` *panics* on an absent drive rather than failing, so each candidate is +checked with `ide_disk_present()` / `usb_storage_present()` first. That is the +whole reason those predicates exist. + +When nothing matches, the panic names what it looked at, because "no root +filesystem" with no list is the least useful thing a kernel can say at the one +moment somebody needs to know which disk it expected: + +``` +root: no SchFS volume on any candidate device. Looked at: + device 3, USB disk: not present + device 1, IDE disk 1: not present + device 0, IDE disk 0: present, but no SchFS on it +``` + +The chosen device is reported in `dmesg` as `root: USB disk (device 3)`. + That is also where PID 1's working directory is re-pointed at the real root, and the reason is worth keeping: @@ -233,12 +271,31 @@ that routing, and the whole boot is still there afterwards: ``` # dmesg | head -4 ---- boot 1 --- -[ 0.00] e820: 1017 MB usable in 8 regions, top 1006 MB (22 entries) -[ 0.00] acpi: 2 cpus, lapic at ffff8000fee00000, ioapic id 0 -[ 0.00] pci: 7 devices +[ 0.000000] e820: 1017 MB usable in 8 regions, top 1002 MB (21 entries) +[ 0.000000] klog: 128 KB at ffff800000737000, nothing recoverable was there +[ 0.000000] acpi: 4 cpus, lapic at ffff8000fee00000, ioapic id 0 +[ 0.000000] console: JetBrains Mono, 10x21 cells, 128 by 38 ``` +**`dmesg` printed nothing at all until 2026-09-22**, which made that promise in +the banner a lie for as long as it had been there. Two separate things were +wrong, and each alone was enough: + +- `sys_syslog()` was `return 0`. `dmesg` does not read `/proc/kmsg`; it calls + `klogctl(10)` for the buffer size and `klogctl(3)` to read, and 0 to the + first one means "the log is zero bytes". A stub that answers 0 is + indistinguishable from a machine that logged nothing. +- With that implemented, `dmesg` was *still* empty. It parses each line with + `sscanf(msg, "<%u>[%llu.%llu] %n", ...)` and **silently drops** anything that + does not match. The ring's `[ 0.00] text` already matches everything + after the priority — scanf skips the leading spaces — so the only missing + thing was the `<6>`. + +`klog_render_syslog()` adds it on the way out rather than storing it, because +the ring is also what a person reads with `cat /proc/kmsg` and what PSOGM +points at after a crash. An ABI detail belongs at the boundary that has the +ABI. + The rule for `kinfo` against `cprintf` is not how interesting a line is — everything was interesting to somebody once. It is: **would you want to see this on a machine that is working?** `pci: 7 devices` is yes the first time and diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index 1b03076..203b513 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -332,10 +332,14 @@ known reasons stops being read, and then it is red for unknown reasons too. **The identity item:** -- [ ] **Boot from a USB disk.** The xHCI stack, bulk-only transport, SCSI and - the block device all work; a superblock reads back through `bread()`. What - is missing is that the root device is compiled in rather than selectable. - Without this the acceptance test above cannot run at all. +- [x] ~~**Boot from a USB disk.**~~ The root device is probed at boot instead of + compiled in: ramdisk (only in a `RAMDISK_ROOT` build), then USB, then the + IDE disks, first SchFS volume wins. Verified with **no IDE disk attached + at all** — boots, mounts the stick, writes a file, and the file is still + there on the next boot. `dmesg` says `root: USB disk (device 3)`, and a + machine with no root names every candidate it looked at instead of + panicking with "no root filesystem". See + [../architecture/boot.md](../architecture/boot.md). #### The `ls -lR` hang, and why it is closed diff --git a/include/defs.h b/include/defs.h index 44d4f24..889d683 100644 --- a/include/defs.h +++ b/include/defs.h @@ -396,6 +396,7 @@ int klog_read(uint64 off, char *dst, int n); uint64 klog_len(void); uint64 klog_prev_bytes(void); int klog_recovered(void); +int klog_render_syslog(char *dst, int cap); // ksym.c -- naming a kernel address. See docs/architecture/debugging.md, D0. int ksym_ready(void); diff --git a/kernel/core/klog.c b/kernel/core/klog.c index 059aa7c..f7e5ec6 100644 --- a/kernel/core/klog.c +++ b/kernel/core/klog.c @@ -296,3 +296,95 @@ klog_read(uint64 off, char *dst, int n) return n; } + +// Render the ring as syslog(2) records, for dmesg. +// +// The ring holds plain readable text -- `[ 0.00] e820: ...` -- because it +// is also what a person reads with `cat /proc/kmsg`, and what PSOGM points at +// after a crash. syslog(2)'s callers expect Linux's record format, which is +// the same line with a priority in front: +// +// <6>[ 0.00] e820: 1017 MB usable in 8 regions +// +// toybox's dmesg parses each line with +// +// sscanf(msg, "<%u>[%llu.%llu] %n", ...) != 3 -> return +// +// and *silently drops* anything that does not match. So a working syslog(2) +// handing back the ring verbatim produced an empty dmesg with no error: every +// line was parsed, rejected and discarded. Only the `<6>` was missing -- the +// bracketed timestamp already matches, because scanf skips the leading spaces. +// +// Rendering here rather than storing the prefix in the ring: the write path +// runs in panic context and from every cprintf, and the stored form is read by +// humans. An ABI detail belongs at the boundary that has the ABI, which is the +// same rule sigset_from_user() follows one directory over. +// +// <6> is KERN_INFO. There are no per-message levels to derive one from, and +// inventing severity from the text would be a worse lie than a constant. +int +klog_render_syslog(char *dst, int cap) +{ + uint64 have = klog_len(), off = 0; + int out = 0, atline = 1; + char chunk[128]; + + if (!klog || cap <= 0 || have == 0) + return 0; + + // Linux returns the *most recent* `cap` bytes. The prefix makes the output + // longer than the source, so find the first whole line that lets the rest + // fit: count what a render from `off` would cost, and drop lines from the + // front until it does. Whole lines, because half a record is worse to a + // parser than a missing one. + for (;;) { + uint64 scan = off; + int need = 0, nl = 1; // nl: next byte starts a line + while (scan < have) { + int got = klog_read(scan, chunk, sizeof(chunk)); + if (got <= 0) + break; + for (int i = 0; i < got; i++) { + if (nl) { need += 3; nl = 0; } + need++; + if (chunk[i] == '\n') nl = 1; + } + scan += got; + } + if (need <= cap || off >= have) + break; + // Advance past one line. + uint64 p = off; + int done = 0; + while (p < have && !done) { + int got = klog_read(p, chunk, sizeof(chunk)); + if (got <= 0) { off = have; break; } + for (int i = 0; i < got; i++) { + p++; + if (chunk[i] == '\n') { off = p; done = 1; break; } + } + if (!done) off = p; + } + if (!done && off >= have) + break; + } + + while (off < have && out < cap) { + int got = klog_read(off, chunk, sizeof(chunk)); + if (got <= 0) + break; + for (int i = 0; i < got && out < cap; i++) { + if (atline) { + if (out + 3 > cap) + return out; // never emit half a prefix + dst[out++] = '<'; dst[out++] = '6'; dst[out++] = '>'; + atline = 0; + } + dst[out++] = chunk[i]; + if (chunk[i] == '\n') + atline = 1; + } + off += got; + } + return out; +} diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index e90997d..fdfd974 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -836,6 +836,81 @@ yield(void) sched(); release(&ptable.lock); } +// Which device the root filesystem is on. +// +// It was `#define ROOTDEV 1` -- IDE disk 1 -- so a kernel could only ever root +// on the disk it was *built* expecting. Booting the same image from a USB +// stick was impossible by construction, and that is the one thing standing +// between this kernel and its own acceptance test, which begins "boot on real +// hardware from a USB stick". +// +// A probe rather than a boot argument, because the answer is discoverable and +// asking someone to name a device number is a worse interface than looking. +// schfs_kmount() is already written to be used this way -- its own comment +// says "the uberblock scan doubles as the probe: no valid uberblock means this +// is simply not a SchFS volume, which is not an error worth reporting loudly" +// -- so each candidate costs one scan and answers yes or no. A failed probe +// leaves the slot unmounted and reusable, which is what makes a loop over +// candidates legal at all. +// +// **The order is preference, not confidence.** A ramdisk root is an explicit +// build choice and wins outright. After it, USB comes before the fixed disks: +// a stick that is plugged in was plugged in on purpose, and a machine with +// both should boot the thing the person is holding. +uint rootdev; + +static int +rootdev_usable(uint dev) +{ + if (dev == RAMDISK_DEV) + return ramdisk_present(); + if (dev == USBDISK_DEV) + return usb_storage_present(); + // iderw() panics on an absent drive rather than failing, so a probe has to + // ask first. That is the whole reason ide_disk_present() exists. + return ide_disk_present((int)dev); +} + +static void +rootdev_mount(void) +{ + static const struct { uint dev; const char *what; } cand[] = { +#ifdef RAMDISK_ROOT + { RAMDISK_DEV, "ramdisk" }, +#endif + { USBDISK_DEV, "USB disk" }, + { 1, "IDE disk 1" }, + { 0, "IDE disk 0" }, + }; + + for (int i = 0; i < (int)NELEM(cand); i++) { + if (!rootdev_usable(cand[i].dev)) + continue; + schfs_kmount(cand[i].dev); + if (schfs_kmounted()) { + rootdev = cand[i].dev; + kinfo("root: %s (device %d)\n", cand[i].what, cand[i].dev); + return; + } + kinfo("root: %s (device %d) carries no SchFS volume\n", + cand[i].what, cand[i].dev); + } + + // Name what was looked at. "no root filesystem" with no list is the least + // useful thing a kernel can say at the one moment somebody needs to know + // which disk it expected. + // Plain %s and no width. cprintf() has no field-width specifier -- it + // prints "%-11s" literally and then reads the *next* argument for the %d, + // which is how the first version of this message came out as + // `%-11s device 1596400`. See print_int() in kernel/core/console.c. + cprintf("root: no SchFS volume on any candidate device. Looked at:\n"); + for (int i = 0; i < (int)NELEM(cand); i++) + cprintf(" device %d, %s: %s\n", cand[i].dev, cand[i].what, + rootdev_usable(cand[i].dev) ? "present, but no SchFS on it" + : "not present"); + panic("no root filesystem"); +} + void forkret(void) { @@ -860,17 +935,10 @@ forkret(void) { iinit(); mountinit(); - // SchFS is the root filesystem. - // - // Mounted here rather than in main() because mounting reads blocks, - // bread() sleeps, and sleeping needs a process context. - // - // schfs_kmount() reports failure by leaving the volume unmounted rather - // than by returning -- it is a probe elsewhere, where "not SchFS" is an - // ordinary answer. Here it is fatal, so ask afterwards. - schfs_kmount(ROOTDEV); - if (!schfs_kmounted()) - panic("schfs root: no SchFS volume on the root device"); + // SchFS is the root filesystem, on whichever device turns out to have + // one. Here rather than in main() because mounting reads blocks, bread() + // sleeps, and sleeping needs a process context. + rootdev_mount(); // Re-point pid 1's cwd at the real root, now that there is one. // // sinit() sets cwd = namei("/") back in main(), which is before this runs. diff --git a/kernel/sys/sys_compat.c b/kernel/sys/sys_compat.c index 262f88b..7342c66 100644 --- a/kernel/sys/sys_compat.c +++ b/kernel/sys/sys_compat.c @@ -14,6 +14,7 @@ #include "posix.h" #include "uaccess.h" #include "errno.h" +#include "klog.h" /* KLOG_BYTES, for syslog(2) action 10 */ extern int linux_syscall_map[]; extern int linux_syscall_map_size; @@ -126,8 +127,71 @@ sys_uname(void) // socket/bind/listen/accept/connect live in sys_net.c now that they do // something. -// syslog: kernel logging facility — no-op for user processes. -addr_t sys_syslog(void) { return 0; } +// syslog(2) -- what `dmesg` actually calls. +// +// This returned 0 for every action, and 0 to action 10 means "the log buffer +// is zero bytes". So `dmesg` printed nothing, on a system whose boot banner +// says *"or dmesg once you are in"* and whose entire quiet-boot design is +// "put the detail in the kernel log and tell them how to read it". The detail +// was there the whole time -- `cat /proc/kmsg` has always worked -- but the +// one command anybody would reach for could not see it. +// +// dmesg does not read /proc/kmsg. It calls klogctl(10) for the buffer size and +// klogctl(3) to read, which is this syscall, and a stub that answers 0 is +// indistinguishable from a machine that logged nothing. +// +// Only the actions that mean something here are implemented. The rest say so +// rather than returning 0: a console log level this kernel does not have is +// not a request that succeeded. +#define SYSLOG_CLOSE 0 +#define SYSLOG_OPEN 1 +#define SYSLOG_READ 2 +#define SYSLOG_READ_ALL 3 +#define SYSLOG_CLEAR 5 +#define SYSLOG_SIZE_UNREAD 9 +#define SYSLOG_SIZE_BUFFER 10 + +addr_t sys_syslog(void) +{ + int type, len; + addr_t bufaddr; + + argint(0, &type); + argaddr(1, &bufaddr); + argint(2, &len); + + switch (type) { + case SYSLOG_CLOSE: + case SYSLOG_OPEN: + return 0; // genuinely nothing to do, as on Linux + + case SYSLOG_SIZE_BUFFER: + return KLOG_BYTES; + + case SYSLOG_SIZE_UNREAD: + return (addr_t)klog_len(); + + case SYSLOG_READ_ALL: { + char *dst; + + if (len < 0) + return -EINVAL; + if (len == 0 || klog_len() == 0) + return 0; + if (argptr(1, &dst, len) < 0) + return -EFAULT; + return (addr_t)klog_render_syslog(dst, len); + } + + // Reading destructively needs a per-reader cursor the ring does not carry, + // and clearing needs the ring to forget -- which is the one thing it exists + // not to do, since its whole purpose is surviving the kernel that wrote it. + case SYSLOG_READ: + case SYSLOG_CLEAR: + default: + return -EINVAL; + } +} // getrandom: fill buffer with pseudo-random bytes. addr_t sys_getrandom(void)