bsdinstall: Add a new runconsoles helper binary

This helper binary will run a given command on every on console, as
defined by /etc/ttys (except for ttyv*, where only ttyv0 will be used).
If one of the command processes exits, the rest will be killed. This
will be used by a future change to start the installer on multiple
consoles.

Reviewed by:	brooks, imp, gjb
Differential Revision:	https://reviews.freebsd.org/D36804
This commit is contained in:
Jessica Clarke 2022-10-03 17:09:16 +01:00
parent f334df7600
commit a2464ee127
8 changed files with 1240 additions and 1 deletions

View File

@ -470,6 +470,7 @@ OLD_FILES+=usr/libexec/bsdinstall/netconfig_ipv4
OLD_FILES+=usr/libexec/bsdinstall/netconfig_ipv6
OLD_FILES+=usr/libexec/bsdinstall/partedit
OLD_FILES+=usr/libexec/bsdinstall/rootpass
OLD_FILES+=usr/libexec/bsdinstall/runconsoles
OLD_FILES+=usr/libexec/bsdinstall/script
OLD_FILES+=usr/libexec/bsdinstall/scriptedpart
OLD_FILES+=usr/libexec/bsdinstall/services

View File

@ -1,7 +1,7 @@
# $FreeBSD$
OSNAME?= FreeBSD
SUBDIR= distextract distfetch partedit scripts
SUBDIR= distextract distfetch partedit runconsoles scripts
SUBDIR_PARALLEL=
SCRIPTS= bsdinstall
MAN= bsdinstall.8

View File

@ -0,0 +1,9 @@
BINDIR= ${LIBEXECDIR}/bsdinstall
PROG= runconsoles
MAN=
SRCS= child.c \
common.c \
runconsoles.c
.include <bsd.prog.mk>

View File

