SchOS/cmd/schinit/main.c
Nexoniarz e659c6b187 bug pass: whole console lines, a quiet SCI, an honest reserve warning
- schinit, schinitd and vgpud build every message from pieces, one
  write() each, so a job starting on another CPU landed mid-line:
  "schinitd: starting vgpud: os.schos.resolvconfno virtio-gpu on this
  machine". They now collect a line and write it at the newline;
  resolvconf's verbose line is one write with its prefix.
- The ACPI SCI registers IRQ_QUIET. A desktop's SCI never fires, and the
  poller took two seconds of silence for a misrouted line ("irq 9:
  nothing has arrived on it") and called the handler every tick. A line
  is only exempt while every handler on it is quiet.
- The loader claims each segment from where the last one's claim ended,
  so two segments sharing a page no longer ask for it twice. When a
  claim is still refused, it names the memory-map entries in the way.
- Stale divesymlink() declaration gone; the kernel.md struct table
  re-measured (inode, buf and file pools grow from slabs now).

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

749 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 "core/types.h"
#include "uapi/user.h"
#include "uapi/fcntl.h"
#include "uapi/posix.h"
#include "net/socket.h"
#include "uapi/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'); }
}
// Whole lines, in one write() each: the same buffer as schinitd's, for the
// same reason -- a line written in pieces is a line another process can land
// in the middle of. See emit() there.
static char lbuf[512];
static int lbn;
static void
lflush(void)
{
if (lbn)
write(1, lbuf, lbn);
lbn = 0;
}
static void
emit(const char *s, int n)
{
for (int i = 0; i < n; i++) {
lbuf[lbn++] = s[i];
if (s[i] == '\n' || lbn == (int)sizeof lbuf)
lflush();
}
}
static void
out(const char *s)
{
emit(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");
lflush();
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.
}
}
}