From 0d42f5820616654829ca980f9d6a782e7e48eafd Mon Sep 17 00:00:00 2001 From: Julius Bairaktaris Date: Thu, 6 Aug 2026 14:25:58 +0200 Subject: [PATCH 1/2] jail: don't let the jailed process outlive the jail ujail forwards SIGTERM to the jailed process and escalates to SIGKILL after the term timeout, but there is no equivalent for ujail itself being SIGKILLed: the jailed process keeps running with nothing left that can stop it, and whoever supervises ujail sees it exit and starts a second instance of the daemon. Both of procd's own supervisors do send that SIGKILL. instance_timeout() escalates to it when an instance does not stop on SIGTERM, and netifd SIGKILLs every child in netifd_kill_processes() when it exits. Set PR_SET_PDEATHSIG in the child after the user/group and capability transitions, so the setting survives them (commit_creds() zeroes pdeath_signal on any credential change), and before the OCI seccomp filter, which then need not permit prctl() to reach the execve(). The parent can die between fork() and the prctl, leaving the death signal bound to the process the child is reparented to and never delivered, so the child self-terminates when it detects the parent's exit on a pidfd inherited from it; getppid()==1 cannot serve as that detection in a jail that creates a PID namespace, where the parent is not visible and getppid() reads 0 either way. Assisted-by: Claude:claude-opus-5 Signed-off-by: Julius Bairaktaris --- jail/jail.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/jail/jail.c b/jail/jail.c index 22419ef..f58711e 100644 --- a/jail/jail.c +++ b/jail/jail.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include /* musl only defined 15 limit types, make sure all 16 are supported */ #ifndef RLIMIT_RTTIME @@ -1854,6 +1856,7 @@ static struct uloop_timeout pre_exec_timeout = { }; int pipes[4]; +static int parent_pidfd = -1; static int exec_jail(void *arg) { char buf[1]; @@ -2087,6 +2090,32 @@ static void post_start_hook(void) free_and_exit(EXIT_FAILURE); } + /* the supervisor can SIGKILL ujail on its own death (netifd on exit, + * procd's instance_timeout() escalation); die with it, otherwise the + * jailed process keeps running and the supervisor's supervisor starts + * a second instance. Placed after set_jail_user() and the capability + * transitions, whose commit_creds() would zero pdeath_signal, but before + * the seccomp filter, which need not permit prctl() to reach execve(). */ + if (prctl(PR_SET_PDEATHSIG, SIGKILL)) { + ERROR("prctl(PR_SET_PDEATHSIG) failed: %m\n"); + free_and_exit(EXIT_FAILURE); + } + + /* the parent can die between the fork() and the prctl above; the death + * signal is then bound to the process we are reparented to and never + * delivered, so detect the exit on the inherited pidfd and die ourselves. + * getppid() == 1 cannot serve here: in a new PID namespace the parent is + * not visible and getppid() reads 0 either way. */ + if (parent_pidfd >= 0) { + struct pollfd pfd = { .fd = parent_pidfd, .events = POLLIN }; + + if (poll(&pfd, 1, 0) > 0) { + ERROR("parent died before PR_SET_PDEATHSIG\n"); + free_and_exit(EXIT_FAILURE); + } + close(parent_pidfd); + } + char **envp = build_envp(opts.seccomp, opts.envp); if (!envp) free_and_exit(EXIT_FAILURE); @@ -3787,6 +3816,8 @@ static void post_main(struct uloop_timeout *t) if (pipe2(&userns_pipe[0], O_CLOEXEC) < 0 || pipe2(&userns_pipe[2], O_CLOEXEC) < 0) free_and_exit(-1); + parent_pidfd = syscall(SYS_pidfd_open, getpid(), 0); + if (has_namespaces()) { if (opts.namespace & CLONE_NEWNS) { if (!opts.extroot && (opts.user || opts.group)) { @@ -3928,6 +3959,7 @@ static void post_main(struct uloop_timeout *t) /* parent process */ char sig_buf[1]; + close(parent_pidfd); uloop_process_add(&jail_process); jail_running = 1; if (seteuid(0)) { From 2c1834abae171261f2d9161e5a28c36cbcc394a4 Mon Sep 17 00:00:00 2001 From: Julius Bairaktaris Date: Thu, 6 Aug 2026 14:26:06 +0200 Subject: [PATCH 2/2] jail: report signal deaths as 128+signal exit codes ujail's own exit code is all its supervisor can observe, and one that only reads WEXITSTATUS cannot tell a jailed process killed by a signal apart from one that exited with the signal number as its code. pppd's exit table collides head-on: EXIT_PEER_AUTH_FAILED is 9 and EXIT_CONNECT_TIME is 11, so a segfaulted pppd under ujail reads as an auth failure and, with "option authfail 1", blocks the WAN link until manual intervention. Without ujail, netifd recorded 0 for a signal death (WEXITSTATUS on a non-exited status) and the link simply retried. Follow the 128+signal convention shells use: the value then sits outside every consumer's exit-code table instead of inside pppd's. Assisted-by: Claude:claude-opus-5 Signed-off-by: Julius Bairaktaris --- jail/jail.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jail/jail.c b/jail/jail.c index f58711e..d74f62f 100644 --- a/jail/jail.c +++ b/jail/jail.c @@ -1795,8 +1795,8 @@ static void jail_process_handler(struct uloop_process *c, int ret) jail_return_code = WEXITSTATUS(ret); INFO("jail (%d) exited with exit: %d\n", c->pid, jail_return_code); } else { - jail_return_code = WTERMSIG(ret); - INFO("jail (%d) exited with signal: %d\n", c->pid, jail_return_code); + jail_return_code = 128 + WTERMSIG(ret); + INFO("jail (%d) exited with signal: %d\n", c->pid, WTERMSIG(ret)); } jail_running = 0; poststop();