Path: blob/main/usr.sbin/bsdinstall/runconsoles/runconsoles.c
106843 views
/*-1* SPDX-License-Identifier: BSD-2-Clause2*3* Copyright (c) 2022 Jessica Clarke <[email protected]>4*5* Redistribution and use in source and binary forms, with or without6* modification, are permitted provided that the following conditions7* are met:8* 1. Redistributions of source code must retain the above copyright9* notice, this list of conditions and the following disclaimer.10* 2. Redistributions in binary form must reproduce the above copyright11* notice, this list of conditions and the following disclaimer in the12* documentation and/or other materials provided with the distribution.13*14* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND15* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE16* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE17* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE18* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL19* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS20* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)21* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT22* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY23* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF24* SUCH DAMAGE.25*/2627/*28* We create the following process hierarchy:29*30* runconsoles utility31* |-- runconsoles [ttyX]32* | `-- utility primary33* |-- runconsoles [ttyY]34* | `-- utility secondary35* ...36* `-- runconsoles [ttyZ]37* `-- utility secondary38*39* Whilst the intermediate processes might seem unnecessary, they are important40* so we can ensure the session leader stays around until the actual program41* being run and all its children have exited when killing them (and, in the42* case of our controlling terminal, that nothing in our current session goes43* on to write to it before then), giving them a chance to clean up the44* terminal (important if a dialog box is showing).45*46* Each of the intermediate processes acquires reaper status, allowing it to47* kill its descendants, not just a single process group, and wait until all48* have finished, not just its immediate child.49*/5051#include <sys/param.h>52#include <sys/errno.h>53#include <sys/queue.h>54#include <sys/resource.h>55#include <sys/sysctl.h>56#include <sys/wait.h>5758#include <err.h>59#include <errno.h>60#include <fcntl.h>61#include <getopt.h>62#include <signal.h>63#include <stdarg.h>64#include <stdbool.h>65#include <stdio.h>66#include <stdlib.h>67#include <string.h>68#include <sysexits.h>69#include <termios.h>70#include <ttyent.h>71#include <unistd.h>7273#include "common.h"74#include "child.h"7576struct consinfo {77const char *name;78STAILQ_ENTRY(consinfo) link;79int fd;80/* -1: not started, 0: reaped */81volatile pid_t pid;82volatile int exitstatus;83};8485STAILQ_HEAD(consinfo_list, consinfo);8687static struct consinfo_list consinfos;88static struct consinfo *primary_consinfo;89static struct consinfo *controlling_consinfo;9091static struct consinfo * volatile first_sigchld_consinfo;9293static struct pipe_barrier wait_first_child_barrier;94static struct pipe_barrier wait_all_children_barrier;9596static const char primary[] = "primary";97static const char secondary[] = "secondary";9899static const struct option longopts[] = {100{ "help", no_argument, NULL, 'h' },101{ NULL, 0, NULL, 0 }102};103104static void105kill_consoles(int sig)106{107struct consinfo *consinfo;108sigset_t set, oset;109110/* Temporarily block signals so PID reading and killing are atomic */111sigfillset(&set);112sigprocmask(SIG_BLOCK, &set, &oset);113STAILQ_FOREACH(consinfo, &consinfos, link) {114if (consinfo->pid != -1 && consinfo->pid != 0)115kill(consinfo->pid, sig);116}117sigprocmask(SIG_SETMASK, &oset, NULL);118}119120static void121sigalrm_handler(int code __unused)122{123int saved_errno;124125saved_errno = errno;126kill_consoles(SIGKILL);127errno = saved_errno;128}129130static void131wait_all_consoles(void)132{133sigset_t set, oset;134int error;135136err_set_exit(NULL);137138/*139* We may be run in a context where SIGALRM is blocked; temporarily140* unblock so we can SIGKILL. Similarly, SIGCHLD may be blocked, but if141* we're waiting on the pipe we need to make sure it's not.142*/143sigemptyset(&set);144sigaddset(&set, SIGALRM);145sigaddset(&set, SIGCHLD);146sigprocmask(SIG_UNBLOCK, &set, &oset);147alarm(KILL_TIMEOUT);148pipe_barrier_wait(&wait_all_children_barrier);149alarm(0);150sigprocmask(SIG_SETMASK, &oset, NULL);151152if (controlling_consinfo != NULL) {153error = tcsetpgrp(controlling_consinfo->fd,154getpgrp());155if (error != 0)156err(EX_OSERR, "could not give up control of %s",157controlling_consinfo->name);158}159}160161static void162kill_wait_all_consoles(int sig)163{164kill_consoles(sig);165wait_all_consoles();166}167168static void169kill_wait_all_consoles_err_exit(int eval __unused)170{171kill_wait_all_consoles(SIGTERM);172}173174static void __dead2175exit_signal_handler(int code)176{177struct consinfo *consinfo;178bool started_console;179180started_console = false;181STAILQ_FOREACH(consinfo, &consinfos, link) {182if (consinfo->pid != -1) {183started_console = true;184break;185}186}187188/*189* If we haven't yet started a console, don't wait for them, since190* we'll never get a SIGCHLD that will wake us up.191*/192if (started_console)193kill_wait_all_consoles(SIGTERM);194195reproduce_signal_death(code);196exit(EXIT_FAILURE);197}198199static void200sigchld_handler_reaped_one(pid_t pid, int status)201{202struct consinfo *consinfo, *child_consinfo;203bool others;204205child_consinfo = NULL;206others = false;207STAILQ_FOREACH(consinfo, &consinfos, link) {208/*209* NB: No need to check consinfo->pid as the caller is210* responsible for passing a valid PID211*/212if (consinfo->pid == pid)213child_consinfo = consinfo;214else if (consinfo->pid != -1 && consinfo->pid != 0)215others = true;216}217218if (child_consinfo == NULL)219return;220221child_consinfo->pid = 0;222child_consinfo->exitstatus = status;223224if (first_sigchld_consinfo == NULL) {225first_sigchld_consinfo = child_consinfo;226pipe_barrier_ready(&wait_first_child_barrier);227}228229if (others)230return;231232pipe_barrier_ready(&wait_all_children_barrier);233}234235static void236sigchld_handler(int code __unused)237{238int status, saved_errno;239pid_t pid;240241saved_errno = errno;242while ((void)(pid = waitpid(-1, &status, WNOHANG)),243pid != -1 && pid != 0)244sigchld_handler_reaped_one(pid, status);245errno = saved_errno;246}247248static const char *249read_primary_console(void)250{251char *buf, *p, *cons;252size_t len;253int error;254255/*256* NB: Format is "cons,...cons,/cons,...cons,", with the list before257* the / being the set of configured consoles, and the list after being258* the list of available consoles.259*/260error = sysctlbyname("kern.console", NULL, &len, NULL, 0);261if (error == -1)262err(EX_OSERR, "could not read kern.console length");263buf = malloc(len);264if (buf == NULL)265err(EX_OSERR, "could not allocate kern.console buffer");266error = sysctlbyname("kern.console", buf, &len, NULL, 0);267if (error == -1)268err(EX_OSERR, "could not read kern.console");269270/* Truncate at / to get just the configured consoles */271p = strchr(buf, '/');272if (p == NULL)273errx(EX_OSERR, "kern.console malformed: no / found");274*p = '\0';275276/*277* Truncate at , to get just the first configured console, the primary278* ("high level") one.279*/280p = strchr(buf, ',');281if (p != NULL)282*p = '\0';283284if (*buf != '\0')285cons = strdup(buf);286else287cons = NULL;288289free(buf);290291return (cons);292}293294static void295read_consoles(void)296{297const char *primary_console;298struct consinfo *consinfo;299int fd, error, flags;300struct ttyent *tty;301char *dev, *name;302pid_t pgrp;303304primary_console = read_primary_console();305306STAILQ_INIT(&consinfos);307while ((tty = getttyent()) != NULL) {308if ((tty->ty_status & TTY_ON) == 0)309continue;310311/*312* Only use the first VTY; starting on others is pointless as313* they're multiplexed, and they get used to show the install314* log and start a shell.315*/316if (strncmp(tty->ty_name, "ttyv", 4) == 0 &&317strcmp(tty->ty_name + 4, "0") != 0)318continue;319320consinfo = malloc(sizeof(struct consinfo));321if (consinfo == NULL)322err(EX_OSERR, "could not allocate consinfo");323324asprintf(&dev, "/dev/%s", tty->ty_name);325if (dev == NULL)326err(EX_OSERR, "could not allocate dev path");327328name = dev + 5;329fd = open(dev, O_RDWR | O_NONBLOCK);330if (fd == -1)331err(EX_IOERR, "could not open %s", dev);332333flags = fcntl(fd, F_GETFL);334if (flags == -1)335err(EX_IOERR, "could not get flags for %s", dev);336337error = fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);338if (error == -1)339err(EX_IOERR, "could not set flags for %s", dev);340341if (tcgetsid(fd) != -1) {342/*343* No need to check controlling session is ours as344* tcgetsid fails with ENOTTY if not.345*/346pgrp = tcgetpgrp(fd);347if (pgrp == -1)348err(EX_IOERR, "could not get pgrp of %s",349dev);350else if (pgrp != getpgrp())351errx(EX_IOERR, "%s controlled by another group",352dev);353354if (controlling_consinfo != NULL)355errx(EX_OSERR,356"multiple controlling terminals %s and %s",357controlling_consinfo->name, name);358359controlling_consinfo = consinfo;360}361362consinfo->name = name;363consinfo->pid = -1;364consinfo->fd = fd;365consinfo->exitstatus = -1;366STAILQ_INSERT_TAIL(&consinfos, consinfo, link);367368if (primary_console != NULL &&369strcmp(consinfo->name, primary_console) == 0)370primary_consinfo = consinfo;371}372373endttyent();374free(__DECONST(char *, primary_console));375376if (STAILQ_EMPTY(&consinfos))377errx(EX_OSERR, "no consoles found");378379if (primary_consinfo == NULL) {380warnx("no primary console found, using first");381primary_consinfo = STAILQ_FIRST(&consinfos);382}383}384385static void386start_console(struct consinfo *consinfo, const char **argv,387char *primary_secondary, struct pipe_barrier *start_barrier,388const sigset_t *oset)389{390pid_t pid;391392if (consinfo == primary_consinfo)393strcpy(primary_secondary, primary);394else395strcpy(primary_secondary, secondary);396397fprintf(stderr, "Starting %s installer on %s\n", primary_secondary,398consinfo->name);399400pid = fork();401if (pid == -1)402err(EX_OSERR, "could not fork");403404if (pid == 0) {405/* Redundant for the first fork but not subsequent ones */406err_set_exit(NULL);407408/*409* We need to destroy the ready ends so we don't block these410* parent-only self-pipes, and might as well destroy the wait411* ends too given we're not going to use them.412*/413pipe_barrier_destroy(&wait_first_child_barrier);414pipe_barrier_destroy(&wait_all_children_barrier);415416child_leader_run(consinfo->name, consinfo->fd,417consinfo != controlling_consinfo, argv, oset,418start_barrier);419}420421consinfo->pid = pid;422423/*424* We have at least one child now so make sure we kill children on425* exit. We also must not do this until we have at least one since426* otherwise we will never receive a SIGCHLD that will ready the pipe427* barrier and thus we will wait forever.428*/429err_set_exit(kill_wait_all_consoles_err_exit);430}431432static void433start_consoles(int argc, char **argv)434{435struct pipe_barrier start_barrier;436struct consinfo *consinfo;437char *primary_secondary;438const char **newargv;439struct sigaction sa;440sigset_t set, oset;441int error, i;442443error = pipe_barrier_init(&start_barrier);444if (error != 0)445err(EX_OSERR, "could not create start children barrier");446447error = pipe_barrier_init(&wait_first_child_barrier);448if (error != 0)449err(EX_OSERR, "could not create wait first child barrier");450451error = pipe_barrier_init(&wait_all_children_barrier);452if (error != 0)453err(EX_OSERR, "could not create wait all children barrier");454455/*456* About to start children, so use our SIGCHLD handler to get notified457* when we need to stop. Once the first child has started we will have458* registered kill_wait_all_consoles_err_exit which needs our SIGALRM handler to459* SIGKILL the children on timeout; do it up front so we can err if it460* fails beforehand.461*462* Also set up our SIGTERM (and SIGINT and SIGQUIT if we're keeping463* control of this terminal) handler before we start children so we can464* clean them up when signalled.465*/466sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;467sa.sa_handler = sigchld_handler;468sigfillset(&sa.sa_mask);469error = sigaction(SIGCHLD, &sa, NULL);470if (error != 0)471err(EX_OSERR, "could not enable SIGCHLD handler");472sa.sa_flags = SA_RESTART;473sa.sa_handler = sigalrm_handler;474error = sigaction(SIGALRM, &sa, NULL);475if (error != 0)476err(EX_OSERR, "could not enable SIGALRM handler");477sa.sa_handler = exit_signal_handler;478error = sigaction(SIGTERM, &sa, NULL);479if (error != 0)480err(EX_OSERR, "could not enable SIGTERM handler");481if (controlling_consinfo == NULL) {482error = sigaction(SIGINT, &sa, NULL);483if (error != 0)484err(EX_OSERR, "could not enable SIGINT handler");485error = sigaction(SIGQUIT, &sa, NULL);486if (error != 0)487err(EX_OSERR, "could not enable SIGQUIT handler");488}489490/*491* Ignore SIGINT/SIGQUIT in parent if a child leader will take control492* of this terminal so only it gets them, and ignore SIGPIPE in parent,493* and child until unblocked, since we're using pipes internally as494* synchronisation barriers between parent and children.495*496* Also ignore SIGTTOU so we can print errors if needed after the child497* has started.498*/499sa.sa_flags = SA_RESTART;500sa.sa_handler = SIG_IGN;501if (controlling_consinfo != NULL) {502error = sigaction(SIGINT, &sa, NULL);503if (error != 0)504err(EX_OSERR, "could not ignore SIGINT");505error = sigaction(SIGQUIT, &sa, NULL);506if (error != 0)507err(EX_OSERR, "could not ignore SIGQUIT");508}509error = sigaction(SIGPIPE, &sa, NULL);510if (error != 0)511err(EX_OSERR, "could not ignore SIGPIPE");512error = sigaction(SIGTTOU, &sa, NULL);513if (error != 0)514err(EX_OSERR, "could not ignore SIGTTOU");515516/*517* Create a fresh copy of the argument array and perform %-substitution;518* a literal % will be replaced with primary_secondary, and any other519* string that starts % will have the leading % removed (thus arguments520* that should start with a % should be escaped with an additional %).521*522* Having all % arguments use primary_secondary means that copying523* either "primary" or "secondary" to it will yield the final argument524* array for the child in constant time, regardless of how many appear.525*/526newargv = malloc(((size_t)argc + 1) * sizeof(char *));527if (newargv == NULL)528err(EX_OSERR, "could not allocate newargv");529530primary_secondary = malloc(MAX(sizeof(primary), sizeof(secondary)));531if (primary_secondary == NULL)532err(EX_OSERR, "could not allocate primary_secondary");533534newargv[0] = argv[0];535for (i = 1; i < argc; ++i) {536switch (argv[i][0]) {537case '%':538if (argv[i][1] == '\0')539newargv[i] = primary_secondary;540else541newargv[i] = argv[i] + 1;542break;543default:544newargv[i] = argv[i];545break;546}547}548newargv[argc] = NULL;549550/*551* Temporarily block signals. The parent needs forking, assigning552* consinfo->pid and, for the first iteration, calling err_set_exit, to553* be atomic, and the child leader shouldn't have signals re-enabled554* until it has configured its signal handlers appropriately as the555* current ones are for the parent's handling of children.556*/557sigfillset(&set);558sigprocmask(SIG_BLOCK, &set, &oset);559STAILQ_FOREACH(consinfo, &consinfos, link)560start_console(consinfo, newargv, primary_secondary,561&start_barrier, &oset);562sigprocmask(SIG_SETMASK, &oset, NULL);563564/* Now ready for children to start */565pipe_barrier_ready(&start_barrier);566}567568static int569wait_consoles(void)570{571pipe_barrier_wait(&wait_first_child_barrier);572573/*574* Once one of our children has exited, kill off the rest and wait for575* them all to exit. This will also set the foreground process group of576* the controlling terminal back to ours if it's one of the consoles.577*/578kill_wait_all_consoles(SIGTERM);579580if (first_sigchld_consinfo == NULL)581errx(EX_SOFTWARE, "failed to find first child that exited");582583return (first_sigchld_consinfo->exitstatus);584}585586static void __dead2587usage(void)588{589fprintf(stderr, "usage: %s utility [argument ...]", getprogname());590exit(EX_USAGE);591}592593int594main(int argc, char **argv)595{596int ch, status;597598while ((ch = getopt_long(argc, argv, "+h", longopts, NULL)) != -1) {599switch (ch) {600case 'h':601default:602usage();603}604}605606argc -= optind;607argv += optind;608609if (argc < 2)610usage();611612/*613* Gather the list of enabled consoles from /etc/ttys, ignoring VTYs614* other than ttyv0 since they're used for other purposes when the615* installer is running, and there would be no point having multiple616* copies on each of the multiplexed virtual consoles anyway.617*/618read_consoles();619620/*621* Start the installer on all the consoles. Do not print after this622* point until our process group is in the foreground again unless623* necessary (we ignore SIGTTOU so we can print errors, but don't want624* to garble a child's output).625*/626start_consoles(argc, argv);627628/*629* Wait for one of the installers to exit, kill the rest, become the630* foreground process group again and get the exit code of the first631* child to exit.632*/633status = wait_consoles();634635/*636* Reproduce the exit code of the first child to exit, including637* whether it was a fatal signal or normal termination.638*/639if (WIFSIGNALED(status))640reproduce_signal_death(WTERMSIG(status));641642if (WIFEXITED(status))643return (WEXITSTATUS(status));644645return (EXIT_FAILURE);646}647648649