diff --git a/CMakeLists.txt b/CMakeLists.txt index 682a191..5181a7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,5 +99,5 @@ if(BUILD_TESTING) add_test(NAME tuxreaperd-zombie-tests COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_tuxreaperd.sh ${CMAKE_CURRENT_BINARY_DIR}/tuxreaperd - ${CMAKE_CURRENT_SOURCE_DIR}/tuxreaperd.c) + ${CMAKE_CURRENT_SOURCE_DIR}/tuxreaperdgnu.c) endif() diff --git a/DEVLOG.md b/DEVLOG.md index 636bffb..2e95b36 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -6,10 +6,12 @@ This release introduces `tuxreaperd`, a micro init system for containers. ### Micro init -- Added `tuxreaperd.c`, a small C daemon used as PID 1 inside containers created by tux-dock. -- Runs as a subreaper (`PR_SET_CHILD_SUBREAPER`) with a SIGCHLD handler that reaps every reparented/terminated descendant, so orphaned processes never accumulate as zombies. -- Forwards SIGTERM/SIGINT/SIGHUP to the primary workload and propagates its exit status (128 + signal for signal deaths). -- `compile.sh` statically builds it (`gcc -O2 -static -Wall -Wextra tuxreaperd.c -o build/tuxreaperd`) alongside the rest of the tree. +- Split the reaper into two implementations: + - `tuxreaperdgnu.c`: the original standard POSIX/libc daemon used as PID 1 inside containers created by tux-dock. + - `tuxreaperdasm.c`: a new freestanding `-nostdlib` syscall engine for `x86_64` and `aarch64` with a tiny static footprint. +- Both run as subreapers (`PR_SET_CHILD_SUBREAPER`) with a SIGCHLD handler that reaps every reparented/terminated descendant, so orphaned processes never accumulate as zombies. +- Both forward SIGTERM/SIGINT/SIGHUP to the primary workload and propagate its exit status (128 + signal for signal deaths). +- `compile.sh` detects the host architecture and builds the freestanding implementation when possible, falling back to `tuxreaperdgnu.c` on failure or unsupported hosts. - `DockerManager::createContainer` bind-mounts `tuxreaperd` into the container as PID 1 and resolves the binary next to the `tux-dock` executable when it is not in the current directory. ### Tests diff --git a/README.md b/README.md index 7f3cac4..752202b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ sudo ./build/tux-dock ./compile.sh --web-test-view 9000 ``` -`compile.sh` also statically builds the `tuxreaperd` micro init (`gcc -O2 -static -Wall -Wextra tuxreaperd.c -o build/tuxreaperd`) so containers can be created with it as PID 1. +`compile.sh` builds the `tuxreaperd` micro init before the main project. On supported hosts (`x86_64`/`amd64`/`aarch64`/`arm64`) it first tries the freestanding syscall implementation in `tuxreaperdasm.c`; on other architectures, or if the freestanding build fails, it falls back to the standard POSIX implementation in `tuxreaperdgnu.c`. The web report is generated under `/tmp` and includes exact CTest output, test summaries, Docker integration output, a text rendition of the TUI flow, and the tuxreaperd zombie-reaping results. It requires `nc`, `netcat`, or Nmap's `ncat`; press `Ctrl-C` to stop the server. The default port is `8095`; pass a port after `--web-test-view` to override it. `--web-test-view --no-test` is invalid. Normal test runs include a Docker integration test using `debian:forky`, so Docker must be available. On systems with 1 GB or less of available memory, the script automatically uses a smaller build configuration and a single build job. diff --git a/compile.sh b/compile.sh index 2e2aa7b..79163b9 100755 --- a/compile.sh +++ b/compile.sh @@ -78,7 +78,28 @@ command -v gcc >/dev/null 2>&1 || { printf '%s\n' "gcc is required to build tuxreaperd" >&2 exit 1 } -gcc -O2 -static -Wall -Wextra tuxreaperd.c -o build/tuxreaperd + +host_arch=$(uname -m) +asm_archs='x86_64 amd64 aarch64 arm64' +build_tuxreaperd() { + case " $asm_archs " in + *" $host_arch "*) + printf '%s\n' "[compile] Building freestanding tuxreaperd for $host_arch" + if gcc -nostdlib -static -fno-stack-protector -fno-asynchronous-unwind-tables \ + -fno-builtin -O2 -s -Wl,--build-id=none -Wl,-z,noseparate-code \ + -Wall -Wextra tuxreaperdasm.c -o build/tuxreaperd 2>/tmp/tuxreaperd-asm-build.log; then + printf '%s\n' "[compile] Freestanding tuxreaperd built successfully" + return 0 + fi + printf '%s\n' "[compile] Freestanding build failed, falling back to libc implementation" >&2 + cat /tmp/tuxreaperd-asm-build.log >&2 + ;; + esac + + printf '%s\n' "[compile] Building standard tuxreaperd for $host_arch" + gcc -O2 -static -Wall -Wextra tuxreaperdgnu.c -o build/tuxreaperd +} +build_tuxreaperd CMAKE_BUILD_PARALLEL_LEVEL="$build_jobs" cmake --build build diff --git a/main.cpp b/main.cpp index ab1f386..13974b3 100644 --- a/main.cpp +++ b/main.cpp @@ -699,7 +699,7 @@ void TuxDockApp::ActionExecDetachedCommand() { } void TuxDockApp::ActionAbout() { - OpenMessage("About Tux-Dock", "Tux-Dock 0.2-beta | Created by markmental"); + OpenMessage("About Tux-Dock", "Tux-Dock 0.3-beta | Created by markmental"); } void TuxDockApp::ExecuteSelectedAction() { diff --git a/tuxreaperdasm.c b/tuxreaperdasm.c new file mode 100644 index 0000000..5f8c17e --- /dev/null +++ b/tuxreaperdasm.c @@ -0,0 +1,488 @@ +struct k_sigaction { + void (*handler)(int); + unsigned long flags; + void (*restorer)(void); + unsigned long mask; +}; + +#if defined(__x86_64__) + +static inline long sys_write(int fd, const void *buf, long n) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(1), "D"((long)fd), "S"(buf), "d"(n) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_exit_group(int status) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(231), "D"((long)status) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_fork(void) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(57) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_execve(const char *path, char *const argv[], char *const envp[]) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(59), "D"(path), "S"(argv), "d"(envp) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_wait4(long pid, int *status, int options, void *rusage) { + long ret; + register long r10 __asm__("r10") = (long)rusage; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(61), "D"(pid), "S"(status), "d"(options), "r"(r10) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_kill(long pid, int sig) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(62), "D"(pid), "S"((long)sig) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_prctl(int option, unsigned long arg2, unsigned long arg3, + unsigned long arg4, unsigned long arg5) { + long ret; + register long r10 __asm__("r10") = (long)arg4; + register long r8 __asm__("r8") = (long)arg5; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(157), "D"((long)option), "S"(arg2), "d"(arg3), "r"(r10), "r"(r8) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_rt_sigaction(int sig, const struct k_sigaction *act, + struct k_sigaction *oldact, unsigned long sigsetsize) { + long ret; + register long r10 __asm__("r10") = (long)sigsetsize; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(13), "D"((long)sig), "S"(act), "d"(oldact), "r"(r10) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_rt_sigprocmask(int how, const unsigned long *set, + unsigned long *oldset, unsigned long sigsetsize) { + long ret; + register long r10 __asm__("r10") = (long)sigsetsize; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(14), "D"((long)how), "S"(set), "d"(oldset), "r"(r10) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long sys_rt_sigsuspend(const unsigned long *mask, unsigned long sigsetsize) { + long ret; + __asm__ volatile ( + "syscall" + : "=a"(ret) + : "a"(130), "D"(mask), "S"(sigsetsize) + : "rcx", "r11", "memory" + ); + return ret; +} + +__asm__( + ".text\n" + ".globl restore_rt\n" + "restore_rt:\n" + " movq $15, %rax\n" + " syscall\n" +); + +__asm__( + ".text\n" + ".globl _start\n" + "_start:\n" + " xorl %ebp, %ebp\n" + " movq (%rsp), %rdi\n" /* argc */ + " leaq 8(%rsp), %rsi\n" /* argv */ + " movq %rsi, %rdx\n" /* scan pointer = argv */ + "1:\n" + " addq $8, %rdx\n" + " cmpq $0, (%rdx)\n" + " jne 1b\n" + " addq $8, %rdx\n" /* envp */ + " andq $-16, %rsp\n" /* align stack */ + " call main\n" + " movq %rax, %rdi\n" + " movq $231, %rax\n" /* exit_group */ + " syscall\n" +); + +#elif defined(__aarch64__) + +static inline long sys_write(int fd, const void *buf, long n) { + register long x8 __asm__("x8") = 64; + register long x0 __asm__("x0") = fd; + register long x1 __asm__("x1") = (long)buf; + register long x2 __asm__("x2") = n; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_exit_group(int status) { + register long x8 __asm__("x8") = 94; + register long x0 __asm__("x0") = status; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_fork(void) { + /* AArch64 has no legacy fork syscall; clone(SIGCHLD, 0) gives fork semantics. */ + register long x8 __asm__("x8") = 220; /* __NR_clone */ + register long x0 __asm__("x0") = 17; /* SIGCHLD */ + register long x1 __asm__("x1") = 0; /* newsp = 0 */ + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_execve(const char *path, char *const argv[], char *const envp[]) { + register long x8 __asm__("x8") = 221; + register long x0 __asm__("x0") = (long)path; + register long x1 __asm__("x1") = (long)argv; + register long x2 __asm__("x2") = (long)envp; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_wait4(long pid, int *status, int options, void *rusage) { + register long x8 __asm__("x8") = 260; + register long x0 __asm__("x0") = pid; + register long x1 __asm__("x1") = (long)status; + register long x2 __asm__("x2") = options; + register long x3 __asm__("x3") = (long)rusage; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_kill(long pid, int sig) { + register long x8 __asm__("x8") = 129; + register long x0 __asm__("x0") = pid; + register long x1 __asm__("x1") = sig; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_prctl(int option, unsigned long arg2, unsigned long arg3, + unsigned long arg4, unsigned long arg5) { + register long x8 __asm__("x8") = 167; + register long x0 __asm__("x0") = option; + register long x1 __asm__("x1") = arg2; + register long x2 __asm__("x2") = arg3; + register long x3 __asm__("x3") = arg4; + register long x4 __asm__("x4") = arg5; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_rt_sigaction(int sig, const struct k_sigaction *act, + struct k_sigaction *oldact, unsigned long sigsetsize) { + register long x8 __asm__("x8") = 134; + register long x0 __asm__("x0") = sig; + register long x1 __asm__("x1") = (long)act; + register long x2 __asm__("x2") = (long)oldact; + register long x3 __asm__("x3") = sigsetsize; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_rt_sigprocmask(int how, const unsigned long *set, + unsigned long *oldset, unsigned long sigsetsize) { + register long x8 __asm__("x8") = 135; + register long x0 __asm__("x0") = how; + register long x1 __asm__("x1") = (long)set; + register long x2 __asm__("x2") = (long)oldset; + register long x3 __asm__("x3") = sigsetsize; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + : "memory", "cc" + ); + return x0; +} + +static inline long sys_rt_sigsuspend(const unsigned long *mask, unsigned long sigsetsize) { + register long x8 __asm__("x8") = 133; + register long x0 __asm__("x0") = (long)mask; + register long x1 __asm__("x1") = sigsetsize; + __asm__ volatile ( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1) + : "memory", "cc" + ); + return x0; +} + +__asm__( + ".text\n" + ".globl restore_rt\n" + "restore_rt:\n" + " mov x8, #139\n" + " svc #0\n" +); + +__asm__( + ".text\n" + ".globl _start\n" + "_start:\n" + " ldr x0, [sp]\n" /* argc */ + " add x1, sp, #8\n" /* argv */ + " mov x2, x1\n" /* scan pointer = argv */ + "1:\n" + " ldr x3, [x2]\n" + " add x2, x2, #8\n" + " cbnz x3, 1b\n" /* x2 now points to envp */ + " mov x4, sp\n" + " bic x4, x4, #15\n" /* align stack via scratch register */ + " mov sp, x4\n" + " bl main\n" + " mov x8, #94\n" /* exit_group */ + " svc #0\n" +); + +#else +#error "tuxreaperdasm.c supports x86_64 and aarch64 only" +#endif + +static long my_strlen(const char *s) { + long n = 0; + while (s[n]) n++; + return n; +} + +static int my_strncmp(const char *a, const char *b, long n) { + for (long i = 0; i < n; i++) { + unsigned char ca = a[i]; + unsigned char cb = b[i]; + if (ca != cb) return (int)ca - (int)cb; + if (ca == 0) return 0; + } + return 0; +} + +static const char *my_strchr(const char *s, char c) { + while (*s) { + if (*s == c) return s; + s++; + } + return 0; +} + +static void my_memcpy(char *dst, const char *src, long n) { + for (long i = 0; i < n; i++) dst[i] = src[i]; +} + +static const char *my_getenv(const char *name, char *const *envp) { + long nlen = my_strlen(name); + for (int i = 0; envp[i]; i++) { + if (my_strncmp(envp[i], name, nlen) == 0 && envp[i][nlen] == '=') { + return envp[i] + nlen + 1; + } + } + return 0; +} + +static void my_execvp(const char *file, char *const argv[], char *const envp[]) { + if (my_strchr(file, '/')) { + sys_execve(file, argv, envp); + return; + } + + const char *path = my_getenv("PATH", envp); + if (!path) path = "/bin:/usr/bin"; + + long flen = my_strlen(file); + const char *p = path; + + while (*p) { + const char *colon = my_strchr(p, ':'); + long dirlen = colon ? (colon - p) : my_strlen(p); + char full[512]; + + if (dirlen + 1 + flen + 1 <= (long)sizeof(full)) { + my_memcpy(full, p, dirlen); + full[dirlen] = '/'; + my_memcpy(full + dirlen + 1, file, flen); + full[dirlen + 1 + flen] = '\0'; + sys_execve(full, argv, envp); + } + + if (!colon) break; + p = colon + 1; + } +} + +static volatile long g_main_child = 0; +static volatile int g_child_exited = 0; +static volatile int g_main_status = 0; + +static void forward_sig(int sig) { + if (g_main_child > 0) { + sys_kill(g_main_child, sig); + } +} + +static void sigchld_handler(int sig) { + (void)sig; + int status; + long pid; + + while ((pid = sys_wait4(-1, &status, 1 /* WNOHANG */, 0)) > 0) { + if (pid == g_main_child) { + g_child_exited = 1; + g_main_status = status; + } + } +} + +#define WIFEXITED(s) (((s) & 0x7f) == 0) +#define WEXITSTATUS(s) (((s) >> 8) & 0xff) +#define WIFSIGNALED(s) ((((s) & 0x7f) != 0) && (((s) & 0x7f) != 0x7f)) +#define WTERMSIG(s) ((s) & 0x7f) + +void restore_rt(void); + +int main(int argc, char **argv, char **envp) { + if (argc < 2) { + static const char usage[] = "Usage: tuxreaperd [args...]\n"; + sys_write(2, usage, sizeof(usage) - 1); + return 1; + } + + if (sys_prctl(36 /* PR_SET_CHILD_SUBREAPER */, 1, 0, 0, 0) < 0) { + static const char err[] = "prctl(PR_SET_CHILD_SUBREAPER) failed\n"; + sys_write(2, err, sizeof(err) - 1); + return 1; + } + + struct k_sigaction sa; + sa.handler = sigchld_handler; + sa.flags = 0x10000000 /* SA_RESTART */ | 1 /* SA_NOCLDSTOP */ | 0x04000000 /* SA_RESTORER */; + sa.restorer = restore_rt; + sa.mask = 0; + sys_rt_sigaction(17 /* SIGCHLD */, &sa, 0, sizeof(sa.mask)); + + sa.handler = forward_sig; + sa.flags = 0x10000000 /* SA_RESTART */ | 0x04000000 /* SA_RESTORER */; + sa.restorer = restore_rt; + sa.mask = 0; + sys_rt_sigaction(15 /* SIGTERM */, &sa, 0, sizeof(sa.mask)); + sys_rt_sigaction(2 /* SIGINT */, &sa, 0, sizeof(sa.mask)); + sys_rt_sigaction(1 /* SIGHUP */, &sa, 0, sizeof(sa.mask)); + + unsigned long block_mask = + (1UL << (17 - 1)) | (1UL << (15 - 1)) | + (1UL << (2 - 1)) | (1UL << (1 - 1)); + unsigned long old_mask; + sys_rt_sigprocmask(0 /* SIG_BLOCK */, &block_mask, &old_mask, sizeof(block_mask)); + + g_main_child = sys_fork(); + if (g_main_child == 0) { + sys_rt_sigprocmask(2 /* SIG_SETMASK */, &old_mask, 0, sizeof(old_mask)); + my_execvp(argv[1], &argv[1], envp); + static const char err[] = "execvp failed\n"; + sys_write(2, err, sizeof(err) - 1); + sys_exit_group(127); + } + + while (!g_child_exited) { + sys_rt_sigsuspend(&old_mask, sizeof(old_mask)); + } + + int status; + while (sys_wait4(-1, &status, 1 /* WNOHANG */, 0) > 0); + + if (WIFEXITED(g_main_status)) { + return WEXITSTATUS(g_main_status); + } else if (WIFSIGNALED(g_main_status)) { + return 128 + WTERMSIG(g_main_status); + } + + return 0; +} diff --git a/tuxreaperd.c b/tuxreaperdgnu.c similarity index 99% rename from tuxreaperd.c rename to tuxreaperdgnu.c index c61da3d..9474474 100644 --- a/tuxreaperd.c +++ b/tuxreaperdgnu.c @@ -88,4 +88,3 @@ int main(int argc, char *argv[]) { return 0; } -