mirror of
https://github.com/Kicksecure/security-misc.git
synced 2025-08-07 02:24:24 -04:00
Add emergency shutdown feature, triggered by root device removal
This commit is contained in:
parent
341dce33fb
commit
f3d46ee562
5 changed files with 337 additions and 0 deletions
2
debian/control
vendored
2
debian/control
vendored
|
@ -20,6 +20,7 @@ Package: security-misc
|
|||
Architecture: all
|
||||
Depends: adduser,
|
||||
apparmor-profile-dist,
|
||||
build-essential,
|
||||
dmsetup,
|
||||
helper-scripts,
|
||||
libcap2-bin,
|
||||
|
@ -27,6 +28,7 @@ Depends: adduser,
|
|||
libpam-modules-bin,
|
||||
libpam-runtime,
|
||||
libpam-umask,
|
||||
memlockd,
|
||||
python3,
|
||||
secure-delete,
|
||||
sudo,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
|
||||
## See the file COPYING for copying conditions.
|
||||
|
||||
[Unit]
|
||||
Description=Emergency shutdown when boot media is removed
|
||||
Documentation=https://github.com/Kicksecure/security-misc
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/libexec/security-misc/force-poweroff-on-boot-media-removal
|
||||
RemainAfterExit=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
24
usr/libexec/security-misc/force-poweroff-on-boot-media-removal
Executable file
24
usr/libexec/security-misc/force-poweroff-on-boot-media-removal
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
|
||||
# See the file COPYING for copying conditions.
|
||||
|
||||
gcc \
|
||||
-o \
|
||||
/run/force-shutdown-when-device-removed \
|
||||
-static \
|
||||
/usr/src/security-misc/force-shutdown-when-device-removed.c \
|
||||
|| {
|
||||
printf "%s\n" 'Could not compile force-shutdown executable!'
|
||||
exit 1;
|
||||
}
|
||||
|
||||
readarray -t root_devices < <(/usr/libexec/helper-scripts/get-backing-devices-for-mountpoint '/');
|
||||
|
||||
## memlockd daemonizes itself, so no need to background it
|
||||
memlockd -c /usr/share/security-misc/security-misc-memlockd.cfg
|
||||
|
||||
/run/force-shutdown-when-device-removed "${root_devices}" &
|
||||
sleep 1
|
||||
disown
|
||||
exit 0
|
2
usr/share/security-misc/security-misc-memlockd.cfg
Normal file
2
usr/share/security-misc/security-misc-memlockd.cfg
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Lock systemd and all of its library dependencies into memory
|
||||
+/usr/bin/systemd
|
295
usr/src/security-misc/force-shutdown-when-device-removed.c
Normal file
295
usr/src/security-misc/force-shutdown-when-device-removed.c
Normal file
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
|
||||
* See the file COPYING for copying conditions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This program is designed specifically to immediately and forcibly power off
|
||||
* the system in the event the device providing the root filesystem is
|
||||
* abruptly removed from the system. The idea is that a user can shut down
|
||||
* a portable installation of Kicksecure by simply yanking the USB drive
|
||||
* containing the installation from the computer. Tails provides essentially
|
||||
* the same feature, however it is known for occasionally failing to do its
|
||||
* job properly.
|
||||
*
|
||||
* The fact that we're triggering a shutdown when the device containing the
|
||||
* root filesystem vanishes presents a number of significant challenges:
|
||||
*
|
||||
* - The device providing the entire operating system is gone. The only things
|
||||
* we will still have left are the kernel, files loaded into RAM (for
|
||||
* instance under /run), and anything that happens to still be in the
|
||||
* system's disk cache.
|
||||
* - Virtually any process on the system may abruptly crash at any time. This
|
||||
* isn't just because applications may be unable to access files. The Linux
|
||||
* kernel's virtual memory subsystem doesn't just page out RAM contents to a
|
||||
* swap file, it will sometimes simply erase pages containing executable
|
||||
* code from memory if it can reload that code from disk later when needed.
|
||||
* If part of a program isn't present in memory, and then the root device
|
||||
* vanishes, any attempt to use code in the absent part of the application
|
||||
* will result in the application crashing. (Attempts to access data in RAM
|
||||
* that happened to be paged out will result in a similar crash.)
|
||||
* - We have no control over what is and isn't in the disk cache, which makes
|
||||
* it unsafe to launch any dynamically linked executable. What happens if we
|
||||
* need to load a missing part of libc? What if the dynamic linker itself
|
||||
* needs loaded from disk?
|
||||
* - Systemd could lock up at any time, since the init process isn't immune to
|
||||
* having bits of it erased from RAM to free up memory. If systemd receives
|
||||
* a SIGSEGV, rather than crashing (which would panic the kernel), it goes
|
||||
* into an "emergency mode" that tries to keep the system as operational as
|
||||
* possible even though PID 1 is now out of service.
|
||||
*
|
||||
* Circumventing this set of difficulties is not easy, and it might not even
|
||||
* be entirely possible. To give our feature the highest chance of success:
|
||||
*
|
||||
* - We use memlockd to lock systemd and all libraries it depends on into
|
||||
* memory. It can holds its own pretty well in the event of a segfault, but
|
||||
* if its crash handler ends up re-segfaulting, that could get ugly.
|
||||
* - We compile the utility at boot time, statically link it against all of
|
||||
* its dependencies (really only one, glibc), and load it into /run. This
|
||||
* allows for decent architecture independence while removing any dependency
|
||||
* on anything that isn't in RAM, thus (hopefully!) making the process
|
||||
* crash-immune.
|
||||
* - Because we're static-linking against glibc, we cannot call anything
|
||||
* defined in stdio.h. This is because glibc uses dlopen() to load iconv
|
||||
* modules, which are used internally by glibc for locale support. Things
|
||||
* defined in stdio.h may use iconv, so calling anything there will
|
||||
* basically make our static-linked executable become dynamically linked,
|
||||
* which could segfault it since the root filesystem is gone. We can't call
|
||||
* anything that could touch Name Service Switch (NSS) either, but we have
|
||||
* no need to do so, so we should be safe there. See
|
||||
* https://stackoverflow.com/questions/57476533/why-is-statically-linking-glibc-discouraged
|
||||
* - We can't use udev either because libudev is only available as a dynamic
|
||||
* library. That means we have to listen to kernel uevents directly to
|
||||
* determine when the root device vanishes. Thankfully this isn't as much of
|
||||
* a pain as it might sound like.
|
||||
* - We don't call out to any external process, since those external processes
|
||||
* could segfault.
|
||||
*
|
||||
* This is likely superior to Tails' implementation, which uses udev (and thus
|
||||
* dynamic linking), uses an interpreter-driven script to shut down the system
|
||||
* when the root device vanishes, and calls out to external executables to
|
||||
* actually shut the system down. These issues are likely why Tails'
|
||||
* implementation of emergency shutdown occasionally fails. See
|
||||
* https://www.reddit.com/r/tails/comments/xh8njn/tails_wont_shutdown_when_i_pull_usb_stick/
|
||||
* (there are other similar posts as well).
|
||||
*/
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/reboot.h>
|
||||
#include <unistd.h>
|
||||
#include <asm/types.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define fd_stdin 0
|
||||
#define fd_stdout 1
|
||||
#define fd_stderr 2
|
||||
|
||||
void print(int fd, char *str) {
|
||||
size_t len = strlen(str) + 1;
|
||||
while (true) {
|
||||
ssize_t write_len = write(fd, str, len);
|
||||
len -= write_len;
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
str += write_len;
|
||||
}
|
||||
}
|
||||
|
||||
void print_usage() {
|
||||
print(fd_stderr, "Usage:\n");
|
||||
print(fd_stderr, " force-shutdown-when-device-removed DEVICE1 [DEVICE2...]\n");
|
||||
print(fd_stderr, "Example:\n");
|
||||
print(fd_stderr, " force-shutdown-when-device-removed /dev/sda3\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (getuid() != 0) {
|
||||
print(fd_stderr, "This program must be run as root!\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (argc < 2) {
|
||||
print(fd_stderr, "Invalid number of arguments!\n");
|
||||
print_usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
size_t target_dev_name_list_len = argc - 1;
|
||||
char **target_dev_name_list = calloc(target_dev_name_list_len,
|
||||
sizeof(char *));
|
||||
if (target_dev_name_list == NULL) {
|
||||
print(fd_stderr, "Out of memory during early setup!\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
char *target_dev_path = argv[i];
|
||||
if (access(target_dev_path, F_OK) != 0) {
|
||||
print(fd_stderr, "One of the specified devices does not exist!\n");
|
||||
print_usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (strncmp(target_dev_path, "/dev/sr", strlen("/dev/sr")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/nvme", strlen("/dev/nvme")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/sd", strlen("/dev/sd")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/mmc", strlen("/dev/mmc")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/vd", strlen("/dev/vd")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/xvd", strlen("/dev/xvd")) != 0
|
||||
&& strncmp(target_dev_path, "/dev/hd", strlen("/dev/hd")) != 0) {
|
||||
print(fd_stderr, "One of the specified devices is not supported!\n");
|
||||
print_usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
size_t device_path_slash_count = 0;
|
||||
for (size_t j = 0; j < strlen(target_dev_path); j++) {
|
||||
if (target_dev_path[j] == '/') {
|
||||
device_path_slash_count++;
|
||||
}
|
||||
}
|
||||
if (device_path_slash_count != 2) {
|
||||
print(fd_stderr, "One of the specified devices is not supported!\n");
|
||||
print_usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
char *target_dev_parse = calloc(1, strlen(target_dev_path) + 1);
|
||||
if (target_dev_parse == NULL) {
|
||||
print(fd_stderr, "Out of memory during early setup!\n");
|
||||
exit(1);
|
||||
}
|
||||
memcpy(target_dev_parse, target_dev_path, strlen(target_dev_path) + 1);
|
||||
|
||||
/* returns "dev" */
|
||||
char *target_dev_name = strtok(target_dev_parse, "/");
|
||||
/* returns the actual device name we want */
|
||||
target_dev_name = strtok(NULL, "/");
|
||||
if (target_dev_name == NULL) {
|
||||
print(fd_stderr, "One of the specified devices is not supported!\n");
|
||||
print_usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
target_dev_name_list[i - 1] = calloc(1, strlen(target_dev_name) + 1);
|
||||
memcpy(target_dev_name_list[i - 1], target_dev_name,
|
||||
strlen(target_dev_name) + 1);
|
||||
free(target_dev_parse);
|
||||
}
|
||||
|
||||
struct sockaddr_nl sa = {
|
||||
.nl_family = AF_NETLINK,
|
||||
.nl_pad = 0,
|
||||
.nl_pid = getpid(),
|
||||
.nl_groups = NETLINK_KOBJECT_UEVENT,
|
||||
};
|
||||
int ns = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
|
||||
if (ns < 0) {
|
||||
print(fd_stderr, "Failed to create netlink socket!\n");
|
||||
exit(1);
|
||||
}
|
||||
int ret = bind(ns, (struct sockaddr *) &sa, sizeof(sa));
|
||||
if (ret < 0) {
|
||||
print(fd_stderr, "Failed to bind netlink socket!\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
/*
|
||||
* So, you looked at `man 7 netlink`, then looked at this code, and can't
|
||||
* figure out how on earth any of this makes sense? Well guess what, turns
|
||||
* out NETLINK_KOBJECT_UEVENT messages break all of the rules about how
|
||||
* netlink messages work specified in that manpage. What you actually
|
||||
* get... well, depends.
|
||||
*
|
||||
* - The messages we actually want are just NUL-separated string lists.
|
||||
* These are the actual kernel uevents.
|
||||
* - Mixed in with those will be uevents generated by systemd-udevd, which
|
||||
* use a different format and are unsuitable for our purposes. We have
|
||||
* to ignore those. Thankfully those messages start with the
|
||||
* NUL-terminated string "libudev" so they're easy to filter out.
|
||||
*/
|
||||
|
||||
int len;
|
||||
char buf[16384];
|
||||
struct iovec iov = { buf, sizeof(buf) };
|
||||
struct sockaddr_nl sa2;
|
||||
struct msghdr msg = { &sa2, sizeof(sa2), &iov, 1, NULL, 0, 0 };
|
||||
len = recvmsg(ns, &msg, 0);
|
||||
if (len == -1) {
|
||||
reboot(RB_POWER_OFF);
|
||||
//print(fd_stderr, "SHUTDOWN!!!\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (len < 8) {
|
||||
/* There aren't any super-short messages we're interested in, discard
|
||||
* them */
|
||||
continue;
|
||||
}
|
||||
if (memcmp(buf, "libudev", 8) == 0) {
|
||||
/* udevd message, ignore */
|
||||
continue;
|
||||
}
|
||||
|
||||
char *tmpbuf = buf;
|
||||
bool device_removed = false;
|
||||
while (len > 0) {
|
||||
if (strcmp(tmpbuf, "ACTION=remove") == 0) {
|
||||
device_removed = true;
|
||||
goto next_str;
|
||||
}
|
||||
|
||||
if (strncmp(tmpbuf, "DEVNAME=", strlen("DEVNAME=")) == 0) {
|
||||
if (device_removed) {
|
||||
char *rem_devname_line;
|
||||
/*
|
||||
* Try to allocate the memory needed to check DEVNAME in a loop. We
|
||||
* really do not want to simply abort here due to an out of memory
|
||||
* condition, because that would result in the shutdown never
|
||||
* occurring. We also don't want to force a shutdown when memory
|
||||
* runs out, as that could result in the user losing work because
|
||||
* they opened too many browser tabs.
|
||||
*/
|
||||
while(true) {
|
||||
rem_devname_line = calloc(1, strlen(tmpbuf) + 1);
|
||||
if (rem_devname_line == NULL) {
|
||||
print(fd_stderr, "Out of memory while parsing devname, retrying in one second\n");
|
||||
sleep(1);
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(rem_devname_line, tmpbuf, strlen(tmpbuf) + 1);
|
||||
/* returns DEVNAME */
|
||||
char *rem_dev_name = strtok(rem_devname_line, "=");
|
||||
/* returns the actual device name */
|
||||
rem_dev_name = strtok(NULL, "=");
|
||||
if (rem_dev_name == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < target_dev_name_list_len; i++) {
|
||||
if (strcmp(rem_dev_name, target_dev_name_list[i]) == 0) {
|
||||
reboot(RB_POWER_OFF);
|
||||
//print(fd_stderr, "SHUTDOWN!!!\n");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
free(rem_devname_line);
|
||||
}
|
||||
}
|
||||
|
||||
next_str:
|
||||
len -= strlen(tmpbuf) + 1;
|
||||
tmpbuf += strlen(tmpbuf) + 1;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue