From ef3e0ff7bb802e86f013551883184da4524e7fac Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 6 Sep 2026 22:09:37 +0200 Subject: [PATCH] shutdown: preserve console graphics mode across the exitrd handoff make_console_stdio() forces KD_TEXT while reopening /dev/console. When systemd-shutdown enters the exitrd, this can expose the text console over a retained graphical shutdown splash. Let callers choose whether to switch to text mode, and preserve the mode for the exitrd handoff. Keep the existing text-mode behavior for manager reexecution and the crash shell. Restore text mode if executing /shutdown fails and before reporting a fatal error and freezing. Add an unsafe, nonparallel virtual-console regression test requiring explicit opt-in and an already-owned inactive VT in a disposable VM. Check graphics preservation and text restoration through a private /dev/console bind mount, retain stderr diagnostics, and restore the saved mode in the parent even if the child fails. --- src/basic/terminal-util.c | 4 +- src/basic/terminal-util.h | 2 +- src/core/crash-handler.c | 2 +- src/core/main.c | 2 +- src/shutdown/shutdown.c | 12 ++++- src/test/meson.build | 5 ++ src/test/test-console-stdio.c | 97 +++++++++++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 src/test/test-console-stdio.c diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 597880d0ff..6980b239d3 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1046,7 +1046,7 @@ int lock_dev_console(void) { return TAKE_FD(fd); } -int make_console_stdio(void) { +int make_console_stdio(bool switch_to_text) { int fd, r; /* Make /dev/console the controlling terminal and stdin/stdout/stderr, if we can. If we can't use @@ -1062,7 +1062,7 @@ int make_console_stdio(void) { return log_error_errno(r, "Failed to make /dev/null stdin/stdout/stderr: %m"); } else { - reset_dev_console_fd(fd, /* switch_to_text= */ true); + reset_dev_console_fd(fd, switch_to_text); r = rearrange_stdio(fd, fd, fd); /* This invalidates 'fd' both on success and on failure. */ if (r < 0) diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 0e65d205a5..92612bf78a 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -112,7 +112,7 @@ int vtnr_from_tty(const char *tty); void reset_dev_console_fd(int fd, bool switch_to_text); int lock_dev_console(void); -int make_console_stdio(void); +int make_console_stdio(bool switch_to_text); int getenv_columns(void); int fd_columns(int fd); diff --git a/src/core/crash-handler.c b/src/core/crash-handler.c index b5286c8e69..39e0249d9d 100644 --- a/src/core/crash-handler.c +++ b/src/core/crash-handler.c @@ -175,7 +175,7 @@ _noreturn_ static void crash(int sig, siginfo_t *siginfo, void *context) { else if (pid == 0) { (void) setsid(); (void) terminal_vhangup("/dev/console"); - (void) make_console_stdio(); + (void) make_console_stdio(/* switch_to_text= */ true); (void) rlimit_nofile_safe(); (void) execle("/bin/sh", "/bin/sh", NULL, environ); diff --git a/src/core/main.c b/src/core/main.c index 61726a022c..7f44cacccb 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2331,7 +2331,7 @@ static int do_reexecute( log_warning_errno(r, "Failed to move /run/systemd/ to /run/systemd.pre-switch-root/, ignoring: %m"); /* Reopen the console */ - (void) make_console_stdio(); + (void) make_console_stdio(/* switch_to_text= */ true); i = 1; /* Leave args[0] empty for now. */ for (int j = 1; j <= argc; j++) diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index 18b7276758..720d06434f 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -644,13 +644,18 @@ static int run(int argc, char *argv[]) { argv[0] = (char*) "/shutdown"; (void) setsid(); - (void) make_console_stdio(); + /* Preserve a graphical splash retained across the exitrd handoff. */ + (void) make_console_stdio(/* switch_to_text= */ false); log_info("Successfully changed into root pivot.\n" "Entering exitrd..."); execv("/shutdown", argv); - log_error_errno(errno, "Failed to execute shutdown binary: %m"); + r = -errno; + + /* The exitrd could not take over. Make subsequent diagnostics visible again. */ + (void) make_console_stdio(/* switch_to_text= */ true); + log_error_errno(r, "Failed to execute shutdown binary: %m"); } else log_error_errno(r, "Failed to switch root to \"/run/initramfs\": %m"); } @@ -736,6 +741,9 @@ static int run(int argc, char *argv[]) { r = log_error_errno(errno, "Failed to invoke reboot(): %m"); error: + /* Do not leave a graphical splash covering the fatal error before freezing. */ + (void) make_console_stdio(/* switch_to_text= */ true); + if (r == 0) log_struct(LOG_EMERG, LOG_MESSAGE("Non-shutdown operation requested, cannot operate."), diff --git a/src/test/meson.build b/src/test/meson.build index 2a305ccddc..f58243b876 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -293,6 +293,11 @@ executables += [ libselinux_cflags, ], }, + test_template + { + 'sources' : files('test-console-stdio.c'), + 'type' : 'unsafe', + 'parallel' : false, + }, test_template + { 'sources' : files('test-coredump-stacktrace.c'), 'type' : 'manual', diff --git a/src/test/test-console-stdio.c b/src/test/test-console-stdio.c new file mode 100644 index 0000000000..457f224e89 --- /dev/null +++ b/src/test/test-console-stdio.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include + +#include "alloc-util.h" +#include "capability-util.h" +#include "env-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "path-util.h" +#include "pidref.h" +#include "process-util.h" +#include "terminal-util.h" +#include "tests.h" + +/* Run only on a dedicated inactive VT in a disposable VM, for example with openvt --wait. + * The caller must already own that VT. The test neither allocates nor disallocates a VT, + * and the mount namespace isolates only the /dev/console path, not the VT itself. */ +TEST_RET(make_console_stdio) { + _cleanup_(pidref_done) PidRef child = PIDREF_NULL; + _cleanup_free_ char *tty = NULL, *path = NULL; + _cleanup_close_ int tty_fd = -EBADF, log_fd = -EBADF; + struct vt_stat state; + int saved_mode, mode, q, r; + + if (secure_getenv_bool("SYSTEMD_TEST_CONSOLE") <= 0) + return log_tests_skipped("Set SYSTEMD_TEST_CONSOLE=1 on a dedicated inactive VT."); + if (geteuid() != 0 || + have_effective_cap(CAP_SYS_ADMIN) <= 0 || + have_effective_cap(CAP_SYS_TTY_CONFIG) <= 0) + return log_tests_skipped("Requires CAP_SYS_ADMIN and CAP_SYS_TTY_CONFIG."); + if (access("/dev/console", F_OK) < 0) + return log_tests_skipped_errno(errno, "No console mount point"); + + if (get_ctty(0, /* ret_devnr= */ NULL, &tty) < 0 || !tty_is_vc(tty)) + return log_tests_skipped("Not running on a virtual console."); + path = ASSERT_NOT_NULL(path_join("/dev", tty)); + tty_fd = open_terminal(path, O_RDWR|O_NOCTTY|O_CLOEXEC); + if (tty_fd < 0) + return log_tests_skipped_errno(tty_fd, "Cannot open the controlling VT"); + if (ioctl(tty_fd, VT_GETSTATE, &state) < 0 || state.v_active == vtnr_from_tty(tty)) + return log_tests_skipped("Requires an inactive virtual console."); + if (ioctl(tty_fd, KDGETMODE, &saved_mode) < 0) + return log_tests_skipped_errno(errno, "Cannot read the console mode"); + log_fd = ASSERT_OK_ERRNO(fcntl(STDERR_FILENO, F_DUPFD_CLOEXEC, 3)); + + /* Keep the parent in the owning session so a child failure cannot hang up the VT. */ + r = pidref_safe_fork( + "test-console-stdio", + FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, + &child); + if (r < 0 && ERRNO_IS_PRIVILEGE(r)) + return log_tests_skipped("Cannot create a private mount namespace."); + ASSERT_OK(r); + if (r == 0) { + r = mount(path, "/dev/console", /* fstype= */ NULL, MS_BIND, /* data= */ NULL); + if (r < 0 && ERRNO_IS_PRIVILEGE(errno)) + _exit(EXIT_TEST_SKIP); + ASSERT_OK_ERRNO(r); + r = ioctl(tty_fd, KDSETMODE, KD_GRAPHICS); + if (r < 0 && ERRNO_IS_PRIVILEGE(errno)) + _exit(EXIT_TEST_SKIP); + ASSERT_OK_ERRNO(r); + + r = make_console_stdio(/* switch_to_text= */ false); + ASSERT_OK_ERRNO(dup2(log_fd, STDERR_FILENO)); + ASSERT_OK(r); + ASSERT_OK_ERRNO(ioctl(STDIN_FILENO, KDGETMODE, &mode)); + ASSERT_EQ(mode, KD_GRAPHICS); + + r = make_console_stdio(/* switch_to_text= */ true); + ASSERT_OK_ERRNO(dup2(log_fd, STDERR_FILENO)); + ASSERT_OK(r); + ASSERT_OK_ERRNO(ioctl(STDIN_FILENO, KDGETMODE, &mode)); + ASSERT_EQ(mode, KD_TEXT); + _exit(EXIT_SUCCESS); + } + + r = pidref_wait_for_terminate_and_check("test-console-stdio", &child, WAIT_LOG); + + /* Restore even after a failed assertion in the child, before asserting in the parent. */ + q = RET_NERRNO(ioctl(tty_fd, KDGETMODE, &mode)); + if (q < 0 || mode != saved_mode) + q = RET_NERRNO(ioctl(tty_fd, KDSETMODE, saved_mode)); + ASSERT_OK(q); + if (r == EXIT_TEST_SKIP) + return log_tests_skipped("Console setup is not permitted."); + ASSERT_OK_ZERO(r); + return EXIT_SUCCESS; +} + +DEFINE_TEST_MAIN(LOG_INFO);