emerg-shutdown: Add shutdown timeout for preventing stuck shutdowns, briefly document feature set and usage

This commit is contained in:
Aaron Rainbolt 2025-07-29 21:16:51 -05:00
parent e42078e90d
commit 1a60da71ed
No known key found for this signature in database
GPG key ID: A709160D73C79109
8 changed files with 257 additions and 26 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,18 @@
## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## 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

View file

@ -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

View file

@ -0,0 +1,28 @@
#!/bin/bash
# Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
# 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

View file

@ -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 <sys/socket.h>
#include <linux/reboot.h>
#include <unistd.h>
@ -97,6 +90,10 @@
#include <linux/vt.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <errno.h>
#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);
}
}