/*-
* 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);
}