SchOS/cmd/schinit/main.c
Nexoniarz eab13241a0 hwreport: describe a machine onto a USB stick, from a Linux-style /sys
hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.

It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.

The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.

Two bugs found on the way:

- The loader handed over at most 40 memory-map entries, in firmware
  order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
  listed last, so that is what was dropped. The entries now have their
  own area at 0x31000 with room for 256, are sorted before merging, and
  the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
  the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.

usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).

Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 12:54:11 +02:00

724 lines
25 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// schinit -- PID 1.
//
// This program used to be the whole init system: it parsed manifests, walked
// /etc/schinit, decided respawn policy, and supervised every job. All of that
// now lives in schinitd (PID 2), and what is left here is deliberately, almost
// aggressively small.
//
// The reason is docs/architecture/init.md, and it is worth restating because the
// smallness looks like an accident otherwise:
//
// **PID 1 is the one program on the machine with no supervisor.** Nothing
// restarts it, nothing notices it hang, and until proc_reincarnate() lands
// its death is a panic. A program in that position should not contain a
// parser, should not walk a filesystem, and should not allocate. So it does
// none of those things. Its entire job is to be too boring to fail.
//
// What it does:
//
// - bootstrap the filesystems, because something must and it happens first
// - fork schinitd and restart it if it dies
// - fork jobs *on schinitd's behalf*, so every job is a child of PID 1
// - reap children and say so
//
// That third one is the load-bearing one. schinitd decides and asks; PID 1
// acts. So schinitd crashing orphans nothing, reparents nothing and restarts
// nothing -- it comes back, reads /proc/schinit, and finds the world exactly
// as it left it. A supervisor that forked its own jobs could not have that.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "socket.h"
#include "schinit.h"
#define SUPERVISOR "/bin/schinitd"
// Jobs PID 1 is holding for schinitd. This is a table of pids and labels, not
// a table of policy: PID 1 does not know what a job *is*, only that it forked
// one and what to call it in /proc/schinit.
#define MAXJOBS 32
struct child {
int pid; // 0 = free slot
unsigned int cookie; // schinitd's handle for it
char label[SCHINIT_MAXLABEL];
int started; // uptime() ticks
};
static struct child children[MAXJOBS];
static int sock = -1; // to schinitd
static int supervisor_pid;
static int last_pong; // uptime() when schinitd last answered
static int last_ping; // uptime() when we last asked
static unsigned int generation = 1; // bumped by proc_reincarnate() later
static int
slen(const char *s)
{
int n = 0;
while (s[n]) n++;
return n;
}
static void
scopy(char *dst, const char *src, int cap)
{
int i = 0;
while (src[i] && i < cap - 1) { dst[i] = src[i]; i++; }
dst[i] = '\0';
}
static int
seq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
// Case-insensitive, so a manifest may say KeepAlive, keepalive or KEEPALIVE.
// Keys are for humans and this is not a language.
static int
sieq(const char *a, const char *b)
{
for (;;) {
char ca = *a, cb = *b;
if (ca >= 'A' && ca <= 'Z') ca += 32;
if (cb >= 'A' && cb <= 'Z') cb += 32;
if (ca != cb) return 0;
if (!ca) return 1;
a++; b++;
}
}
static int
ends_with(const char *s, const char *suffix)
{
int ls = slen(s), lf = slen(suffix);
if (lf > ls) return 0;
return seq(s + ls - lf, suffix);
}
// How the machine was booted; see /proc/bootmode and kinfo() in the kernel.
static int verbose_boot;
static void
read_bootmode(void)
{
char b[16];
int fd = open("/proc/bootmode", O_RDONLY);
int n;
if (fd < 0)
return;
n = read(fd, b, sizeof(b) - 1);
close(fd);
if (n > 0) { b[n] = 0; verbose_boot = (b[0] == 'v'); }
}
static void
out(const char *s)
{
write(1, (void *)s, slen(s));
}
static void
outn(int v)
{
char buf[12];
int i = 11;
int neg = v < 0;
if (neg) v = -v;
buf[i--] = '\0';
if (v == 0) buf[i--] = '0';
while (v > 0) { buf[i--] = (char)('0' + v % 10); v /= 10; }
if (neg) buf[i--] = '-';
out(&buf[i + 1]);
}
// ---------------------------------------------------------------------------
// Jobs
// ---------------------------------------------------------------------------
// Two formatters, because publish() needs them and pulling in a printf would
// put a format-string parser inside PID 1.
static int
fmt_str(char *dst, const char *s)
{
int n = 0;
while (s[n]) { dst[n] = s[n]; n++; }
return n;
}
static int
fmt_num(char *dst, int v)
{
char tmp[12];
int i = 0, n = 0;
if (v < 0) { dst[n++] = '-'; v = -v; }
if (v == 0) { dst[n++] = '0'; return n; }
while (v > 0) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i-- > 0) dst[n++] = tmp[i];
return n;
}
static void
bootstrap(void)
{
// /dev before the console, because the console now lives in it.
//
// The old order was the other way round -- open a console first so every
// message below had somewhere to go -- and the fallback below is what keeps
// that guarantee. If the mount fails there is no /dev/console and no way to
// say so, so the original path stays: a node called `console` in the root
// directory, which is where this used to put one.
//
// /dev is a filesystem now, not a pile of nodes. Every major and minor it
// contains is the kernel's -- devsw[] in kernel/dev/cdev.c is what they
// dispatch on -- and this program used to hold a second copy of that
// mapping with nothing checking the two agreed.
//
// It was also slow in a way that only showed on a fresh image: each mknod
// was one SchFS transaction, and the twenty of them cost 2600 ms of the
// first boot before this program printed its first line. On a second boot
// the nodes already existed and the same code took 0 ms, so the cost was
// invisible to anyone not building images -- which is to say, invisible to
// everyone except whoever works on this.
mkdir("/dev");
mount("dev", "/dev", "devtmpfs", 0, 0);
if (open("/dev/console", O_RDWR) < 0) {
// No devtmpfs. Fall back to a real node on the root filesystem, which
// is what every boot did before this.
if (open("console", O_RDWR) < 0) {
mknod("console", 1, 1);
open("console", O_RDWR);
}
}
dup(0);
dup(0);
// /proc is a real filesystem (kernel/fs/procfs.c), not a handful of nodes:
// it has to be, because one directory per process cannot be created ahead
// of time. That is what makes ps and top work.
//
// The thirteen mknod() calls that used to follow this mount are gone. They
// ran *after* it, so every one of them was resolving a path into a
// read-only synthetic filesystem and failing -- dead weight kept as a
// fallback for a mount that has not failed in a long time, exactly as the
// note in procfs.c said it was.
mkdir("/proc");
mount("proc", "/proc", "proc", 0, 0);
// Mount points, and nothing mounts on them yet. They are created by the
// image builder (see sysroot-install), so these are a fallback for an image
// that does not have them -- a mkdir that fails because the directory is
// already there costs a path walk and no writes, where creating one costs a
// whole SchFS transaction.
mkdir("/sys");
mkdir("/run");
// The hardware, at Linux's paths, which is where lspci, lsusb and
// hwreport look for it. See kernel/fs/sysfs.c.
mount("sysfs", "/sys", "sysfs", 0, 0);
}
// ---------------------------------------------------------------------------
// The job table, for /proc/schinit
// ---------------------------------------------------------------------------
// Publish what PID 1 is holding.
//
// docs/architecture/init.md, "State outside the process": it lives outside
// whichever process is
// supervising, so a restarted supervisor can read the world back instead of
// rebuilding it. It is written by PID 1 because PID 1 is the process that
// cannot lose it.
//
// One line per running job: cookie, pid, start tick, label. Written whenever
// the table changes rather than on read, because a procfs generator runs in
// the kernel and the kernel has no business walking this program's memory.
static void
publish(void)
{
char buf[MAXJOBS * (SCHINIT_MAXLABEL + 40) + 64];
int n = 0;
// Hand-rolled rather than snprintf, for the same reason as everything else
// here: no allocation, no format parser, no libc surprise in PID 1.
n += fmt_str(buf + n, "generation ");
n += fmt_num(buf + n, (int)generation);
n += fmt_str(buf + n, "\nsupervisor ");
n += fmt_num(buf + n, supervisor_pid);
n += fmt_str(buf + n, "\n");
for (int i = 0; i < MAXJOBS; i++) {
if (!children[i].pid)
continue;
n += fmt_num(buf + n, (int)children[i].cookie);
n += fmt_str(buf + n, " ");
n += fmt_num(buf + n, children[i].pid);
n += fmt_str(buf + n, " ");
n += fmt_num(buf + n, children[i].started);
n += fmt_str(buf + n, " ");
n += fmt_str(buf + n, children[i].label);
n += fmt_str(buf + n, "\n");
}
int fd = open("/proc/schinit", O_WRONLY);
if (fd < 0)
return; // no procfs yet, or it is read-only
write(fd, buf, n);
close(fd);
}
// Read the table back after a reincarnation.
//
// This is the one parse in PID 1, and it is worth being uneasy about. It is
// bounded: a fixed buffer, four integers and a label per line, no allocation,
// and it reads a format this same program wrote. It happens once, at startup,
// only when the kernel has just re-exec'd us. The alternative is forgetting
// every running job, which is worse than a twenty-line scan of known text.
static void
reclaim(void)
{
char buf[MAXJOBS * (SCHINIT_MAXLABEL + 40) + 64];
int fd = open("/proc/schinit", O_RDONLY);
int n, slot = 0;
if (fd < 0)
return;
n = read(fd, buf, (int)sizeof(buf) - 1);
close(fd);
if (n <= 0)
return;
buf[n] = 0;
char *p = buf;
while (*p && slot < MAXJOBS) {
char *line = p;
while (*p && *p != '\n') p++;
if (*p == '\n') *p++ = 0;
unsigned int cookie = 0;
int pid = 0, started = 0, digits = 0;
char *q = line;
if (*q < '0' || *q > '9') continue; // a header line
while (*q >= '0' && *q <= '9') { cookie = cookie * 10 + (unsigned)(*q++ - '0'); digits++; }
while (*q == ' ') q++;
while (*q >= '0' && *q <= '9') pid = pid * 10 + (*q++ - '0');
while (*q == ' ') q++;
while (*q >= '0' && *q <= '9') started = started * 10 + (*q++ - '0');
while (*q == ' ') q++;
if (!digits || pid <= 0 || !*q) continue;
children[slot].pid = pid;
children[slot].cookie = cookie;
children[slot].started = started;
scopy(children[slot].label, q, SCHINIT_MAXLABEL);
slot++;
}
if (slot) {
out("schinit: reclaimed ");
outn(slot);
out(" running job(s) from /proc/schinit\n");
}
}
// ---------------------------------------------------------------------------
// Spawning, on request
// ---------------------------------------------------------------------------
static void
redirect(const char *path, int fd, int flags)
{
if (!path || !path[0])
return;
close(fd);
if (open((char *)path, flags) != fd) {
// Could not put it on the right descriptor. Say nothing -- this runs
// in the child, between fork and exec, and the console may be exactly
// the thing that failed to open.
}
}
// Fork and exec one job. Returns the pid, or a negative number.
//
// Everything this needs arrives in the message. It does not read a file, does
// not look at a manifest, and does not consult any policy: schinitd worked all
// of that out and sent the answer.
static int
spawn(struct schinit_spawn *s)
{
// Rebuild argv and envp from the flat buffer. Pointers cannot cross a
// process boundary, so the message carries offsets and they are turned
// back into pointers here, on this stack, where they are valid.
char *argv[SCHINIT_MAXARGS + 1];
char *envp[SCHINIT_MAXENVV + 1];
unsigned int na = s->nargs, ne = s->nenvs;
if (na > SCHINIT_MAXARGS) na = SCHINIT_MAXARGS;
if (ne > SCHINIT_MAXENVV) ne = SCHINIT_MAXENVV;
// A hostile or broken offset must not walk off the buffer. The strings are
// NUL-terminated inside strbuf and the last byte is forced to NUL below,
// so a bad offset yields a short string rather than a read past the end.
s->strbuf[SCHINIT_STRBUF - 1] = 0;
s->program[SCHINIT_MAXPATH - 1] = 0;
s->label[SCHINIT_MAXLABEL - 1] = 0;
for (unsigned int i = 0; i < na; i++) {
unsigned short off = s->args[i];
argv[i] = (off < SCHINIT_STRBUF) ? &s->strbuf[off] : (char *)"";
}
argv[na] = 0;
for (unsigned int i = 0; i < ne; i++) {
unsigned short off = s->envs[i];
envp[i] = (off < SCHINIT_STRBUF) ? &s->strbuf[off] : (char *)"";
}
envp[ne] = 0;
if (!argv[0])
argv[0] = s->program;
int pid = fork();
if (pid < 0)
return -1;
if (pid == 0) {
// A session of its own, so terminal input reaches it. A session with
// no controlling terminal is not delivered terminal input at all --
// without this the shell echoes and executes nothing.
setsid();
redirect(s->stdin_path, 0, O_RDWR);
redirect(s->stdout_path, 1, O_RDWR);
redirect(s->stderr_path, 2, O_RDWR);
if (s->own_tty)
ioctl(0, TIOCSCTTY, 0);
// The socket to schinitd is PID 1's and nobody else's.
close(sock);
execve(s->program, argv, ne ? envp : 0);
out("schinit: cannot exec ");
out(s->program);
out("\n");
exit();
}
return pid;
}
// ---------------------------------------------------------------------------
// The supervisor
// ---------------------------------------------------------------------------
static int
start_supervisor(void)
{
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
out("schinit: socketpair failed\n");
return -1;
}
int pid = fork();
if (pid < 0) {
close(sv[0]);
close(sv[1]);
return -1;
}
if (pid == 0) {
close(sv[0]);
// A fixed descriptor rather than an argument: schinitd is exec'd, so
// there is nowhere to pass a number, and a well-known one is one less
// thing for the two of them to disagree about.
if (sv[1] != SCHINIT_SOCK_FD) {
close(SCHINIT_SOCK_FD);
dup2(sv[1], SCHINIT_SOCK_FD);
close(sv[1]);
}
char *argv[] = { (char *)"schinitd", 0 };
execve((char *)SUPERVISOR, argv, 0);
out("schinit: cannot exec " SUPERVISOR "\n");
exit();
}
close(sv[1]);
sock = sv[0];
supervisor_pid = pid;
last_pong = last_ping = uptime(); // it has not missed anything yet
return pid;
}
static void
send_reply(unsigned int op, unsigned int cookie, int pid, int status)
{
struct schinit_reply r;
r.op = op;
r.cookie = cookie;
r.pid = pid;
r.status = status;
// A supervisor that has stopped reading must not wedge PID 1. The socket
// buffer is far larger than anything this sends in a burst, and if the
// write fails the supervisor is already lost -- it will be restarted, and
// it will read /proc/schinit rather than this message.
write(sock, &r, sizeof(r));
}
// One message from schinitd. Returns 0 if the socket has closed.
static int
serve(void)
{
union schinit_msg m;
int n = read(sock, &m, sizeof(m));
if (n <= 0)
return 0; // supervisor gone
switch (m.op) {
case SCHINIT_HELLO: {
struct schinit_hello h;
h.op = SCHINIT_WELCOME;
h.proto = SCHINIT_PROTO;
h.generation = generation;
h.reserved = 0;
write(sock, &h, sizeof(h));
break;
}
case SCHINIT_PONG:
last_pong = uptime();
break;
case SCHINIT_SPAWN: {
int pid = spawn(&m.spawn);
if (pid > 0) {
for (int i = 0; i < MAXJOBS; i++) {
if (children[i].pid)
continue;
children[i].pid = pid;
children[i].cookie = m.spawn.cookie;
children[i].started = uptime();
scopy(children[i].label, m.spawn.label, SCHINIT_MAXLABEL);
break;
}
publish();
}
send_reply(SCHINIT_SPAWNED, m.spawn.cookie, pid, 0);
break;
}
default:
// An op PID 1 does not know. Ignore it rather than guess: this is the
// only process that cannot afford to act on a message it misread.
break;
}
return 1;
}
// ---------------------------------------------------------------------------
int
main(int argc, char *argv[])
{
// argv[1] is the generation, and it is only there when the kernel put it
// there -- proc_reincarnate() re-execs this program in place after PID 1
// has died. A normal boot has no argument and is generation 1.
if (argc > 1) {
unsigned int g = 0;
for (char *q = argv[1]; *q >= '0' && *q <= '9'; q++)
g = g * 10 + (unsigned int)(*q - '0');
if (g)
generation = g;
}
bootstrap();
// Only now: /proc is what bootstrap() mounts, and this reads a file in it.
// Before the mount it silently returned "quiet" every time, which made the
// verbose boot quiet in exactly the one process that runs first.
read_bootmode();
// A reincarnated PID 1 has the children but not the table.
//
// exec() replaced this program's memory and kept everything else -- the
// pid, the open files, and crucially the *children*, which are still
// parented here because nothing killed them. What did not survive is the
// array describing them, so it is read back from where it was published
// precisely so that it could be.
//
// Without this the next publish() overwrites the record with an empty
// table, the supervisor reads that, believes nothing is running and starts
// everything a second time. Same split brain as before, one level up.
if (generation > 1) {
// exec() keeps open files, which is what lets this program still have
// a console to complain to -- and also means the previous life's
// socket to the old supervisor is still open here. Leaving it open
// holds the old supervisor's channel up, so it never sees a hangup,
// never exits, and sits there reporting that PID 1 has stopped
// pinging it. Which is true, and no longer its business.
for (int fd = 3; fd < 32; fd++)
close(fd);
reclaim();
}
// Quiet unless asked, or unless this is not the first pid 1.
//
// "SchOS process 1" is a line about the system working exactly as designed,
// which is the definition of what a quiet boot leaves out. A *reincarnated*
// pid 1 is the opposite -- it means the last one died and was replaced --
// so that one prints either way.
if (verbose_boot || generation > 1) {
out("schinit: SchOS process 1");
if (generation > 1) {
out(", generation ");
outn((int)generation);
out(" (reincarnated)");
}
out("\n");
}
// Generation 4 is the safety net: a manifest that kills init must not kill
// it for ever, so this generation does not read /etc/schinit at all. One
// shell, on the console, and nothing else -- enough to log in and fix
// whatever has been taking the machine down.
if (generation >= 4) {
out("schinit: safe mode -- console shell only, /etc/schinit ignored\n");
if (fork() == 0) {
setsid();
ioctl(0, TIOCSCTTY, 0);
char *sh[] = { (char *)"-zsh", 0 };
execve((char *)"/bin/zsh", sh, 0);
char *fb[] = { (char *)"sh", 0 };
execve((char *)"/bin/sh", fb, 0);
exit();
}
publish();
for (;;) {
int st, pid;
while ((pid = waitpid(-1, &st, WNOHANG)) > 0)
;
sleep(20);
}
}
if (start_supervisor() < 0) {
// No supervisor. Rather than idle usefully-looking, fall back to a
// shell so the machine is still usable and the failure is visible.
out("schinit: no supervisor; falling back to a shell\n");
if (fork() == 0) {
setsid();
char *argv[] = { (char *)"sh", 0 };
execve((char *)"/bin/sh", argv, 0);
exit();
}
}
publish();
if (verbose_boot)
out("schinit: ready (pid 1)\n");
for (;;) {
// Two things can happen -- a request arrives, or a child exits -- and
// both of them block. poll() with a timeout plus WNOHANG turns that
// into one loop with no signal handler, which is the simplest thing
// that works and therefore the right thing for this program.
struct pollfd pf;
pf.fd = sock;
pf.events = POLLIN;
pf.revents = 0;
if (sock >= 0 && poll(&pf, 1, 200) > 0 && (pf.revents & POLLIN)) {
if (!serve()) {
close(sock);
sock = -1;
}
} else if (sock < 0) {
sleep(10);
}
// The heartbeat. A supervisor that has stopped answering is stuck,
// and a stuck supervisor is indistinguishable from a working one by
// any means except asking it something -- which is the whole reason
// this exists. See docs/architecture/init.md, "The heartbeat".
if (sock >= 0) {
int now = uptime();
if (now - last_ping >= SCHINIT_PING_SECS * 100) {
struct schinit_hello p;
p.op = SCHINIT_PING;
p.proto = SCHINIT_PROTO;
p.generation = generation;
p.reserved = 0;
write(sock, &p, sizeof(p));
last_ping = now;
}
if (now - last_pong >= SCHINIT_DEADLINE_SECS * 100) {
out("schinit: supervisor stopped answering; replacing it\n");
// Ask it to die, then replace it *without waiting*.
//
// kill() sets a flag the target checks the next time it runs,
// and the whole premise here is that it is not running. A
// supervisor stuck in a loop will notice at its next trap; one
// blocked forever never will. Waiting for a corpse that may
// never be produced is how a recovery path becomes a second
// hang -- which is exactly what the first version of this did.
//
// Two supervisors briefly existing is safe, and safe in the
// way that matters: the old one's socket is closed here, so
// the moment it runs again its first write fails and it exits.
// It cannot spawn anything, because it cannot ask.
kill(supervisor_pid);
close(sock);
sock = -1;
int old = supervisor_pid;
if (start_supervisor() > 0)
out("schinit: new supervisor started\n");
publish(); // the table names the live supervisor
(void)old;
}
}
int status;
int pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (pid == supervisor_pid) {
out("schinit: supervisor died; restarting\n");
if (sock >= 0) { close(sock); sock = -1; }
supervisor_pid = 0;
// The jobs are untouched. They are children of this process,
// not of the supervisor, so nothing was orphaned and nothing
// needs restarting -- which is the entire reason jobs are
// forked here. The new supervisor reads /proc/schinit and
// picks up where the old one stopped.
sleep(100); // one second, so a crash loop is slow
start_supervisor();
publish();
continue;
}
for (int i = 0; i < MAXJOBS; i++) {
if (children[i].pid != pid)
continue;
unsigned int cookie = children[i].cookie;
children[i].pid = 0;
children[i].label[0] = 0;
publish();
if (sock >= 0)
send_reply(SCHINIT_EXITED, cookie, pid, status);
break;
}
// A pid in no slot is an orphan the kernel reparented here. It has
// been reaped, which was the whole obligation.
}
}
}