SchOS/kernel/core/klog.c
Nexoniarz 7e88cea151 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 <noreply@anthropic.com>
2026-09-22 01:24:34 +02:00

390 lines
12 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// The kernel log that outlives the kernel. See include/klog.h.
//
// Two properties are worth reading the code for, because both are easy to
// write and easy to get subtly wrong:
//
// **It takes no lock.** klog_putc() runs inside consputc(), which runs with
// cons.lock held on the ordinary path and with interrupts off and every other
// CPU spinning on the panic path. A lock here would add a way for the logger
// to deadlock the thing it exists to report on, which is the one failure it
// must not have. The cost is that two CPUs printing at once can interleave
// characters -- which is already true of the console itself, so the log is no
// worse than the screen it mirrors.
//
// **It buffers before it exists.** The region is carved after e820_init(), so
// everything printed before that -- the loader's handover, the memory map
// itself -- has nowhere to go yet. Dropping it would mean the log is blank for
// exactly the part of the boot that is hardest to debug any other way, so
// early output goes into a small static array and is flushed into the ring the
// moment there is one.
#include "types.h"
#include "defs.h"
#include "memlayout.h"
#include "klog.h"
#include "sbuf.h"
#include "dbg.h"
// consputc()'s cursor-movement escape, from kernel/core/console.c.
#define BACKSPACE 0x100
static struct klog_hdr *klog;
static int recovered; // the buffer held a previous boot when we found it
static uint64 prev_bytes; // how much of it that was
// Early output, before there is a ring to put it in. One page is enough for
// the handful of lines between the first cprintf and klog_init(); anything
// past it is dropped rather than overwritten, because the *first* lines of a
// boot that died early are the ones worth having.
static char early[4096];
static int nearly;
static int early_lost;
// Line state, kept here rather than derived, so a timestamp is emitted once
// per line and not once per write.
static int at_line_start = 1;
// Over the fields that do not move while a kernel is running -- deliberately
// not over head and mark.
//
// Those two change on every character written, and a sum that covered them
// would have to be recomputed on every character too: eight multiplies per
// byte of log, for a check whose entire job is to notice that some *other*
// program used this memory between two boots. head and mark are validated by
// range instead, which catches the same thing without the cost.
//
// Not a CRC either, for the same reason of proportion: this does not need to
// detect a clever corruption, and dragging kernel/fs/schfs's crc32c into core
// to do it would be a dependency in the wrong direction.
static uint32
checksum(const struct klog_hdr *h)
{
uint32 s = 0x9e3779b9u;
s = s * 31 + h->magic;
s = s * 31 + h->version;
s = s * 31 + h->size;
return s;
}
static int
looks_valid(const struct klog_hdr *h, uint64 bytes)
{
if (h->magic != KLOG_MAGIC || h->version != KLOG_VERSION)
return 0;
if (h->size != bytes - sizeof(struct klog_hdr))
return 0;
if (h->sum != checksum(h))
return 0;
// head and mark are outside the sum, so they are checked for sense instead:
// a boot cannot have ended before it started, and nothing this kernel does
// writes a quarter of a petabyte to a log.
if (h->mark > h->head)
return 0;
if (h->head > (1ULL << 48))
return 0;
return 1;
}
static void
raw_putc(char c)
{
klog->data[klog->head % klog->size] = c;
klog->head++;
}
static void
raw_puts(const char *s)
{
while (*s)
raw_putc(*s++);
}
// "[ 2.84] ", from the timer tick. Written by hand because the only
// formatter in the kernel is cprintf, and cprintf is what calls this.
static void
stamp(void)
{
uint t = ticks;
uint sec = t / 100;
uint cs = t % 100;
char b[16];
int i = 0, j;
raw_putc('[');
if (sec == 0) {
b[i++] = '0';
} else {
while (sec && i < 12) { b[i++] = '0' + (sec % 10); sec /= 10; }
}
for (j = i; j < 6; j++)
raw_putc(' ');
while (i-- > 0)
raw_putc(b[i]);
raw_putc('.');
raw_putc('0' + (cs / 10));
raw_putc('0' + (cs % 10));
raw_putc(']');
raw_putc(' ');
}
void
klog_putc(int c)
{
char ch = (char)c;
// Carriage returns and the console's backspace are cursor movement, not
// content. A log full of them reads as corruption.
if (ch == '\r' || c == BACKSPACE || ch == 0)
return;
if (!klog) {
if (nearly < (int)sizeof(early))
early[nearly++] = ch;
else
early_lost = 1;
return;
}
if (at_line_start && ch != '\n')
stamp();
raw_putc(ch);
at_line_start = (ch == '\n');
}
// The log's own region.
//
// It is not on any allocator's list and never will be -- kinit1 is started
// above it -- so the only way to discover that something has been handed a
// page of it is to ask here. A page of the log turning up on kalloc's free
// list is the failure mode this whole region is arranged to avoid, and this
// is what would say so.
static void
klog_whose(struct sbuf *out, struct whose *w)
{
addr_t base, kva;
if (!klog)
return;
base = (addr_t)klog;
kva = (addr_t)P2V(w->phys);
if (kva < base || kva >= base + KLOG_BYTES)
return;
w->owners++;
sb_puts(out, " klog the kernel log, ");
sb_putu(out, (kva - base) / 1024);
sb_puts(out, " KB into a ");
sb_putu(out, KLOG_BYTES / 1024);
sb_puts(out, " KB region\n");
}
void
klog_init(void *base, uint64 bytes)
{
struct klog_hdr *h = (struct klog_hdr *)base;
if (bytes <= sizeof(struct klog_hdr))
return;
if (looks_valid(h, bytes)) {
recovered = 1;
prev_bytes = h->head - h->mark; // what the *last* boot wrote
} else {
// Either the first boot on this machine or a region something else has
// been using. Either way the contents cannot be trusted, and printing
// somebody else's bytes as if they were a log would be worse than
// printing nothing.
h->magic = KLOG_MAGIC;
h->version = KLOG_VERSION;
h->size = (uint32)(bytes - sizeof(struct klog_hdr));
h->boot = 0;
h->head = 0;
recovered = 0;
prev_bytes = 0;
}
h->mark = h->head;
h->boot++;
h->sum = checksum(h);
klog = h;
// A divider in the log itself rather than a flag a reader has to know about:
// whoever ends up reading this will be reading text, possibly by eye, and a
// line that says where one boot ends is worth more than a field that has to
// be interpreted.
at_line_start = 1;
raw_puts("\n--- boot ");
{
char b[12];
int i = 0;
uint32 n = h->boot;
if (n == 0) b[i++] = '0';
while (n) { b[i++] = '0' + (n % 10); n /= 10; }
while (i-- > 0) raw_putc(b[i]);
}
raw_puts(" ---\n");
for (int i = 0; i < nearly; i++) {
char ch = early[i];
if (ch == '\r' || ch == 0)
continue;
if (at_line_start && ch != '\n')
stamp();
raw_putc(ch);
at_line_start = (ch == '\n');
}
if (early_lost)
raw_puts("klog: early output overflowed and some was dropped\n");
nearly = 0;
h->sum = checksum(h);
dbg_register_whose("klog", klog_whose);
// Said out loud on every boot, because "the log survived" and "the log was
// silently started over" look identical in a file that begins with a boot
// divider either way.
if (recovered)
kinfo("klog: %d KB at %p, %d bytes carried over from the previous boot\n",
(int)(bytes / 1024), (uint64)base, (int)prev_bytes);
else
kinfo("klog: %d KB at %p, nothing recoverable was there\n",
(int)(bytes / 1024), (uint64)base);
}
// How many bytes the ring currently holds, oldest first.
uint64
klog_len(void)
{
if (!klog)
return 0;
return klog->head < klog->size ? klog->head : klog->size;
}
uint64 klog_prev_bytes(void) { return prev_bytes; }
int klog_recovered(void) { return recovered; }
// Read at a byte offset into the log, where 0 is the oldest byte still held.
//
// Serving straight out of the ring rather than rendering into a scratch buffer
// the way every other /proc file does, because this file is 128 KB and that
// pattern regenerates the whole thing on every read into a stack array. See
// procread() in kernel/dev/procfs.c.
int
klog_read(uint64 off, char *dst, int n)
{
uint64 have, first;
int i;
if (!klog || n <= 0)
return 0;
have = klog_len();
if (off >= have)
return 0; // EOF
if ((uint64)n > have - off)
n = (int)(have - off);
first = klog->head - have; // logical position of the oldest byte
for (i = 0; i < n; i++)
dst[i] = klog->data[(first + off + i) % klog->size];
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;
}