// 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. } } }