@ -0,0 +1,386 @@
/*-
* 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.
*/
#include <sys/param.h>
#include <sys/errno.h>
#include <sys/procctl.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 <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"
/* -1: not started, 0: reaped */
static volatile pid_t grandchild_pid = -1;
static volatile int grandchild_status;
static struct pipe_barrier wait_grandchild_barrier;
static struct pipe_barrier wait_all_descendants_barrier;
static void
kill_descendants(int sig)
{
struct procctl_reaper_kill rk;
rk.rk_sig = sig;
rk.rk_flags = 0;
procctl(P_PID, getpid(), PROC_REAP_KILL, &rk);
}
static void
sigalrm_handler(int sig __unused)
{
int saved_errno;
saved_errno = errno;
kill_descendants(SIGKILL);
errno = saved_errno;
}
static void
wait_all_descendants(void)
{
sigset_t set, oset;
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_descendants_barrier);
alarm(0);
sigprocmask(SIG_SETMASK, &oset, NULL);
}
static void
sigchld_handler(int sig __unused)
{
int status, saved_errno;
pid_t pid;
saved_errno = errno;
while ((void)(pid = waitpid(-1, &status, WNOHANG)),
pid != -1 && pid != 0) {
/* NB: No need to check grandchild_pid due to the pid checks */
if (pid == grandchild_pid) {
grandchild_status = status;
grandchild_pid = 0;
pipe_barrier_ready(&wait_grandchild_barrier);
}
}
/*
* Another process calling kill(..., SIGCHLD) could cause us to get
* here before we've spawned the grandchild; only ready when we have no
* children if the grandchild has been reaped.
*/
if (pid == -1 && errno == ECHILD && grandchild_pid == 0)
pipe_barrier_ready(&wait_all_descendants_barrier);
errno = saved_errno;
}
static void
exit_signal_handler(int sig)
{
int saved_errno;
/*
* If we get killed before we've started the grandchild then just exit
* with that signal, otherwise kill all our descendants with that
* signal and let the main program pick up the grandchild's death.
*/
if (grandchild_pid == -1) {
reproduce_signal_death(sig);
exit(EXIT_FAILURE);
}
saved_errno = errno;
kill_descendants(sig);
errno = saved_errno;
}
static void
kill_wait_all_descendants(int sig)
{
kill_descendants(sig);
wait_all_descendants();
}
static void
kill_wait_all_descendants_err_exit(int eval __unused)
{
kill_wait_all_descendants(SIGTERM);
}
static void __dead2
grandchild_run(const char **argv, const sigset_t *oset)
{
sig_t orig;
/* Restore signals */
orig = signal(SIGALRM, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGALRM");
orig = signal(SIGCHLD, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGCHLD");
orig = signal(SIGTERM, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGTERM");
orig = signal(SIGINT, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGINT");
orig = signal(SIGQUIT, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGQUIT");
orig = signal(SIGPIPE, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGPIPE");
orig = signal(SIGTTOU, SIG_DFL);
if (orig == SIG_ERR)
err(EX_OSERR, "could not restore SIGTTOU");
/* Now safe to unmask signals */
sigprocmask(SIG_SETMASK, oset, NULL);
/* Only run with stdin/stdout/stderr */
closefrom(3);
/* Ready to execute the requested program */
execvp(argv[0], __DECONST(char * const *, argv));
err(EX_OSERR, "cannot execvp %s", argv[0]);
}
static int
wait_grandchild_descendants(void)
{
pipe_barrier_wait(&wait_grandchild_barrier);
/*
* Once the grandchild itself has exited, kill any lingering
* descendants and wait until we've reaped them all.
*/
kill_wait_all_descendants(SIGTERM);
if (grandchild_pid != 0)
errx(EX_SOFTWARE, "failed to reap grandchild");
return (grandchild_status);
}
void
child_leader_run(const char *name, int fd, bool new_session, const char **argv,
const sigset_t *oset, struct pipe_barrier *start_children_barrier)
{
struct pipe_barrier start_grandchild_barrier;
pid_t pid, sid, pgid;
struct sigaction sa;
int error, status;
sigset_t set;
setproctitle("%s [%s]", getprogname(), name);
error = procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL);
if (error != 0)
err(EX_OSERR, "could not acquire reaper status");
/*
* Set up our own signal handlers for everything the parent overrides
* other than SIGPIPE and SIGTTOU which we leave as ignored, since we
* also use pipe-based synchronisation and want to be able to print
* errors.
*/
sa.sa_flags = SA_RESTART;
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_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");
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");
/*
* Now safe to unmask signals. Note that creating the barriers used by
* the SIGCHLD handler with signals unmasked is safe since they won't
* be used if the grandchild hasn't been forked (and reaped), which
* comes later.
*/
sigprocmask(SIG_SETMASK, oset, NULL);
error = pipe_barrier_init(&start_grandchild_barrier);
if (error != 0)
err(EX_OSERR, "could not create start grandchild barrier");
error = pipe_barrier_init(&wait_grandchild_barrier);
if (error != 0)
err(EX_OSERR, "could not create wait grandchild barrier");
error = pipe_barrier_init(&wait_all_descendants_barrier);
if (error != 0)
err(EX_OSERR, "could not create wait all descendants barrier");
/*
* Create a new session if this is on a different terminal to
* the current one, otherwise just create a new process group to keep
* things as similar as possible between the two cases.
*/
if (new_session) {
sid = setsid();
pgid = sid;
if (sid == -1)
err(EX_OSERR, "could not create session");
} else {
sid = -1;
pgid = getpid();
error = setpgid(0, pgid);
if (error == -1)
err(EX_OSERR, "could not create process group");
}
/* Wait until parent is ready for us to start */
pipe_barrier_destroy_ready(start_children_barrier);
pipe_barrier_wait(start_children_barrier);
/*
* Use the console for stdin/stdout/stderr.
*
* NB: dup2(2) is a no-op if the two fds are equal, and the call to
* closefrom(2) later in the grandchild will close the fd if it isn't
* one of stdin/stdout/stderr already. This means we do not need to
* handle that special case differently.
*/
error = dup2(fd, STDIN_FILENO);
if (error == -1)
err(EX_IOERR, "could not dup %s to stdin", name);
error = dup2(fd, STDOUT_FILENO);
if (error == -1)
err(EX_IOERR, "could not dup %s to stdout", name);
error = dup2(fd, STDERR_FILENO);
if (error == -1)
err(EX_IOERR, "could not dup %s to stderr", name);
/*
* If we created a new session, make the console our controlling
* terminal. Either way, also make this the foreground process group.
*/
if (new_session) {
error = tcsetsid(STDIN_FILENO, sid);
if (error != 0)
err(EX_IOERR, "could not set session for %s", name);
} else {
error = tcsetpgrp(STDIN_FILENO, pgid);
if (error != 0)
err(EX_IOERR, "could not set process group for %s",
name);
}
/*
* Temporarily block signals again; forking, setting grandchild_pid and
* calling err_set_exit need to all be atomic for similar reasons as
* the parent when forking us.
*/
sigfillset(&set);
sigprocmask(SIG_BLOCK, &set, NULL);
pid = fork();
if (pid == -1)
err(EX_OSERR, "could not fork");
if (pid == 0) {
/*
* We need to destroy the ready ends so we don't block these
* child leader-only self-pipes, and might as well destroy the
* wait ends too given we're not going to use them.
*/
pipe_barrier_destroy(&wait_grandchild_barrier);
pipe_barrier_destroy(&wait_all_descendants_barrier);
/* Wait until the parent has put us in a new process group */
pipe_barrier_destroy_ready(&start_grandchild_barrier);
pipe_barrier_wait(&start_grandchild_barrier);
grandchild_run(argv, oset);
}
grandchild_pid = pid;
/*
* Now the grandchild exists make sure to clean it up, and any of its
* descendants, on exit.
*/
err_set_exit(kill_wait_all_descendants_err_exit);
sigprocmask(SIG_SETMASK, oset, NULL);
/* Start the grandchild and wait for it and its descendants to exit */
pipe_barrier_ready(&start_grandchild_barrier);
status = wait_grandchild_descendants();
if (WIFSIGNALED(status))
reproduce_signal_death(WTERMSIG(status));
if (WIFEXITED(status))
exit(WEXITSTATUS(status));
exit(EXIT_FAILURE);
}

View File

@ -0,0 +1,30 @@
/*-
* 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.
*/
void child_leader_run(const char *name, int fd, bool new_session,
const char **argv, const sigset_t *oset,
struct pipe_barrier *start_barrier) __dead2;

View File

@ -0,0 +1,56 @@
/*-
* 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.
*/
#include <sys/param.h>
#include <sys/resource.h>
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <sysexits.h>
#include <unistd.h>
#include "common.h"
void
reproduce_signal_death(int sig)
{
struct rlimit rl;
if (signal(sig, SIG_DFL) == SIG_ERR)
err(EX_OSERR,
"cannot set action to reproduce signal %d",
sig);
rl.rlim_cur = 0;
rl.rlim_max = 0;
if (setrlimit(RLIMIT_CORE, &rl) == -1)
err(EX_OSERR,
"cannot disable core dumps to reproduce signal %d",
sig);
kill(getpid(), sig);
}

View File

@ -0,0 +1,110 @@
/*-
* 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.
*/
#define KILL_TIMEOUT 10
/*
* NB: Most of these do not need to be volatile, but a handful are used in
* signal handler contexts, so for simplicity we make them all volatile rather
* than duplicate the implementation.
*/
struct pipe_barrier {
volatile int fds[2];
};
static __inline int
pipe_barrier_init(struct pipe_barrier *p)
{
int error, fds[2], i;
error = pipe(fds);
if (error != 0)
return (error);
for (i = 0; i < 2; ++i)
p->fds[i] = fds[i];
return (0);
}
static __inline void
pipe_barrier_wait(struct pipe_barrier *p)
{
ssize_t ret;
char temp;
int fd;
fd = p->fds[0];
p->fds[0] = -1;
do {
ret = read(fd, &temp, 1);
} while (ret == -1 && errno == EINTR);
close(fd);
}
static __inline void
pipe_barrier_ready(struct pipe_barrier *p)
{
int fd;
fd = p->fds[1];
p->fds[1] = -1;
close(fd);
}
static __inline void
pipe_barrier_destroy_impl(struct pipe_barrier *p, int i)
{
int fd;
fd = p->fds[i];
if (fd != -1) {
p->fds[i] = -1;
close(fd);
}
}
static __inline void
pipe_barrier_destroy_wait(struct pipe_barrier *p)
{
pipe_barrier_destroy_impl(p, 0);
}
static __inline void
pipe_barrier_destroy_ready(struct pipe_barrier *p)
{
pipe_barrier_destroy_impl(p, 1);
}
static __inline void
pipe_barrier_destroy(struct pipe_barrier *p)
{
pipe_barrier_destroy_wait(p);
pipe_barrier_destroy_ready(p);
}
void reproduce_signal_death(int sig);

View File

@ -0,0 +1,647 @@
/*-
* 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);
}