From a2464ee12761660f50d0b6f59f233949ebcacc87 Mon Sep 17 00:00:00 2001 From: Jessica Clarke Date: Mon, 3 Oct 2022 17:09:16 +0100 Subject: [PATCH] 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 --- tools/build/mk/OptionalObsoleteFiles.inc | 1 + usr.sbin/bsdinstall/Makefile | 2 +- usr.sbin/bsdinstall/runconsoles/Makefile | 9 + usr.sbin/bsdinstall/runconsoles/child.c | 386 +++++++++++ usr.sbin/bsdinstall/runconsoles/child.h | 30 + usr.sbin/bsdinstall/runconsoles/common.c | 56 ++ usr.sbin/bsdinstall/runconsoles/common.h | 110 +++ usr.sbin/bsdinstall/runconsoles/runconsoles.c | 647 ++++++++++++++++++ 8 files changed, 1240 insertions(+), 1 deletion(-) create mode 100644 usr.sbin/bsdinstall/runconsoles/Makefile create mode 100644 usr.sbin/bsdinstall/runconsoles/child.c create mode 100644 usr.sbin/bsdinstall/runconsoles/child.h create mode 100644 usr.sbin/bsdinstall/runconsoles/common.c create mode 100644 usr.sbin/bsdinstall/runconsoles/common.h create mode 100644 usr.sbin/bsdinstall/runconsoles/runconsoles.c diff --git a/tools/build/mk/OptionalObsoleteFiles.inc b/tools/build/mk/OptionalObsoleteFiles.inc index 4145859f21cd..484089d47203 100644 --- a/tools/build/mk/OptionalObsoleteFiles.inc +++ b/tools/build/mk/OptionalObsoleteFiles.inc @@ -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 diff --git a/usr.sbin/bsdinstall/Makefile b/usr.sbin/bsdinstall/Makefile index 761051a5f48f..d09bef110518 100644 --- a/usr.sbin/bsdinstall/Makefile +++ b/usr.sbin/bsdinstall/Makefile @@ -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 diff --git a/usr.sbin/bsdinstall/runconsoles/Makefile b/usr.sbin/bsdinstall/runconsoles/Makefile new file mode 100644 index 000000000000..49666c6b8d80 --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/Makefile @@ -0,0 +1,9 @@ +BINDIR= ${LIBEXECDIR}/bsdinstall +PROG= runconsoles +MAN= + +SRCS= child.c \ + common.c \ + runconsoles.c + +.include diff --git a/usr.sbin/bsdinstall/runconsoles/child.c b/usr.sbin/bsdinstall/runconsoles/child.c new file mode 100644 index 000000000000..45b62e0c8841 --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/child.c @@ -0,0 +1,386 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Jessica Clarke + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +} diff --git a/usr.sbin/bsdinstall/runconsoles/child.h b/usr.sbin/bsdinstall/runconsoles/child.h new file mode 100644 index 000000000000..40d6b9d6cbf1 --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/child.h @@ -0,0 +1,30 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Jessica Clarke + * + * 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; diff --git a/usr.sbin/bsdinstall/runconsoles/common.c b/usr.sbin/bsdinstall/runconsoles/common.c new file mode 100644 index 000000000000..843cee70ba82 --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/common.c @@ -0,0 +1,56 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Jessica Clarke + * + * 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 +#include + +#include +#include +#include +#include +#include + +#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); +} + diff --git a/usr.sbin/bsdinstall/runconsoles/common.h b/usr.sbin/bsdinstall/runconsoles/common.h new file mode 100644 index 000000000000..5c3623f15cba --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/common.h @@ -0,0 +1,110 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Jessica Clarke + * + * 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); diff --git a/usr.sbin/bsdinstall/runconsoles/runconsoles.c b/usr.sbin/bsdinstall/runconsoles/runconsoles.c new file mode 100644 index 000000000000..7051bb5a8ed0 --- /dev/null +++ b/usr.sbin/bsdinstall/runconsoles/runconsoles.c @@ -0,0 +1,647 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Jessica Clarke + * + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +}