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.

(cherry picked from commit ef3e0ff7bb)
This commit is contained in:
Birk Skyum
2026-10-16 11:36:31 +01:00
committed by Luca Boccassi
parent 1946818513
commit bfba34d783
7 changed files with 117 additions and 7 deletions
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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++)
+10 -2
View File
@@ -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."),
+5
View File
@@ -292,6 +292,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',
+97
View File
@@ -0,0 +1,97 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <linux/kd.h>
#include <linux/vt.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <unistd.h>
#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);