diff --git a/src/Makefile b/src/Makefile index 67124b2..c71a097 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,7 +15,7 @@ PORTAL_OBJS := $(patsubst $(src)/%.c,%.o,$(PORTAL_SRCS)) igloo-objs += igloo_hc.o hooks/syscalls_hc.o hooks/ioctl_hc.o \ hooks/sock_hc.o hooks/uname_hc.o hooks/block_mounts.o \ - hooks/igloo_open.o hooks/signal_hc.o \ + hooks/igloo_open.o hooks/signal_hc.o hooks/exit_hc.o \ hyperfs/hyperfs.o \ netdevs/igloonet.o \ $(PORTAL_OBJS) diff --git a/src/hooks/exit_hc.c b/src/hooks/exit_hc.c new file mode 100644 index 0000000..4e1a6e9 --- /dev/null +++ b/src/hooks/exit_hc.c @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include +#include "igloo_hypercall.h" /* pulls in igloo_hypercall_consts.h (no guard) */ +#include "igloo.h" +#include "exit_hc.h" +#include "portal/portal.h" +#include "portal/portal_internal.h" +#include "args.h" + +/* + * do_exit() task-exit hook. + * + * A kprobe on do_exit() fires on EVERY task death -- normal exit/exit_group, + * fatal signals, OOM, kernel-forced -- which the exit/exit_group *syscall* + * hooks miss (a signal death never issues an exit syscall). This is the + * authoritative process-exit source for the `processes` model: it replaces the + * host-side signal-death heuristic (which can't tell a caught SIGSEGV from a + * fatal one) and gives the real, wait-status-encoded exit code. + * + * Gated by an enable flag toggled from the host (register/unregister ops) so it + * costs nothing unless a plugin opts in -- same shape as signal_hc's hook count. + */ + +static struct kprobe exit_kp; +static bool exit_kp_registered; /* portal ops are serialized, so a plain bool */ + +static void do_exit_hyp(struct exit_event *event) { + igloo_portal(IGLOO_HYP_PROC_EXIT, (unsigned long)event, 0); +} + +static int exit_pre_handler(struct kprobe *p, struct pt_regs *regs) { + struct task_struct *t = current; + struct exit_event event; + + /* No enable-flag check: the probe is only installed while enabled (armed on + * the host register op, removed on unregister), so if we are here it is on. + * That makes the disabled case cost literally nothing -- no probe, no trap. */ + + /* User processes only; kernel threads are not part of the process model. + * At do_exit() entry the task's mm/flags are still intact (exit_mm runs + * later inside do_exit), so PF_KTHREAD is reliable here. */ + if (t->flags & PF_KTHREAD) + return 0; + + /* One event per process: report at thread-group-leader death, matching the + * leader-only for_each_process walk. exit_group tears down every thread + * including the leader, so a process death always reaches here via the + * leader; non-leader thread exits are intentionally not surfaced. */ + if (!thread_group_leader(t)) + return 0; + + memset(&event, 0, sizeof(event)); + event.pid = task_tgid_vnr(t); + event.tid = task_pid_vnr(t); + event.create_time = t->start_time; /* identity: pairs w/ ProcStart */ + event.code = (int64_t)(long)regs_get_argument(regs, 0); /* do_exit(long code) */ + strncpy(event.comm, t->comm, TASK_COMM_LEN); + + do_exit_hyp(&event); + return 0; +} + +int exit_hc_init(void) { + /* Nothing to arm at boot: the kprobe is installed lazily on the first host + * enable() so an un-opted-in run pays zero cost (no probe, no per-exit + * trap). Prep the probe descriptor only. */ + memset(&exit_kp, 0, sizeof(exit_kp)); + exit_kp.symbol_name = "do_exit"; + exit_kp.pre_handler = exit_pre_handler; + exit_kp_registered = false; + return 0; +} + +/* Host enable/disable. Lazily arm/disarm the kprobe so the disabled state is + * free. Portal op handlers run in the (sleepable) portal loop context, so + * register_kprobe()/unregister_kprobe() are safe to call here. No per-process + * filtering: the model wants every user-process death. */ +void handle_op_register_exit_hook(portal_region *mem_region) { + int ret = 0; + + if (!exit_kp_registered) { + ret = register_kprobe(&exit_kp); + if (ret < 0) + /* do_exit is NOKPROBE-blacklisted on some kernels; degrade + * gracefully (no exit events). If this ever bites a target kernel, + * switch symbol_name to the sched_process_exit tracepoint, which is + * never blacklisted. */ + printk(KERN_ERR "IGLOO: Failed to register kprobe on do_exit: %d " + "(exit events unavailable)\n", ret); + else { + exit_kp_registered = true; + printk(KERN_INFO "IGLOO: Armed exit kprobe on do_exit\n"); + } + } + + mem_region->header.op = (ret < 0) ? HYPER_RESP_WRITE_FAIL + : HYPER_RESP_WRITE_OK; +} + +void handle_op_unregister_exit_hook(portal_region *mem_region) { + if (exit_kp_registered) { + unregister_kprobe(&exit_kp); + exit_kp_registered = false; + } + mem_region->header.op = HYPER_RESP_WRITE_OK; +} diff --git a/src/hooks/exit_hc.h b/src/hooks/exit_hc.h new file mode 100644 index 0000000..cf57d25 --- /dev/null +++ b/src/hooks/exit_hc.h @@ -0,0 +1,33 @@ +#ifndef _EXIT_HC_H +#define _EXIT_HC_H + +#include +#include +#include "portal/portal_types.h" + +/* + * Emitted once per user-process death, from a kprobe on do_exit(). Unlike the + * exit/exit_group *syscall* hooks, this fires on the kernel task-exit path, so + * it also captures deaths that never issue an exit syscall -- fatal signals + * (SIGSEGV/SIGKILL/...), OOM kills, and kernel-forced exits -- with the real + * exit code. ``code`` is the raw do_exit() argument in wait(2) status encoding: + * (code & 0x7f) != 0 -> killed by signal (code & 0x7f); 0x80 bit = core dump + * else -> exited normally, status = (code >> 8) & 0xff + * so the host decodes WIFSIGNALED / WIFEXITED without guessing. + * + * ``create_time`` is task->start_time -- the same value osi_proc_node uses as + * process identity -- so a ProcExit pairs exactly with its ProcStart. + */ +struct exit_event { + uint64_t pid; /* thread-group id = process id (namespaced) */ + uint64_t tid; /* task pid = thread id (namespaced) */ + uint64_t create_time; /* task->start_time; identity, pairs w/ start */ + int64_t code; /* raw do_exit() code, wait-status encoded */ + char comm[TASK_COMM_LEN]; /* dying task comm */ +}; + +int exit_hc_init(void); +void handle_op_register_exit_hook(portal_region *mem_region); +void handle_op_unregister_exit_hook(portal_region *mem_region); + +#endif /* _EXIT_HC_H */ diff --git a/src/igloo_hc.c b/src/igloo_hc.c index 2f4b233..b77ee49 100644 --- a/src/igloo_hc.c +++ b/src/igloo_hc.c @@ -38,6 +38,7 @@ int uname_hc_init(void); int block_mounts_init(void); int igloo_open_init(void); int signal_hc_init(void); +int exit_hc_init(void); /* Register probes for mmap and munmap */ int init_module(void) { @@ -58,6 +59,11 @@ int init_module(void) { return ret; } + if ((ret = exit_hc_init()) != 0) { + printk(KERN_ERR "Failed to register exit_hc returning %d\n", ret); + return ret; + } + if ((ret = ioctl_hc_init()) != 0) { printk(KERN_ERR "Failed to register ioctl_hc returning %d\n", ret); return ret; diff --git a/src/igloo_hypercall_consts.h b/src/igloo_hypercall_consts.h index 1cda943..f9a4c97 100644 --- a/src/igloo_hypercall_consts.h +++ b/src/igloo_hypercall_consts.h @@ -31,6 +31,7 @@ enum igloo_hypercall_constants { IGLOO_HYP_SYSCALL_RETURN = 0x1339, IGLOO_HYP_SETUP_TASK_COMM = 0x133a, IGLOO_HYP_SIGNAL_DELIVER = 0x133b, + IGLOO_HYP_PROC_EXIT = 0x133c, /* Uprobe operations */ IGLOO_HYP_UPROBE_ENTER = 0x6901, diff --git a/src/portal/portal_op_list.h b/src/portal/portal_op_list.h index 6e8e450..cc3bca6 100644 --- a/src/portal/portal_op_list.h +++ b/src/portal/portal_op_list.h @@ -28,6 +28,8 @@ X(set_portalcall_fastpath, SET_PORTALCALL_FASTPATH) \ X(register_signal_hook, REGISTER_SIGNAL_HOOK) \ X(unregister_signal_hook, UNREGISTER_SIGNAL_HOOK) \ + X(register_exit_hook, REGISTER_EXIT_HOOK) \ + X(unregister_exit_hook, UNREGISTER_EXIT_HOOK) \ X(ffi_exec, FFI_EXEC) \ X(kallsyms_lookup, KALLSYMS_LOOKUP) \ X(tramp_generate, TRAMP_GENERATE) \