From 1a60da71eddfcc6fb72a34596c770cd754146887 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Tue, 29 Jul 2025 21:16:51 -0500 Subject: [PATCH] emerg-shutdown: Add shutdown timeout for preventing stuck shutdowns, briefly document feature set and usage --- README.md | 13 ++ .../emerg-shutdown/30_security_misc.conf | 14 ++ .../system-preset/50-security-misc.preset | 4 + usr/lib/systemd/system/emerg-shutdown.service | 3 +- .../systemd/system/ensure-shutdown.service | 18 ++ usr/libexec/security-misc/emerg-shutdown | 3 +- usr/libexec/security-misc/ensure-shutdown | 28 +++ usr/src/security-misc/emerg-shutdown.c | 200 +++++++++++++++--- 8 files changed, 257 insertions(+), 26 deletions(-) create mode 100644 usr/lib/systemd/system/ensure-shutdown.service create mode 100755 usr/libexec/security-misc/ensure-shutdown diff --git a/README.md b/README.md index cf3ea62..ac12886 100644 --- a/README.md +++ b/README.md @@ -712,6 +712,19 @@ See: * https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=860040 * https://forums.whonix.org/t/cannot-use-pkexec/8129 +## Emergency shutdown + +- Forcibly powers off the system if the drive the system booted from is + removed from the system. +- Forcibly powers off the system if a user-configurable "panic key sequence" + is pressed (Ctrl+Alt+Delete by default). +- Forcibly powers off the system if + `sudo /run/emerg-shutdown --instant-shutdown` is called. +- Optional - Forcibly powers off the system if shutdown gets stuck for longer + than a user-configurable number of seconds (30 by default). Requires tuning + by the user to function properly, see notes in + `/etc/security-misc/emerg-shutdown/30_security_misc.conf`. + ## Application-specific hardening - Enables "`apt-get --error-on=any`" which makes apt exit non-zero for diff --git a/etc/security-misc/emerg-shutdown/30_security_misc.conf b/etc/security-misc/emerg-shutdown/30_security_misc.conf index a4bb394..e844374 100644 --- a/etc/security-misc/emerg-shutdown/30_security_misc.conf +++ b/etc/security-misc/emerg-shutdown/30_security_misc.conf @@ -17,3 +17,17 @@ ## The default key sequence triggers a shutdown when Ctrl+Alt+Delete is ## pressed, allowing the use of either the left or right Ctrl and Alt keys. EMERG_SHUTDOWN_KEYS="KEY_LEFTCTRL|KEY_RIGHTCTRL,KEY_LEFTALT|KEY_RIGHTALT,KEY_DELETE" + +## Set the maximum number of seconds shutdown can take. If shutdown gets stuck +## for longer than this, the system will forcibly power down. +## +## NOTE: This requires ensure-shutdown.service to be enabled, which is not +## done by default. Enabling ensure-shutdown.service will cause shutdown to +## always take at least as long as systemd's DefaultTimeoutStopSec (which by +## default is 90 seconds). If you are going to enable ensure-shutdown.service, +## it is highly recommended to set DefaultTimeoutStopSec to a much smaller +## value, such as 5 seconds. The maximum shutdown time set here should be at +## least 10 seconds *longer* than DefaultTimeoutStopSec, to give normal +## shutdown a chance to actually succeed before forcibly shutting down the +## system. +ENSURE_SHUTDOWN_TIMEOUT=30 diff --git a/usr/lib/systemd/system-preset/50-security-misc.preset b/usr/lib/systemd/system-preset/50-security-misc.preset index 1895526..004563c 100644 --- a/usr/lib/systemd/system-preset/50-security-misc.preset +++ b/usr/lib/systemd/system-preset/50-security-misc.preset @@ -17,3 +17,7 @@ disable proc-hidepid.service ## Disable due to issues. See: ## https://github.com/Kicksecure/security-misc/issues/159 disable harden-module-loading.service + +## Disable due to timing difficulties. See: +## https://github.com/systemd/systemd/issues/38261#issuecomment-3134580852 +disable ensure-shutdown.service diff --git a/usr/lib/systemd/system/emerg-shutdown.service b/usr/lib/systemd/system/emerg-shutdown.service index c1fca25..0eb4258 100644 --- a/usr/lib/systemd/system/emerg-shutdown.service +++ b/usr/lib/systemd/system/emerg-shutdown.service @@ -6,8 +6,9 @@ Description=Emergency shutdown when boot media is removed Documentation=https://github.com/Kicksecure/security-misc [Service] -Type=exec +Type=notify ExecStart=/usr/libexec/security-misc/emerg-shutdown +NotifyAccess=main [Install] WantedBy=multi-user.target diff --git a/usr/lib/systemd/system/ensure-shutdown.service b/usr/lib/systemd/system/ensure-shutdown.service new file mode 100644 index 0000000..30bcc23 --- /dev/null +++ b/usr/lib/systemd/system/ensure-shutdown.service @@ -0,0 +1,18 @@ +## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +[Unit] +Description=Forcibly shut down the system if normal shutdown gets stuck +Documentation=https://github.com/Kicksecure/security-misc +Wants=emerg-shutdown.service +After=emerg-shutdown.service + +[Service] +Type=oneshot +RemainAfterExit=true +ExecStart=/usr/libexec/security-misc/ensure-shutdown +ExecStop=bash -c -- 'echo "d" > /run/emerg-shutdown-trigger' +KillMode=process + +[Install] +WantedBy=multi-user.target diff --git a/usr/libexec/security-misc/emerg-shutdown b/usr/libexec/security-misc/emerg-shutdown index d8dc7f9..81dc9c1 100755 --- a/usr/libexec/security-misc/emerg-shutdown +++ b/usr/libexec/security-misc/emerg-shutdown @@ -39,9 +39,10 @@ if [ ! -f '/run/emerg-shutdown' ]; then printf "%s\n" 'Could not compile force-shutdown executable!' exit 1; } - fi +systemd-notify --ready + ## memlockd daemonizes itself, so no need to background it. memlockd -c /usr/share/security-misc/security-misc-memlockd.cfg || true diff --git a/usr/libexec/security-misc/ensure-shutdown b/usr/libexec/security-misc/ensure-shutdown new file mode 100755 index 0000000..8eb663f --- /dev/null +++ b/usr/libexec/security-misc/ensure-shutdown @@ -0,0 +1,28 @@ +#!/bin/bash + +# Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC +# See the file COPYING for copying conditions. + +set -o errexit +set -o nounset +set -o errtrace +set -o pipefail + +source /usr/libexec/helper-scripts/strings.bsh + +## Make sure globs sort in a predictable, reproducible fashion +export LC_ALL=C + +## Read emergency shutdown key configuration +for config_file in /etc/security-misc/emerg-shutdown/*.conf; do + source "${config_file}" +done +if [ -z "${ENSURE_SHUTDOWN_TIMEOUT}" ] \ + || ! is_whole_number "${ENSURE_SHUTDOWN_TIMEOUT}"; then + ENSURE_SHUTDOWN_TIMEOUT=30; +fi + +/run/emerg-shutdown --monitor-fifo "--timeout=${ENSURE_SHUTDOWN_TIMEOUT}" & +sleep 1 +disown +exit 0 diff --git a/usr/src/security-misc/emerg-shutdown.c b/usr/src/security-misc/emerg-shutdown.c index 5a01e17..83cb6de 100644 --- a/usr/src/security-misc/emerg-shutdown.c +++ b/usr/src/security-misc/emerg-shutdown.c @@ -74,13 +74,6 @@ * (there are other similar posts as well). */ -/* - * TODO: Consider handling signals more gracefully (perhaps use ppoll instead - * of poll, handle things like EINTR, etc.). Right now the plan is to simply - * terminate when a signal is received and let systemd restart the process, - * but it might be better to just be signal-resilient. - */ - #include #include #include @@ -97,6 +90,10 @@ #include #include #include +#include +#include +#include +#include #define fd_stdin 0 #define fd_stdout 1 @@ -106,6 +103,11 @@ #define input_path_size 20 #define key_flags_len 12 +#define hw_monitor_val 1 +#define fifo_monitor_val 2 + +#define max_sig_num 31 + int console_fd = 0; /* Adapted from kloak/src/keycodes.c */ @@ -289,6 +291,8 @@ void print_usage() { print(fd_stderr, " emerg-shutdown --devices=DEVICE1[,DEVICE2...] --keys=KEY_1[,KEY_2|KEY_3...]\n"); print(fd_stderr, "Or:\n"); print(fd_stderr, " emerg-shutdown --instant-shutdown\n"); + print(fd_stderr, "Or:\n"); + print(fd_stderr, " emerg-shutdown --monitor-fifo --timeout=TIMEOUT\n"); print(fd_stderr, "Example:\n"); print(fd_stderr, " emerg-shutdown --devices=/dev/sda3 --keys=KEY_POWER\n"); } @@ -439,7 +443,8 @@ trykill: LINUX_REBOOT_CMD_POWER_OFF, NULL); } -int main(int argc, char **argv) { +/* Monitor for device removal and emergency shutdown key combos. */ +void hw_monitor(int argc, char **argv) { /* Working variables */ size_t target_dev_list_len = 0; char **target_dev_name_raw_list = NULL; @@ -464,23 +469,7 @@ int main(int argc, char **argv) { int ie_idx = 0; size_t kg_idx = 0; - /* Prerequisite check */ - if (getuid() != 0) { - print(fd_stderr, "This program must be run as root!\n"); - exit(1); - } - - /* Argument parsing */ - if (argc < 2) { - print(fd_stderr, "Invalid number of arguments!\n"); - print_usage(); - exit(1); - } - for (arg_idx = 1; arg_idx < argc; arg_idx++) { - if (strncmp(argv[arg_idx], "--instant-shutdown", strlen("--instant-shutdown")) == 0) { - kill_system(); - } if (strncmp(argv[arg_idx], "--devices=", strlen("--devices=")) == 0) { if (target_dev_name_raw_list != NULL) { print(fd_stderr, "--devices cannot be passed more than once!\n"); @@ -495,6 +484,12 @@ int main(int argc, char **argv) { exit(1); } load_list(argv[arg_idx], &panic_key_list_len, &panic_key_str_list, ",", true); + } else { + print(fd_stderr, "Unrecognized argument '"); + print(fd_stderr, argv[arg_idx]); + print(fd_stderr, "' passed!\n"); + print_usage(); + exit(1); } } @@ -851,4 +846,161 @@ next_str: } } } + + print(fd_stderr, "Hardware monitor poll gave up!\n"); + exit(1); +} + +/* + * Monitor for a kill command on a fifo. Two commands are recognized: + * + * - 'k': Instantly kill the system. + * - 'd': Wait 15 seconds, then kill the system. This is used to keep systemd + * from delaying shutdown excessively. + */ +void fifo_monitor(int argc, char **argv) { + long monitor_fifo_timeout = 0; + char *arg_copy = NULL; + char *arg_part = NULL; + char *arg_num_end = NULL; + const char *trigger_fifo_path = "/run/emerg-shutdown-trigger"; + int trigger_fifo_fd = 0; + struct pollfd trigger_fifo_poll = { 0 }; + char trigger_fifo_charbuf = '\0'; + ssize_t trigger_fifo_readlen = 0; + int sig_idx = 0; + struct sigaction sigact_swallow = { 0 }; + + if (strncmp(argv[2], "--timeout=", strlen("--timeout=")) != 0) { + print(fd_stderr, "Timeout not passed for --monitor-fifo!\n"); + print_usage(); + exit(1); + } + + arg_copy = safe_calloc(1, strlen(argv[2]) + 1); + memcpy(arg_copy, argv[2], strlen(argv[2]) + 1); + /* returns "--timeout" */ + arg_part = strtok(arg_copy, "="); + /* returns everything after the = sign */ + arg_part = strtok(NULL, ""); + monitor_fifo_timeout = strtol(arg_part, &arg_num_end, 10); + if (errno == ERANGE) { + print(fd_stderr, "Timeout out of range!\n"); + print_usage(); + exit(1); + } + if (*arg_num_end != '\0') { + print(fd_stderr, "Timeout is not purely numeric!\n"); + print_usage(); + exit(1); + } + if (monitor_fifo_timeout < 1) { + print(fd_stderr, "Timeout is less than one!\n"); + print_usage(); + exit(1); + } + + free(arg_copy); + arg_copy = NULL; + arg_part = NULL; + arg_num_end = NULL; + + if (mkfifo(trigger_fifo_path, 0777) == -1) { + print(fd_stderr, "Cannot create trigger fifo!\n"); + exit(1); + } + + trigger_fifo_fd = open(trigger_fifo_path, O_RDONLY | O_NONBLOCK); + if (trigger_fifo_fd == -1) { + print(fd_stderr, "Cannot open trigger fifo for reading!\n"); + exit(1); + } + + trigger_fifo_poll.fd = trigger_fifo_fd; + trigger_fifo_poll.events = POLLIN; + + /* Swallow all signals that we can. */ + sigact_swallow.sa_handler = SIG_IGN; + for (sig_idx = 1; sig_idx < max_sig_num; sig_idx++) { + if (sig_idx == SIGSTOP) { + continue; + } + if (sig_idx == SIGKILL) { + continue; + } + if (sigaction(sig_idx, &sigact_swallow, NULL) == -1) { + print(fd_stderr, "Failed to set up signal ignores!\n"); + exit(1); + } + } + for (sig_idx = SIGRTMIN; sig_idx <= SIGRTMAX; sig_idx++) { + if (sigaction(sig_idx, &sigact_swallow, NULL) == -1) { + print(fd_stderr, "Failed to set up real-time signal ignores!\n"); + exit(1); + } + } + + while (poll(&trigger_fifo_poll, 1, -1) != -1) { + trigger_fifo_readlen = read(trigger_fifo_fd, &trigger_fifo_charbuf, 1); + if (trigger_fifo_readlen != 1) { + print(fd_stderr, "Error reading from trigger fifo!\n"); + exit(1); + } + if (trigger_fifo_charbuf == 'k') { + kill_system(); + } else if (trigger_fifo_charbuf == 'd') { + sleep(monitor_fifo_timeout); + kill_system(); + } + } + + print(fd_stderr, "Trigger fifo poll gave up!\n"); + exit(1); +} + +int main(int argc, char **argv) { + int monitor_mode = hw_monitor_val; + + /* Prerequisite check */ + if (getuid() != 0) { + print(fd_stderr, "This program must be run as root!\n"); + exit(1); + } + + if (argc < 2) { + print(fd_stderr, "Not enough arguments!\n"); + print_usage(); + exit(1); + } + + if (strcmp(argv[1], "--instant-shutdown") == 0) { + if (argc != 2) { + print(fd_stderr, "Too many arguments, --instant-shutdown must be passed alone!\n"); + print_usage(); + exit(1); + } + + kill_system(); + } + if (strcmp(argv[1], "--monitor-fifo") == 0) { + if (argc != 3) { + print(fd_stderr, "Wrong number of arguments for --monitor-fifo!\n"); + print_usage(); + exit(1); + } + + monitor_mode = fifo_monitor_val; + } + + if (monitor_mode == hw_monitor_val) { + /* hw_monitor handles its own argument parsing */ + hw_monitor(argc, argv); + } else if (monitor_mode == fifo_monitor_val) { + /* fifo_monitor handles its own argument parsing */ + fifo_monitor(argc, argv); + } else { + print(fd_stderr, "Unknown monitor mode chosen!\n"); + print_usage(); + exit(1); + } }