// SPDX-License-Identifier: GPL-2.0-or-later // // schinitd -- the supervisor. PID 2, and restartable. // // Everything that used to make PID 1 complicated lives here: reading // /etc/schinit, parsing manifests, deciding what runs, respawn policy and // throttling. docs/architecture/init.md calls this "where the bugs will be", which is // exactly why it is a separate program -- a bug here is a restart, not a panic. // // The one rule that makes the split worth having: // // **This program never forks a job.** It decides, and asks PID 1 to act. So // every job on the machine is a child of PID 1 at all times, and this // process dying 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 children could not be restarted without // either losing them to reparenting or racing the new copy to adopt them -- // the split-brain failure docs/architecture/init.md rejects the two-init scheme for. #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 JOBDIR "/etc/schinit" // Sizes. These mirror the protocol's, deliberately: a job that parses here but // will not fit in a spawn message is a job that fails at the last moment, and // keeping the two the same makes that impossible rather than unlikely. #define MAXJOBS 32 #define MAXARGS SCHINIT_MAXARGS #define MAXENVV SCHINIT_MAXENVV #define MAXLABEL SCHINIT_MAXLABEL #define MAXPATH SCHINIT_MAXPATH #define MAXLINE 256 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); } // Whole lines, in one write() each. // // Every message here is assembled from pieces -- a prefix, a job's label, a // number -- and each piece used to be its own write(). The console keeps one // write() together, but nothing keeps two of them together, so a job starting // on another CPU landed in the middle of the line: "schinitd: starting vgpud: // os.schos.resolvconfno virtio-gpu on this machine" was this process and // vgpud taking turns a word at a time. The pieces now collect here and go out // at the newline. 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)); } // How the machine was booted, read once. // // The routine progress lines below -- one per job started, plus the summary -- // are the userspace half of the same problem the kernel has: worth reading on // a boot you are watching, and noise on the several thousand after it. They // are not deleted, they are held back, and `make qemu` with F8 or any verbose // boot brings all of them back. // // Failures are never held back. A job that could not start, a channel that // closed, a pid 1 that stopped answering: those print either way, because a // silent supervisor is worse than a chatty one. static int verbose_boot; // Whether the boot menu was asked for a network debugger. See // SCHBOOT_DEBUG_FLAG in include/arch/schboot.h -- the kernel only publishes it, and // this is what acts on it, because what it turns on is a job. static int debug_boot; static void read_bootmode(void) { char b[16]; int fd = open("/proc/bootmode", O_RDONLY); int n; if (fd >= 0) { n = read(fd, b, sizeof(b) - 1); close(fd); if (n > 0) { b[n] = 0; verbose_boot = (b[0] == 'v'); } } fd = open("/proc/bootdebug", O_RDONLY); if (fd >= 0) { n = read(fd, b, sizeof(b) - 1); close(fd); if (n > 0) { b[n] = 0; debug_boot = (b[0] == 'o' && b[1] == 'n'); } } } // Progress, as opposed to news. static void vout(const char *s) { if (verbose_boot) 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 // --------------------------------------------------------------------------- struct job { char label[MAXLABEL]; char program[MAXPATH]; char *argv[MAXARGS + 1]; char argstore[MAXLINE]; // Environment, one "KEY=VALUE" per Environment line, repeatable. Stored // as pointers into envstore so the job owns its own strings -- the parse // buffer is gone by the time anything execs. char *envp[MAXENVV + 1]; char envstore[MAXLINE * 2]; int envlen; int envn; char stdin_path[MAXPATH]; char stdout_path[MAXPATH]; char stderr_path[MAXPATH]; int run_at_load; int keep_alive; int disabled; int throttle; // seconds between respawns int own_tty; // claim the console as controlling terminal // A socket the job wants schinit to hold. Parsed now, honoured in // milestone 2 -- recorded here so the manifest format does not have to // change when it is. char sock_path[MAXPATH]; int pid; // 0 when not running unsigned int cookie; // PID 1's handle for this job while it runs int last_start; // uptime() ticks, for throttling int starts; }; static struct job jobs[MAXJOBS]; static int njobs; static void job_defaults(struct job *j) { j->label[0] = j->program[0] = '\0'; j->stdin_path[0] = j->stdout_path[0] = j->stderr_path[0] = '\0'; j->sock_path[0] = '\0'; j->argstore[0] = '\0'; j->argv[0] = 0; j->run_at_load = 0; j->keep_alive = 0; j->disabled = 0; j->throttle = 1; j->own_tty = 0; j->pid = 0; j->last_start = 0; j->starts = 0; } static int truthy(const char *v) { return sieq(v, "yes") || sieq(v, "true") || sieq(v, "1") || sieq(v, "on"); } static int parse_int(const char *s) { int v = 0; while (*s >= '0' && *s <= '9') { v = v * 10 + (*s - '0'); s++; } return v; } // Split Args into argv, in place within the job's own storage. argv[0] is the // program's name as it should see it, which is conventionally the basename and // not the full path -- so a manifest says `Args = sh` for `/bin/sh`. static void split_args(struct job *j, const char *value) { scopy(j->argstore, value, MAXLINE); int n = 0; char *p = j->argstore; while (*p && n < MAXARGS) { while (*p == ' ' || *p == '\t') p++; if (!*p) break; j->argv[n++] = p; while (*p && *p != ' ' && *p != '\t') p++; if (*p) *p++ = '\0'; } j->argv[n] = 0; } // --------------------------------------------------------------------------- // Manifest parsing // // Sectioned key = value. Comments start with #. A [Socket name] section marks // everything after it as belonging to that socket, which is why Path is read // into sock_path rather than treated as a job key. // --------------------------------------------------------------------------- // One "KEY=VALUE" onto the job's environment. Repeatable, so a manifest reads // // Environment = TERM=linux // Environment = PATH=/bin:/usr/bin // // rather than inventing a separator that a value might contain. static void add_env(struct job *j, const char *kv) { if (j->envn >= MAXENVV) return; int n = slen(kv); if (j->envlen + n + 1 > (int)sizeof(j->envstore)) return; char *dst = j->envstore + j->envlen; scopy(dst, kv, n + 1); j->envp[j->envn++] = dst; j->envp[j->envn] = 0; j->envlen += n + 1; } static char * trim(char *s) { while (*s == ' ' || *s == '\t') s++; int n = slen(s); while (n > 0 && (s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\r' || s[n-1] == '\n')) s[--n] = '\0'; return s; } static int load_job(const char *path) { if (njobs >= MAXJOBS) { out("schinit: too many jobs, ignoring "); out(path); out("\n"); return -1; } int fd = open((char *)path, O_RDONLY); if (fd < 0) return -1; // Whole file into one static buffer. A job manifest that does not fit in // 4 KB is a manifest doing something it should not. static char buf[4096]; int n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n <= 0) return -1; buf[n] = '\0'; struct job *j = &jobs[njobs]; job_defaults(j); int in_socket = 0; char *p = buf; while (*p) { char *line = p; while (*p && *p != '\n') p++; if (*p) *p++ = '\0'; // Strip comments before trimming, so trailing comments work too. for (char *c = line; *c; c++) if (*c == '#') { *c = '\0'; break; } line = trim(line); if (!*line) continue; if (line[0] == '[') { // [Socket name] -- the only section kind for now. Anything else is // ignored rather than fatal: an unknown section from a newer // schinit should not stop this one booting the machine. in_socket = (line[1] == 'S' || line[1] == 's'); continue; } char *eq = line; while (*eq && *eq != '=') eq++; if (!*eq) continue; // not key = value; skip *eq = '\0'; char *key = trim(line); char *val = trim(eq + 1); if (in_socket) { if (sieq(key, "Path")) scopy(j->sock_path, val, MAXPATH); continue; } if (sieq(key, "Label")) scopy(j->label, val, MAXLABEL); else if (sieq(key, "Program")) scopy(j->program, val, MAXPATH); else if (sieq(key, "Args")) split_args(j, val); else if (sieq(key, "RunAtLoad")) j->run_at_load = truthy(val); else if (sieq(key, "KeepAlive")) j->keep_alive = truthy(val); else if (sieq(key, "Disabled")) j->disabled = truthy(val); else if (sieq(key, "ThrottleInterval")) j->throttle = parse_int(val); else if (sieq(key, "ControllingTerminal")) j->own_tty = truthy(val); else if (sieq(key, "Environment")) add_env(j, val); else if (sieq(key, "StandardIn")) scopy(j->stdin_path, val, MAXPATH); else if (sieq(key, "StandardOut")) scopy(j->stdout_path, val, MAXPATH); else if (sieq(key, "StandardError")) scopy(j->stderr_path, val, MAXPATH); // Unknown keys are ignored on purpose, same reasoning as sections. } if (!j->program[0]) { out("schinit: "); out(path); out(": no Program, ignoring\n"); return -1; } if (!j->label[0]) scopy(j->label, j->program, MAXLABEL); if (!j->argv[0]) { // No Args given: pass the program path as argv[0], which is what a // program expects when nothing better is said. scopy(j->argstore, j->program, MAXLINE); j->argv[0] = j->argstore; j->argv[1] = 0; } if (j->throttle <= 0) j->throttle = 1; njobs++; return 0; } // Read every *.job in JOBDIR. getdents64 rather than a hardcoded list, so // dropping a file in the directory is all it takes to add a job. static void load_jobs(void) { int fd = open(JOBDIR, O_RDONLY); if (fd < 0) { out("schinit: no " JOBDIR "\n"); return; } static char dbuf[4096]; for (;;) { int n = getdents64(fd, dbuf, sizeof(dbuf)); if (n <= 0) break; int off = 0; while (off < n) { struct dirent64 *d = (struct dirent64 *)(dbuf + off); // Step by d_reclen: records are packed and variable length. if (d->d_reclen == 0) break; off += d->d_reclen; if (d->d_name[0] == '.') continue; if (!ends_with(d->d_name, ".job")) continue; char path[MAXPATH]; scopy(path, JOBDIR "/", MAXPATH); int at = slen(path); scopy(path + at, d->d_name, MAXPATH - at); load_job(path); } } close(fd); } // --------------------------------------------------------------------------- // Talking to PID 1 // --------------------------------------------------------------------------- static int sock = SCHINIT_SOCK_FD; // Flatten a job into a spawn request. Strings are copied into one buffer and // referred to by offset, because pointers do not survive the trip. static int pack(struct schinit_spawn *s, struct job *j, unsigned int cookie) { int used = 0; for (unsigned int z = 0; z < sizeof(*s); z++) ((char *)s)[z] = 0; s->op = SCHINIT_SPAWN; s->cookie = cookie; scopy(s->label, j->label, SCHINIT_MAXLABEL); scopy(s->program, j->program, SCHINIT_MAXPATH); scopy(s->stdin_path, j->stdin_path, SCHINIT_MAXPATH); scopy(s->stdout_path, j->stdout_path, SCHINIT_MAXPATH); scopy(s->stderr_path, j->stderr_path, SCHINIT_MAXPATH); s->own_tty = j->own_tty; for (int i = 0; i < MAXARGS && j->argv[i]; i++) { int n = slen(j->argv[i]) + 1; if (used + n > SCHINIT_STRBUF) return -1; // too much text; refuse rather than trim s->args[s->nargs++] = (unsigned short)used; scopy(&s->strbuf[used], j->argv[i], n); used += n; } for (int i = 0; i < j->envn; i++) { int n = slen(j->envp[i]) + 1; if (used + n > SCHINIT_STRBUF) return -1; s->envs[s->nenvs++] = (unsigned short)used; scopy(&s->strbuf[used], j->envp[i], n); used += n; } return 0; } // Ask PID 1 to start a job. The reply may be preceded by exit notifications // for other jobs, so this reads until it sees the answer to *this* cookie and // handles anything else on the way past. static void note_exit(unsigned int cookie, int pid, int status); static unsigned int next_cookie = 1; static int ask_spawn(struct job *j) { struct schinit_spawn s; unsigned int cookie = next_cookie++; if (pack(&s, j, cookie) < 0) { out("schinitd: "); out(j->label); out(": job too large to send\n"); return -1; } if (write(sock, &s, sizeof(s)) != sizeof(s)) return -1; for (;;) { struct schinit_reply r; if (read(sock, &r, sizeof(r)) != sizeof(r)) return -1; // PID 1 is gone, which cannot happen if (r.op == SCHINIT_SPAWNED && r.cookie == cookie) { if (r.pid > 0) { j->pid = r.pid; j->cookie = cookie; j->last_start = uptime(); j->starts++; } return r.pid; } if (r.op == SCHINIT_EXITED) note_exit(r.cookie, r.pid, r.status); } } static struct job * job_by_cookie(unsigned int cookie) { for (int i = 0; i < njobs; i++) if (jobs[i].cookie == cookie && jobs[i].pid) return &jobs[i]; return 0; } static void note_exit(unsigned int cookie, int pid, int status) { struct job *j = job_by_cookie(cookie); if (!j) return; // not ours, or already noticed j->pid = 0; j->cookie = 0; if (!j->keep_alive) return; // A job that says it cannot run on this machine is believed. The status is // wait(2)-encoded: the low seven bits are the signal that killed it, so a // job that was killed or faulted never matches this and is restarted the // way KeepAlive promises. See SCHINIT_EXIT_NORESTART. if ((status & 0x7f) == 0 && ((status >> 8) & 0xff) == SCHINIT_EXIT_NORESTART) { // Not a failure: the job looked, found the machine did not have what // it drives, and said so properly. Worth reading once and not every // boot on a machine that will never have one. vout("schinitd: "); vout(j->label); vout(" has nothing to do on this machine; not restarting\n"); return; } // Throttle. A job that exits immediately and forever would otherwise spin // the machine; launchd's ThrottleInterval exists for exactly this. int ticks = j->throttle * 100; int since = uptime() - j->last_start; if (since < ticks) sleep(ticks - since); out("schinitd: restarting "); out(j->label); out("\n"); ask_spawn(j); } // --------------------------------------------------------------------------- // Adopting what is already running // --------------------------------------------------------------------------- // Read /proc/schinit and claim the jobs PID 1 is already holding. // // This is the half of "State outside the process" that makes the other half worth // anything. PID 1 publishes the table; without this, a restarted supervisor // reads its manifests, sees nothing running *as far as it knows*, and starts // everything a second time. The console shell then exists twice, both reading // the same terminal, and every keystroke goes to whichever woke first -- the // split-brain failure the plan rejects the two-init scheme for, arrived at by // a different route. // // The state is outside the process precisely so that coming back is a *read* // rather than a guess. static unsigned int adopt(void) { char buf[2048]; int fd = open("/proc/schinit", O_RDONLY); unsigned int highest = 0; if (fd < 0) return 0; int n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n <= 0) return 0; buf[n] = 0; // Lines are "cookie pid started label". The two header lines do not match // that shape and fall out of the parse on their own. char *p = buf; while (*p) { char *line = p; while (*p && *p != '\n') p++; if (*p == '\n') *p++ = 0; unsigned int cookie = 0; int pid = 0, started = 0, f = 0; char *q = line; // cookie if (*q < '0' || *q > '9') continue; while (*q >= '0' && *q <= '9') { cookie = cookie * 10 + (*q++ - '0'); f++; } 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 (!f || !pid || !*q) continue; for (int i = 0; i < njobs; i++) { if (!seq(jobs[i].label, q)) continue; jobs[i].pid = pid; jobs[i].cookie = cookie; jobs[i].last_start = started; out("schinitd: adopting "); out(jobs[i].label); out("\n"); break; } if (cookie > highest) highest = cookie; } return highest; } // --------------------------------------------------------------------------- int main(void) { struct schinit_hello h; read_bootmode(); // Version handshake before anything else. A mismatch is a refusal rather // than a best-effort interpretation -- see the header for why. h.op = SCHINIT_HELLO; h.proto = SCHINIT_PROTO; h.generation = 0; h.reserved = 0; if (write(sock, &h, sizeof(h)) != sizeof(h)) { out("schinitd: no channel to pid 1\n"); lflush(); exit(); } if (read(sock, &h, sizeof(h)) != sizeof(h) || h.op != SCHINIT_WELCOME) { out("schinitd: pid 1 did not answer\n"); lflush(); exit(); } if (h.proto != SCHINIT_PROTO) { out("schinitd: protocol mismatch with pid 1\n"); lflush(); exit(); } load_jobs(); vout("schinitd: "); if (verbose_boot) outn(njobs); vout(" job(s) loaded, generation "); if (verbose_boot) { outn((int)h.generation); out("\n"); } // Anything PID 1 is already holding is ours again, not something to start. unsigned int highest = adopt(); next_cookie = highest + 1; // Everything except whatever owns the console. That one waits for the // greeting below -- see the comment on it. for (int i = 0; i < njobs; i++) { struct job *j = &jobs[i]; // The one job a boot-menu choice can un-disable. // // Deliberately by label rather than by a manifest key like // `EnableIfDebug`: a key would be a general mechanism, and the general // version of "the boot menu can start things that ship switched off" // is a worse idea than the specific one. One job, named here, where // anybody reading this file can see which. if (debug_boot && j->disabled && seq(j->label, "os.schos.schdbgd")) { out("schinitd: remote debugging was chosen at boot; " "starting schdbgd\n"); j->disabled = 0; } if (j->disabled || !j->run_at_load || j->own_tty) continue; if (j->pid) continue; // adopted; it never stopped running vout("schinitd: starting "); vout(j->label); vout("\n"); ask_spawn(j); } vout("schinitd: ready\n"); // The one line a working boot prints, and the last one. // // Here rather than in the kernel because "the machine is up" is not true // until the jobs that make it usable have been started, and because // anything printed before this point would come after it on screen. The // kernel's whole boot is a kinfo() and is in the klog; this is the part a // person is meant to read. // Only the first supervisor of a boot says it. A replacement adopts // running jobs rather than starting them, so greeting the user again would // be announcing a boot that already happened -- and it printed twice for // real, the day schinitd was replaced during startup. if (!verbose_boot && h.generation <= 1) out("\nWelcome to SchOS!" " (F8 at boot for the full log, or dmesg once you are in)\n\n"); // *Now* the shell on the console. // // It used to be started with everything else, and then this greeting was // printed -- which made the two a race that the shell won about half the // time. A boot that sometimes reads // // Welcome to SchOS! (F8 at boot for the full log, ...) // root@schos / ยป // // and sometimes reads the same two lines the other way round is not two // orderings of one boot, it is a greeting that looks like it was printed // by mistake. // // Holding it back until after the write is enough to settle it: this // process writes the line, and only then asks PID 1 to spawn the shell, so // the fork cannot even have happened yet. Nothing else waits on it -- the // console shell is the last job to start either way, and starting it a // few microseconds later costs nothing a person can see. // // `own_tty` rather than a name: the job that claims the console as its // controlling terminal is exactly the job whose output lands here. for (int i = 0; i < njobs; i++) { struct job *j = &jobs[i]; if (j->disabled || !j->run_at_load || !j->own_tty || j->pid) continue; vout("schinitd: starting "); vout(j->label); vout("\n"); ask_spawn(j); } // From here on this program does two things: answer PID 1's heartbeat, and // act on the exits it reports. There is no wait() in this program and // there cannot be -- the children are not ours. // // The read is polled rather than blocking, so that a PID 1 which has // stopped talking is noticed. A blocking read would sit there forever // looking exactly like a quiet machine, which is the failure mode this // whole heartbeat exists to make visible. int last_ping = uptime(); for (;;) { struct pollfd pf; pf.fd = sock; pf.events = POLLIN; pf.revents = 0; int pr = poll(&pf, 1, 1000); // The channel is gone. This happens to the *old* supervisor when PID 1 // has replaced it: PID 1 closes its end, so poll reports a hangup and // never reports readable again. Without this the replaced supervisor // sits in the loop for ever, complaining that PID 1 has stopped // pinging -- which it has, because it is no longer talking to us. if (pr > 0 && (pf.revents & (POLLHUP | POLLERR | POLLNVAL))) { out("schinitd: channel closed; this supervisor has been replaced\n"); lflush(); exit(); } if (pr > 0 && (pf.revents & POLLIN)) { union schinit_msg m; int n = read(sock, &m, sizeof(m)); if (n <= 0) { // PID 1 closed the channel. It is the one process that cannot // have gone away, so this means we are being replaced. out("schinitd: channel closed\n"); lflush(); exit(); } if (m.op == SCHINIT_PING) { struct schinit_hello p; p.op = SCHINIT_PONG; p.proto = SCHINIT_PROTO; p.generation = 0; p.reserved = 0; write(sock, &p, sizeof(p)); last_ping = uptime(); } else if (m.op == SCHINIT_EXITED) { note_exit(m.reply.cookie, m.reply.pid, m.reply.status); } } // Silence from PID 1. // // There is nothing this program can do about it -- PID 1 cannot be // killed and cannot be replaced from here -- so it says so, loudly, // once. docs/architecture/init.md is explicit that this is the limit: a hang // in PID 1 is only caught by noticing, and if both ends are wedged at // once nothing catches it. There is no hardware watchdog. // // Saying so is still worth doing. The alternative is a machine that // looks idle while its init is stuck, which is how this class of bug // stays invisible for months. static int complained; if (!complained && uptime() - last_ping >= SCHINIT_DEADLINE_SECS * 100) { out("schinitd: pid 1 has not pinged for " "the deadline -- it may be wedged\n"); complained = 1; } } }