git: a2464ee12761 - main - bsdinstall: Add a new runconsoles helper binary

From: Jessica Clarke <jrtc27_at_FreeBSD.org>
Date: Mon, 03 Oct 2022 16:09:41 UTC
The branch main has been updated by jrtc27:

URL: https://cgit.FreeBSD.org/src/commit/?id=a2464ee12761660f50d0b6f59f233949ebcacc87

commit a2464ee12761660f50d0b6f59f233949ebcacc87
Author:     Jessica Clarke <jrtc27@FreeBSD.org>
AuthorDate: 2022-10-03 16:09:16 +0000
Commit:     Jessica Clarke <jrtc27@FreeBSD.org>
CommitDate: 2022-10-03 16:09:16 +0000

    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(-)

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 <bsd.prog.mk>
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 <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);
+}
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 <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;
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 <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);
+}
+
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 <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);
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 <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;
+
*** 343 LINES SKIPPED ***