diff --git a/src/runtime/forkipc.c b/src/runtime/forkipc.c index 403e6485..c3ac7306 100644 --- a/src/runtime/forkipc.c +++ b/src/runtime/forkipc.c @@ -2239,7 +2239,10 @@ int64_t sys_clone3(hv_vcpu_t vcpu, * child and write the guest FD number to ca.pidfd. */ if (ret > 0 && want_pidfd && ca.pidfd != 0) { - int pfd = pidfd_create(g, ret); + /* A CLONE_VM child has no host pid of its own, so it resolves to -1 and + * its pidfd carries no monitor. + */ + int pfd = pidfd_create(g, ret, proc_resolve_guest_pid(ret)); if (pfd >= 0) { int32_t pfd32 = (int32_t) pfd; if (guest_write_small(g, ca.pidfd, &pfd32, sizeof(pfd32)) < 0) { diff --git a/src/syscall/proc-pidfd.c b/src/syscall/proc-pidfd.c index 9b882ce8..91f1502d 100644 --- a/src/syscall/proc-pidfd.c +++ b/src/syscall/proc-pidfd.c @@ -105,7 +105,7 @@ static void *pidfd_monitor_thread(void *arg) return NULL; } -int pidfd_create(guest_t *g, int64_t target_pid) +int pidfd_create(guest_t *g, int64_t target_pid, pid_t host_pid) { (void) g; int pfd[2]; @@ -141,9 +141,16 @@ int pidfd_create(guest_t *g, int64_t target_pid) entry->write_end = pfd[1]; pthread_mutex_unlock(&pidfd_lock); - pid_t host_pid = proc_guest_to_host_pid(target_pid); - if (host_pid > 0) { - bool monitor_ok = false; + /* host_pid <= 0 means the target lives inside this host process -- the + * caller itself, or a CLONE_VM child, which holds a guest tid but no host + * pid of its own. Neither can be watched from here, and neither has exited, + * so the fd stays unreadable rather than being completed. + */ + if (host_pid <= 0) + return gfd; + + bool monitor_ok = false; + { int64_t *ctx = malloc(2 * sizeof(int64_t)); if (ctx) { ctx[0] = target_pid; @@ -164,10 +171,16 @@ int pidfd_create(guest_t *g, int64_t target_pid) free(ctx); } } - if (!monitor_ok) - proc_pidfd_notify_exit(target_pid); } + /* Nothing will ever mark this fd readable without a monitor behind it, so + * complete it rather than leave the guest polling forever. A target that + * has already exited needs no special case: the monitor thread finds it + * gone and completes the fd the same way. + */ + if (!monitor_ok) + proc_pidfd_notify_exit(target_pid); + return gfd; } @@ -204,11 +217,16 @@ int64_t sys_pidfd_open(guest_t *g, int64_t pid, unsigned int flags) if (flags != 0) return -LINUX_EINVAL; + /* The kernel rejects a non-positive pid before it looks anything up. */ + if (pid <= 0) + return -LINUX_EINVAL; + if (pid == proc_get_pid()) - return pidfd_create(g, pid); + return pidfd_create(g, pid, 0); - if (proc_guest_to_host_pid(pid) > 0) - return pidfd_create(g, pid); + pid_t host_pid = proc_resolve_guest_pid(pid); + if (host_pid > 0) + return pidfd_create(g, pid, host_pid); return -LINUX_ESRCH; } @@ -238,7 +256,7 @@ int64_t sys_pidfd_send_signal(guest_t *g, return 0; } - pid_t host_pid = proc_guest_to_host_pid(pid); + pid_t host_pid = proc_resolve_guest_pid(pid); if (host_pid > 0) { if (sig == 0) { if (kill(host_pid, 0) < 0) diff --git a/src/syscall/proc-pidfd.h b/src/syscall/proc-pidfd.h index 98ad841b..d1315a3a 100644 --- a/src/syscall/proc-pidfd.h +++ b/src/syscall/proc-pidfd.h @@ -12,7 +12,12 @@ #include "core/guest.h" void pidfd_init(void); -int pidfd_create(guest_t *g, int64_t target_pid); + +/* Create a pidfd on @target_pid. @host_pid is the host process to watch for its + * exit, or <= 0 when the target lives in this host process and so cannot be + * watched (the caller itself, or a CLONE_VM child). + */ +int pidfd_create(guest_t *g, int64_t target_pid, pid_t host_pid); void proc_pidfd_notify_exit(int64_t exited_pid); int64_t proc_pidfd_lookup_pid(int guest_fd); int64_t sys_pidfd_open(guest_t *g, int64_t pid, unsigned int flags); diff --git a/src/syscall/proc.c b/src/syscall/proc.c index a6bb2479..ceab7de9 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -1814,6 +1814,12 @@ pid_t proc_namespace_host_pid(int64_t guest_pid) : -1; } +pid_t proc_resolve_guest_pid(int64_t guest_pid) +{ + pid_t host_pid = proc_guest_to_host_pid(guest_pid); + return host_pid > 0 ? host_pid : proc_namespace_host_pid(guest_pid); +} + int64_t proc_host_to_guest_pid(pid_t host_pid) { pthread_mutex_lock(&pid_lock); diff --git a/src/syscall/proc.h b/src/syscall/proc.h index 309e5076..7ecb0787 100644 --- a/src/syscall/proc.h +++ b/src/syscall/proc.h @@ -505,6 +505,17 @@ int proc_get_namespace_targets(proc_signal_target_t *out, */ pid_t proc_namespace_host_pid(int64_t guest_pid); +/* Resolve one guest pid the way pid-directed signalling does: the child table + * answers for descendants, the fork-family registry for every other relative. + * Every caller that turns a guest pid the guest named into a host pid -- kill, + * pidfd_open, pidfd_send_signal -- goes through here, so they all reach the + * same set of processes. + * + * Returns the host pid, or -1 when no live fork-family member carries that + * guest pid. + */ +pid_t proc_resolve_guest_pid(int64_t guest_pid); + /* Publish the caller's current guest pid/pgid to the fork-family registry. */ void proc_registry_publish_self(void); diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index 2f4cf5fc..864db593 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -1034,17 +1034,6 @@ static int kill_deliver_targets(const proc_signal_target_t *targets, return delivered; } -/* Resolve a guest pid for kill(2). The child table answers for descendants. - * Every other member of the fork family, the caller's own parent above all, - * exists only in the namespace registry, the same source the group and - * broadcast forms already read. - */ -static pid_t kill_resolve_host_pid(int64_t gpid) -{ - pid_t hpid = proc_guest_to_host_pid(gpid); - return hpid > 0 ? hpid : proc_namespace_host_pid(gpid); -} - static int64_t sc_kill(guest_t *g, uint64_t x0, uint64_t x1, @@ -1094,7 +1083,7 @@ static int64_t sc_kill(guest_t *g, } int64_t r = (pid == (int) our_pid) ? 0 : -LINUX_ESRCH; if (r == -LINUX_ESRCH) { - pid_t hpid = kill_resolve_host_pid((int64_t) pid); + pid_t hpid = proc_resolve_guest_pid((int64_t) pid); if (hpid > 0) r = (kill(hpid, 0) == 0) ? 0 : -LINUX_ESRCH; } @@ -1159,7 +1148,7 @@ static int64_t sc_kill(guest_t *g, signal_queue(sig); return 0; } - pid_t hpid = kill_resolve_host_pid((int64_t) pid); + pid_t hpid = proc_resolve_guest_pid((int64_t) pid); if (hpid > 0) return (proc_send_guest_signal(hpid, (int64_t) pid, sig) == 0) ? 0 diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 14aeadc4..359037dc 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -741,6 +741,8 @@ run_unit_tests() "$bindir/test-kill-pgroup" test_check "$runner" "test-kill-parent" "0 failed" \ "$bindir/test-kill-parent" + test_check "$runner" "test-pidfd-targets" "0 failed" \ + "$bindir/test-pidfd-targets" test_rc "$runner" "test-sigio" 0 "$bindir/test-sigio" test_rc "$runner" "test-fault-signal-mt" 0 "$bindir/test-fault-signal-mt" test_rc "$runner" "test-exit-group-worker" 0 "$bindir/test-exit-group-worker" diff --git a/tests/test-pidfd-targets.c b/tests/test-pidfd-targets.c new file mode 100644 index 00000000..8940e3d8 --- /dev/null +++ b/tests/test-pidfd-targets.c @@ -0,0 +1,225 @@ +/* + * Test which targets pidfd_open accepts and which of them report an exit + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Two relatives outside the caller's descendant table. A forked child opens a + * pidfd on its parent and signals through it, and a second child opens a pidfd + * on its sibling and polls it for the sibling's exit. Both go through the same + * fork-family lookup kill(getppid(), sig) uses. On Linux they succeed; they + * must succeed here too. + * + * Two targets that legitimately have no exit monitor behind them are checked + * too, because both must stay unreadable rather than read as exited: the caller + * itself, and a CLONE_VM child, which holds a guest tid but no host process of + * its own. A non-positive pid is rejected before any lookup. + * + * The sibling half is ordered by two pipe handshakes rather than by sleeps: the + * sibling exits only once the watcher reports its pidfd open, so a slow fork + * cannot turn the test red. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "raw-syscall.h" + +#define __NR_pidfd_open_nr 434 +#define __NR_pidfd_send_signal_nr 424 + + +static volatile sig_atomic_t got_usr1 = 0; + +static void usr1_handler(int sig) +{ + (void) sig; + got_usr1 = 1; +} + +static bool wait_flag(int max_ms) +{ + for (int i = 0; i < max_ms && !got_usr1; i++) { + struct timespec ts = {0, 1000000}; /* 1 ms */ + nanosleep(&ts, NULL); + } + return got_usr1 != 0; +} + +/* Child 1: the parent is not in this child's descendant table, so both the + * probe and the signal resolve through the fork-family registry. + */ +static int signal_parent(void) +{ + if (kill(getppid(), 0) != 0) + return 1; + long pfd = raw_syscall2(__NR_pidfd_open_nr, (long) getppid(), 0); + if (pfd < 0) + return 2; + if (raw_syscall4(__NR_pidfd_send_signal_nr, pfd, 0, 0, 0) != 0) + return 3; + if (raw_syscall4(__NR_pidfd_send_signal_nr, pfd, SIGUSR1, 0, 0) != 0) + return 4; + return 0; +} + +/* Child 2: open a pidfd on the sibling whose pid arrives on @pid_in, report + * that it is open on @ready_out, then wait for the sibling's exit to make the + * pidfd readable. The sibling is neither ancestor nor descendant here. + */ +static int watch_sibling(int pid_in, int ready_out) +{ + int64_t sibling = 0; + if (read(pid_in, &sibling, sizeof(sibling)) != (ssize_t) sizeof(sibling)) + return 1; + long pfd = raw_syscall2(__NR_pidfd_open_nr, (long) sibling, 0); + if (pfd < 0) + return 2; + uint8_t byte = 0; + if (write(ready_out, &byte, 1) != 1) + return 1; + struct pollfd pv = {.fd = (int) pfd, .events = POLLIN}; + int n = poll(&pv, 1, 10000); + return n == 1 && (pv.revents & POLLIN) ? 0 : 3; +} + +/* Map a child exit status onto one line of output. Returns 1 if it failed. */ +static int report(const char *what, pid_t pid, const char *const *reasons) +{ + int status = 0; + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) { + fprintf(stderr, "FAIL: %s did not exit cleanly\n", what); + return 1; + } + int code = WEXITSTATUS(status); + if (code == 0) + return 0; + fprintf(stderr, "FAIL: %s: %s\n", what, reasons[code - 1]); + return 1; +} + +int main(void) +{ + int failed = 0; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = usr1_handler; + sigaction(SIGUSR1, &sa, NULL); + + pid_t signaller = fork(); + if (signaller < 0) + return 1; + if (signaller == 0) + _exit(signal_parent()); + + if (!wait_flag(5000)) { + fprintf(stderr, + "FAIL: SIGUSR1 through the parent pidfd not delivered\n"); + failed++; + } + static const char *const signal_reasons[] = { + "kill(getppid(), 0) failed", + "pidfd_open(getppid()) failed", + "pidfd_send_signal(pfd, 0) failed", + "pidfd_send_signal(pfd, SIGUSR1) failed", + }; + failed += report("parent signaller", signaller, signal_reasons); + + /* A self-pidfd has nothing to watch, and a live process is not an exited + * one, so it must not poll readable. + */ + long selfpfd = raw_syscall2(__NR_pidfd_open_nr, (long) getpid(), 0); + if (selfpfd < 0) { + fprintf(stderr, "FAIL: pidfd_open(self) failed\n"); + failed++; + } else { + struct pollfd sv = {.fd = (int) selfpfd, .events = POLLIN}; + if (poll(&sv, 1, 100) != 0) { + fprintf(stderr, "FAIL: self pidfd reports the caller exited\n"); + failed++; + } + close((int) selfpfd); + } + + /* The kernel rejects a non-positive pid outright rather than reporting it + * as a missing process. + */ + if (raw_syscall2(__NR_pidfd_open_nr, 0, 0) != -22 /* EINVAL */) { + fprintf(stderr, "FAIL: pidfd_open(0) did not return EINVAL\n"); + failed++; + } + if (raw_syscall2(__NR_pidfd_open_nr, -1, 0) != -22 /* EINVAL */) { + fprintf(stderr, "FAIL: pidfd_open(-1) did not return EINVAL\n"); + failed++; + } + + int pidp[2], readyp[2], gop[2]; + if (pipe(pidp) < 0 || pipe(readyp) < 0 || pipe(gop) < 0) + return 1; + + pid_t watcher = fork(); + if (watcher < 0) + return 1; + if (watcher == 0) { + close(pidp[1]); + close(readyp[0]); + close(gop[0]); + close(gop[1]); + _exit(watch_sibling(pidp[0], readyp[1])); + } + + pid_t sibling = fork(); + if (sibling < 0) + return 1; + if (sibling == 0) { + close(pidp[0]); + close(pidp[1]); + close(readyp[0]); + close(readyp[1]); + close(gop[1]); + + /* Exit only once the parent closes the other end, which it does after + * the watcher has its pidfd. + */ + uint8_t byte = 0; + (void) read(gop[0], &byte, 1); + _exit(0); + } + + close(pidp[0]); + close(readyp[1]); + close(gop[0]); + + int64_t sibling_pid = sibling; + if (write(pidp[1], &sibling_pid, sizeof(sibling_pid)) != + (ssize_t) sizeof(sibling_pid)) { + fprintf(stderr, + "FAIL: could not hand the sibling pid to the watcher\n"); + failed++; + } + close(pidp[1]); + + uint8_t ready = 0; + if (read(readyp[0], &ready, 1) != 1) + fprintf(stderr, "note: watcher never reported its pidfd open\n"); + close(readyp[0]); + close(gop[1]); + + waitpid(sibling, NULL, 0); + static const char *const watch_reasons[] = { + "pipe handshake with the parent failed", + "pidfd_open(sibling) failed", + "sibling pidfd never reported the exit", + }; + failed += report("sibling watcher", watcher, watch_reasons); + + printf("%s: %d failed\n", failed == 0 ? "PASS" : "FAIL", failed); + return failed == 0 ? 0 : 1; +} diff --git a/tests/test-pidfd.c b/tests/test-pidfd.c index 5f3562ca..89d686f2 100644 --- a/tests/test-pidfd.c +++ b/tests/test-pidfd.c @@ -28,6 +28,8 @@ #define __NR_wait4 260 #define CLONE_PIDFD 0x00001000 +#define CLONE_VM 0x00000100 +#define __NR_nanosleep 101 #define SIGCHLD 17 int passes = 0, fails = 0; @@ -148,6 +150,62 @@ int main(void) raw_syscall4(__NR_wait4, child, 0, 0, 0); } + /* clone3(CLONE_PIDFD | CLONE_VM): the child runs inside this process and so + * has a guest tid but no host pid of its own. Nothing outside can watch it, + * but it has not exited either, so its pidfd must stay unreadable while it + * runs rather than report an exit that never happened. + * + * This case cannot be cross-checked against a real kernel: a CLONE_VM child + * resumes on the fresh stack below, where the compiler's copy of the clone3 + * return value is undefined, so the child branch is only reliable under + * elfuse. Keep the child to raw syscalls for the same reason. + */ + TEST("clone3 CLONE_PIDFD|CLONE_VM pidfd not readable while child runs"); + { + void *stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (stack == MAP_FAILED) { + FAIL("mmap for stack failed"); + goto done; + } + + int32_t vm_pidfd = -1; + struct clone_args ca = {0}; + ca.flags = CLONE_PIDFD | CLONE_VM; + ca.pidfd = (uint64_t) &vm_pidfd; + ca.exit_signal = SIGCHLD; + ca.stack = (uint64_t) stack; + ca.stack_size = 65536; + + long child = raw_syscall2(__NR_clone3, (long) &ca, sizeof(ca)); + if (child == 0) { + for (;;) { + uint64_t ts[2] = {5, 0}; + raw_syscall2(__NR_nanosleep, (long) ts, 0); + } + } + if (child < 0) { + FAIL("clone3 CLONE_VM failed"); + goto done; + } + + /* Without this, the check below passes vacuously: ppoll skips a + * negative fd and reports nothing, so an unwritten pidfd would look + * like one that correctly stays unreadable. + */ + EXPECT_TRUE(vm_pidfd >= 0, + "clone3 wrote no pidfd for the CLONE_VM " + "child"); + + struct linux_pollfd pf = {.fd = vm_pidfd, .events = 1, .revents = 0}; + uint64_t ts[2] = {0, 100000000}; /* 100 ms */ + long pr = raw_syscall5(__NR_ppoll, (long) &pf, 1, (long) ts, 0, 0); + EXPECT_TRUE(pr == 0 && pf.revents == 0, + "pidfd reports an exit while the CLONE_VM child runs"); + + raw_syscall1(__NR_close, vm_pidfd); + } + done: SUMMARY("test-pidfd"); return fails ? 1 : 0;