/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2022 Jessica Clarke <jrtc27@FreeBSD.org> * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* * We create the following process hierarchy: * * runconsoles utility * |-- runconsoles [ttyX] * | `-- utility primary * |-- runconsoles [ttyY] * | `-- utility secondary * ... * `-- runconsoles [ttyZ] * `-- utility secondary * * Whilst the intermediate processes might seem unnecessary, they are important * so we can ensure the session leader stays around until the actual program * being run and all its children have exited when killing them (and, in the * case of our controlling terminal, that nothing in our current session goes * on to write to it before then), giving them a chance to clean up the * terminal (important if a dialog box is showing). * * Each of the intermediate processes acquires reaper status, allowing it to * kill its descendants, not just a single process group, and wait until all * have finished, not just its immediate child. */ #include <sys/param.h> #include <sys/errno.h> #include <sys/queue.h> #include <sys/resource.h> #include <sys/sysctl.h> #include <sys/wait.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <getopt.h> #include <signal.h> #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sysexits.h> #include <termios.h> #include <ttyent.h> #include <unistd.h> #include "common.h" #include "child.h" struct consinfo { const char *name; STAILQ_ENTRY(consinfo) link; int fd; /* -1: not started, 0: reaped */ volatile pid_t pid; volatile int exitstatus; }; STAILQ_HEAD(consinfo_list, consinfo); static struct consinfo_list consinfos; static struct consinfo *primary_consinfo; static struct consinfo *controlling_consinfo; static struct consinfo * volatile first_sigchld_consinfo; static struct pipe_barrier wait_first_child_barrier; static struct pipe_barrier wait_all_children_barrier; static const char primary[] = "primary"; static const char secondary[] = "secondary"; static const struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { NULL, 0, NULL, 0 } }; static void kill_consoles(int sig) { struct consinfo *consinfo; sigset_t set, oset; /* Temporarily block signals so PID reading and killing are atomic */ sigfillset(&set); sigprocmask(SIG_BLOCK, &set, &oset); STAILQ_FOREACH(consinfo, &consinfos, link) { if (consinfo->pid != -1 && consinfo->pid != 0) kill(consinfo->pid, sig); } sigprocmask(SIG_SETMASK, &oset, NULL); } static void sigalrm_handler(int code __unused) { int saved_errno; saved_errno = errno; kill_consoles(SIGKILL); errno = saved_errno; } static void wait_all_consoles(void) { sigset_t set, oset; int error; err_set_exit(NULL); /* * We may be run in a context where SIGALRM is blocked; temporarily * unblock so we can SIGKILL. Similarly, SIGCHLD may be blocked, but if * we're waiting on the pipe we need to make sure it's not. */ sigemptyset(&set); sigaddset(&set, SIGALRM); sigaddset(&set, SIGCHLD); sigprocmask(SIG_UNBLOCK, &set, &oset); alarm(KILL_TIMEOUT); pipe_barrier_wait(&wait_all_children_barrier); alarm(0); sigprocmask(SIG_SETMASK, &oset, NULL); if (controlling_consinfo != NULL) { error = tcsetpgrp(controlling_consinfo->fd, getpgrp()); if (error != 0) err(EX_OSERR, "could not give up control of %s", controlling_consinfo->name); } } static void kill_wait_all_consoles(int sig) { kill_consoles(sig); wait_all_consoles(); } static void kill_wait_all_consoles_err_exit(int eval __unused) { kill_wait_all_consoles(SIGTERM); } static void __dead2 exit_signal_handler(int code) { struct consinfo *consinfo; bool started_console; started_console = false; STAILQ_FOREACH(consinfo, &consinfos, link) { if (consinfo->pid != -1) { started_console = true; break; } } /* * If we haven't yet started a console, don't wait for them, since * we'll never get a SIGCHLD that will wake us up. */ if (started_console) kill_wait_all_consoles(SIGTERM); reproduce_signal_death(code); exit(EXIT_FAILURE); } static void sigchld_handler_reaped_one(pid_t pid, int status) { struct consinfo *consinfo, *child_consinfo; bool others; child_consinfo = NULL; others = false; STAILQ_FOREACH(consinfo, &consinfos, link) { /* * NB: No need to check consinfo->pid as the caller is * responsible for passing a valid PID */ if (consinfo->pid == pid) child_consinfo = consinfo; else if (consinfo->pid != -1 && consinfo->pid != 0) others = true; } if (child_consinfo == NULL) return; child_consinfo->pid = 0; child_consinfo->exitstatus = status; if (first_sigchld_consinfo == NULL) { first_sigchld_consinfo = child_consinfo; pipe_barrier_ready(&wait_first_child_barrier); } if (others) return; pipe_barrier_ready(&wait_all_children_barrier); } static void sigchld_handler(int code __unused) { int status, saved_errno; pid_t pid; saved_errno = errno; while ((void)(pid = waitpid(-1, &status, WNOHANG)), pid != -1 && pid != 0) sigchld_handler_reaped_one(pid, status); errno = saved_errno; } static const char * read_primary_console(void) { char *buf, *p, *cons; size_t len; int error; /* * NB: Format is "cons,...cons,/cons,...cons,", with the list before * the / being the set of configured consoles, and the list after being * the list of available consoles. */ error = sysctlbyname("kern.console", NULL, &len, NULL, 0); if (error == -1) err(EX_OSERR, "could not read kern.console length"); buf = malloc(len); if (buf == NULL) err(EX_OSERR, "could not allocate kern.console buffer"); error = sysctlbyname("kern.console", buf, &len, NULL, 0); if (error == -1) err(EX_OSERR, "could not read kern.console"); /* Truncate at / to get just the configured consoles */ p = strchr(buf, '/'); if (p == NULL) errx(EX_OSERR, "kern.console malformed: no / found"); *p = '\0'; /* * Truncate at , to get just the first configured console, the primary * ("high level") one. */ p = strchr(buf, ','); if (p != NULL) *p = '\0'; if (*buf != '\0') cons = strdup(buf); else cons = NULL; free(buf); return (cons); } static void read_consoles(void) { const char *primary_console; struct consinfo *consinfo; int fd, error, flags; struct ttyent *tty; char *dev, *name; pid_t pgrp; primary_console = read_primary_console(); STAILQ_INIT(&consinfos); while ((tty = getttyent()) != NULL) { if ((tty->ty_status & TTY_ON) == 0) continue; /* * Only use the first VTY; starting on others is pointless as * they're multiplexed, and they get used to show the install * log and start a shell. */ if (strncmp(tty->ty_name, "ttyv", 4) == 0 && strcmp(tty->ty_name + 4, "0") != 0) continue; consinfo = malloc(sizeof(struct consinfo)); if (consinfo == NULL) err(EX_OSERR, "could not allocate consinfo"); asprintf(&dev, "/dev/%s", tty->ty_name); if (dev == NULL) err(EX_OSERR, "could not allocate dev path"); name = dev + 5; fd = open(dev, O_RDWR | O_NONBLOCK); if (fd == -1) err(EX_IOERR, "could not open %s", dev); flags = fcntl(fd, F_GETFL); if (flags == -1) err(EX_IOERR, "could not get flags for %s", dev); error = fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); if (error == -1) err(EX_IOERR, "could not set flags for %s", dev); if (tcgetsid(fd) != -1) { /* * No need to check controlling session is ours as * tcgetsid fails with ENOTTY if not. */ pgrp = tcgetpgrp(fd); if (pgrp == -1) err(EX_IOERR, "could not get pgrp of %s", dev); else if (pgrp != getpgrp()) errx(EX_IOERR, "%s controlled by another group", dev); if (controlling_consinfo != NULL) errx(EX_OSERR, "multiple controlling terminals %s and %s", controlling_consinfo->name, name); controlling_consinfo = consinfo; } consinfo->name = name; consinfo->pid = -1; consinfo->fd = fd; consinfo->exitstatus = -1; STAILQ_INSERT_TAIL(&consinfos, consinfo, link); if (primary_console != NULL && strcmp(consinfo->name, primary_console) == 0) primary_consinfo = consinfo; } endttyent(); free(__DECONST(char *, primary_console)); if (STAILQ_EMPTY(&consinfos)) errx(EX_OSERR, "no consoles found"); if (primary_consinfo == NULL) { warnx("no primary console found, using first"); primary_consinfo = STAILQ_FIRST(&consinfos); } } static void start_console(struct consinfo *consinfo, const char **argv, char *primary_secondary, struct pipe_barrier *start_barrier, const sigset_t *oset) { pid_t pid; if (consinfo == primary_consinfo) strcpy(primary_secondary, primary); else strcpy(primary_secondary, secondary); fprintf(stderr, "Starting %s installer on %s\n", primary_secondary, consinfo->name); pid = fork(); if (pid == -1) err(EX_OSERR, "could not fork"); if (pid == 0) { /* Redundant for the first fork but not subsequent ones */ err_set_exit(NULL); /* * We need to destroy the ready ends so we don't block these * parent-only self-pipes, and might as well destroy the wait * ends too given we're not going to use them. */ pipe_barrier_destroy(&wait_first_child_barrier); pipe_barrier_destroy(&wait_all_children_barrier); child_leader_run(consinfo->name, consinfo->fd, consinfo != controlling_consinfo, argv, oset, start_barrier); } consinfo->pid = pid; /* * We have at least one child now so make sure we kill children on * exit. We also must not do this until we have at least one since * otherwise we will never receive a SIGCHLD that will ready the pipe * barrier and thus we will wait forever. */ err_set_exit(kill_wait_all_consoles_err_exit); } static void start_consoles(int argc, char **argv) { struct pipe_barrier start_barrier; struct consinfo *consinfo; char *primary_secondary; const char **newargv; struct sigaction sa; sigset_t set, oset; int error, i; error = pipe_barrier_init(&start_barrier); if (error != 0) err(EX_OSERR, "could not create start children barrier"); error = pipe_barrier_init(&wait_first_child_barrier); if (error != 0) err(EX_OSERR, "could not create wait first child barrier"); error = pipe_barrier_init(&wait_all_children_barrier); if (error != 0) err(EX_OSERR, "could not create wait all children barrier"); /* * About to start children, so use our SIGCHLD handler to get notified * when we need to stop. Once the first child has started we will have * registered kill_wait_all_consoles_err_exit which needs our SIGALRM handler to * SIGKILL the children on timeout; do it up front so we can err if it * fails beforehand. * * Also set up our SIGTERM (and SIGINT and SIGQUIT if we're keeping * control of this terminal) handler before we start children so we can * clean them up when signalled. */ sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; sa.sa_handler = sigchld_handler; sigfillset(&sa.sa_mask); error = sigaction(SIGCHLD, &sa, NULL); if (error != 0) err(EX_OSERR, "could not enable SIGCHLD handler"); sa.sa_flags = SA_RESTART; sa.sa_handler = sigalrm_handler; error = sigaction(SIGALRM, &sa, NULL); if (error != 0) err(EX_OSERR, "could not enable SIGALRM handler"); sa.sa_handler = exit_signal_handler; error = sigaction(SIGTERM, &sa, NULL); if (error != 0) err(EX_OSERR, "could not enable SIGTERM handler"); if (controlling_consinfo == NULL) { error = sigaction(SIGINT, &sa, NULL); if (error != 0) err(EX_OSERR, "could not enable SIGINT handler"); error = sigaction(SIGQUIT, &sa, NULL); if (error != 0) err(EX_OSERR, "could not enable SIGQUIT handler"); } /* * Ignore SIGINT/SIGQUIT in parent if a child leader will take control * of this terminal so only it gets them, and ignore SIGPIPE in parent, * and child until unblocked, since we're using pipes internally as * synchronisation barriers between parent and children. * * Also ignore SIGTTOU so we can print errors if needed after the child * has started. */ sa.sa_flags = SA_RESTART; sa.sa_handler = SIG_IGN; if (controlling_consinfo != NULL) { error = sigaction(SIGINT, &sa, NULL); if (error != 0) err(EX_OSERR, "could not ignore SIGINT"); error = sigaction(SIGQUIT, &sa, NULL); if (error != 0) err(EX_OSERR, "could not ignore SIGQUIT"); } error = sigaction(SIGPIPE, &sa, NULL); if (error != 0) err(EX_OSERR, "could not ignore SIGPIPE"); error = sigaction(SIGTTOU, &sa, NULL); if (error != 0) err(EX_OSERR, "could not ignore SIGTTOU"); /* * Create a fresh copy of the argument array and perform %-substitution; * a literal % will be replaced with primary_secondary, and any other * string that starts % will have the leading % removed (thus arguments * that should start with a % should be escaped with an additional %). * * Having all % arguments use primary_secondary means that copying * either "primary" or "secondary" to it will yield the final argument * array for the child in constant time, regardless of how many appear. */ newargv = malloc(((size_t)argc + 1) * sizeof(char *)); if (newargv == NULL) err(EX_OSERR, "could not allocate newargv"); primary_secondary = malloc(MAX(sizeof(primary), sizeof(secondary))); if (primary_secondary == NULL) err(EX_OSERR, "could not allocate primary_secondary"); newargv[0] = argv[0]; for (i = 1; i < argc; ++i) { switch (argv[i][0]) { case '%': if (argv[i][1] == '\0') newargv[i] = primary_secondary; else newargv[i] = argv[i] + 1; break; default: newargv[i] = argv[i]; break; } } newargv[argc] = NULL; /* * Temporarily block signals. The parent needs forking, assigning * consinfo->pid and, for the first iteration, calling err_set_exit, to * be atomic, and the child leader shouldn't have signals re-enabled * until it has configured its signal handlers appropriately as the * current ones are for the parent's handling of children. */ sigfillset(&set); sigprocmask(SIG_BLOCK, &set, &oset); STAILQ_FOREACH(consinfo, &consinfos, link) start_console(consinfo, newargv, primary_secondary, &start_barrier, &oset); sigprocmask(SIG_SETMASK, &oset, NULL); /* Now ready for children to start */ pipe_barrier_ready(&start_barrier); } static int wait_consoles(void) { pipe_barrier_wait(&wait_first_child_barrier); /* * Once one of our children has exited, kill off the rest and wait for * them all to exit. This will also set the foreground process group of * the controlling terminal back to ours if it's one of the consoles. */ kill_wait_all_consoles(SIGTERM); if (first_sigchld_consinfo == NULL) errx(EX_SOFTWARE, "failed to find first child that exited"); return (first_sigchld_consinfo->exitstatus); } static void __dead2 usage(void) { fprintf(stderr, "usage: %s utility [argument ...]", getprogname()); exit(EX_USAGE); } int main(int argc, char **argv) { int ch, status; while ((ch = getopt_long(argc, argv, "+h", longopts, NULL)) != -1) { switch (ch) { case 'h': default: usage(); } } argc -= optind; argv += optind; if (argc < 2) usage(); /* * Gather the list of enabled consoles from /etc/ttys, ignoring VTYs * other than ttyv0 since they're used for other purposes when the * installer is running, and there would be no point having multiple * copies on each of the multiplexed virtual consoles anyway. */ read_consoles(); /* * Start the installer on all the consoles. Do not print after this * point until our process group is in the foreground again unless * necessary (we ignore SIGTTOU so we can print errors, but don't want * to garble a child's output). */ start_consoles(argc, argv); /* * Wait for one of the installers to exit, kill the rest, become the * foreground process group again and get the exit code of the first * child to exit. */ status = wait_consoles(); /* * Reproduce the exit code of the first child to exit, including * whether it was a fatal signal or normal termination. */ if (WIFSIGNALED(status)) reproduce_signal_death(WTERMSIG(status)); if (WIFEXITED(status)) return (WEXITSTATUS(status)); return (EXIT_FAILURE); }