The branch main has been updated by markj: URL: https://cgit.FreeBSD.org/src/commit/?id=0083a4d6224f1290af31528b55ba27c51e27ba46
commit 0083a4d6224f1290af31528b55ba27c51e27ba46 Author: Mark Johnston <[email protected]> AuthorDate: 2026-07-07 14:14:39 +0000 Commit: Mark Johnston <[email protected]> CommitDate: 2026-07-07 14:14:39 +0000 tests/procdesc: Add some test cases for pdopenpid() Reviewed by: kib MFC with: 5c32aa785184 ("kern: add pdopenpid(2)") Differential Revision: https://reviews.freebsd.org/D58023 --- tests/sys/kern/procdesc.c | 467 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) diff --git a/tests/sys/kern/procdesc.c b/tests/sys/kern/procdesc.c index 2fe50be45bf7..388104054b82 100644 --- a/tests/sys/kern/procdesc.c +++ b/tests/sys/kern/procdesc.c @@ -27,16 +27,21 @@ */ #include <sys/types.h> +#include <sys/capsicum.h> +#include <sys/resource.h> #include <sys/user.h> #include <sys/proc.h> #include <sys/procdesc.h> #include <sys/sysctl.h> #include <sys/wait.h> +#include <errno.h> #include <fcntl.h> #include <poll.h> #include <pthread.h> +#include <signal.h> #include <stdio.h> +#include <string.h> #include <unistd.h> #include <atf-c.h> @@ -205,11 +210,473 @@ ATF_TC_BODY(poll_exit_wakeup, tc) ATF_REQUIRE_MSG(close(pd) == 0, "close: %s", strerror(errno)); } +/* Tests for pdopenpid(2) */ + +/* + * Basic: open a process descriptor for a child, verify pdgetpid() returns the + * right pid. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_basic); +ATF_TC_BODY(pdopenpid_basic, tc) +{ + pid_t child, queried; + int fd; + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd >= 0, "pdopenpid: %s", strerror(errno)); + + ATF_REQUIRE_MSG(pdgetpid(fd, &queried) == 0, + "pdgetpid: %s", strerror(errno)); + ATF_REQUIRE_EQ(child, queried); + + ATF_REQUIRE_MSG(pdkill(fd, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_EQ(child, waitpid(child, NULL, 0)); + ATF_REQUIRE(close(fd) == 0); +} + +/* + * pdopenpid with PD_CLOEXEC should set the close-on-exec flag. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_cloexec); +ATF_TC_BODY(pdopenpid_cloexec, tc) +{ + pid_t child; + int fd, flags; + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd = pdopenpid(child, PD_CLOEXEC); + ATF_REQUIRE_MSG(fd >= 0, "pdopenpid: %s", strerror(errno)); + + flags = fcntl(fd, F_GETFD); + ATF_REQUIRE(flags >= 0); + ATF_REQUIRE(flags & FD_CLOEXEC); + + ATF_REQUIRE_MSG(pdkill(fd, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_EQ(child, waitpid(child, NULL, 0)); + ATF_REQUIRE(close(fd) == 0); +} + +/* + * Invalid flags should return EINVAL. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_einval); +ATF_TC_BODY(pdopenpid_einval, tc) +{ + ATF_REQUIRE_ERRNO(EINVAL, pdopenpid(getpid(), 0xdeadbeef) < 0); + ATF_REQUIRE_ERRNO(EINVAL, pdopenpid(getpid(), ~PD_ALLOWED_AT_FORK) < 0); +} + +/* + * Validate handling of EMFILE. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_emfile); +ATF_TC_BODY(pdopenpid_emfile, tc) +{ + pid_t child; + struct rlimit rl; + int fd; + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + /* + * Determine the lowest unused fd, then set the fd limit to that + * value so that no new fds can be allocated. + */ + fd = dup(STDIN_FILENO); + ATF_REQUIRE(fd >= 0); + ATF_REQUIRE(close(fd) == 0); + + ATF_REQUIRE_EQ(getrlimit(RLIMIT_NOFILE, &rl), 0); + rl.rlim_cur = fd; + ATF_REQUIRE_EQ(setrlimit(RLIMIT_NOFILE, &rl), 0); + + ATF_REQUIRE_ERRNO(EMFILE, pdopenpid(child, 0) < 0); + + /* + * The child should not have been killed or reparented as a side + * effect of the failed syscall. + */ + ATF_REQUIRE_MSG(kill(child, 0) == 0, + "child was killed: %s", strerror(errno)); + ATF_REQUIRE_MSG(waitpid(child, NULL, WNOHANG) == 0, + "child was reparented"); + + ATF_REQUIRE_MSG(kill(child, SIGKILL) == 0, + "kill: %s", strerror(errno)); + ATF_REQUIRE_EQ(child, waitpid(child, NULL, 0)); +} + +/* + * Opening a nonexistent pid should return ESRCH. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_esrch); +ATF_TC_BODY(pdopenpid_esrch, tc) +{ + ATF_REQUIRE_ERRNO(ESRCH, pdopenpid(123456789, 0) < 0); +} + +/* + * pdopenpid should fail in capability mode. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_capmode); +ATF_TC_BODY(pdopenpid_capmode, tc) +{ + pid_t child, parent; + + parent = getpid(); + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + while (getppid() == parent) + sleep(1); + _exit(0); + } + + ATF_REQUIRE_MSG(cap_enter() == 0, "cap_enter: %s", strerror(errno)); + ATF_REQUIRE_ERRNO(ECAPMODE, pdopenpid(child, 0) < 0); +} + +/* + * Open a process descriptor for a child that already has one from pdfork(). + * Both fds should refer to the same process. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_pdfork_then_open); +ATF_TC_BODY(pdopenpid_pdfork_then_open, tc) +{ + pid_t child, pid1, pid2; + int fd1, fd2; + + child = pdfork(&fd1, PD_DAEMON); + ATF_REQUIRE_MSG(child >= 0, "pdfork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd2 = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd2 >= 0, "pdopenpid: %s", strerror(errno)); + + ATF_REQUIRE(pdgetpid(fd1, &pid1) == 0); + ATF_REQUIRE(pdgetpid(fd2, &pid2) == 0); + ATF_REQUIRE_EQ(pid1, pid2); + ATF_REQUIRE_EQ(child, pid1); + + /* Kill via the second fd, wait via the first. */ + ATF_REQUIRE_MSG(pdkill(fd2, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_MSG(pdwait(fd1, NULL, WEXITED, NULL, NULL) == 0, + "pdwait: %s", strerror(errno)); + + ATF_REQUIRE(close(fd1) == 0); + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * Open a process descriptor for a child created with fork(), which has no + * pre-existing procdesc. Then use pdkill() and pdwait() on it. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_fork_then_open); +ATF_TC_BODY(pdopenpid_fork_then_open, tc) +{ + pid_t child; + int fd, status; + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd >= 0, "pdopenpid: %s", strerror(errno)); + + ATF_REQUIRE_MSG(pdkill(fd, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_MSG(pdwait(fd, &status, WEXITED, NULL, NULL) == 0, + "pdwait: %s", strerror(errno)); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_EQ(WTERMSIG(status), SIGKILL); + + ATF_REQUIRE(close(fd) == 0); +} + +/* + * Closing one fd should not kill the process when another fd still references + * the same procdesc. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_close_one_of_two); +ATF_TC_BODY(pdopenpid_close_one_of_two, tc) +{ + pid_t child, queried; + int fd1, fd2, status; + + child = pdfork(&fd1, PD_DAEMON); + ATF_REQUIRE_MSG(child >= 0, "pdfork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd2 = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd2 >= 0, "pdopenpid: %s", strerror(errno)); + + /* Close the first fd; the process should remain alive. */ + ATF_REQUIRE(close(fd1) == 0); + + /* + * Verify the process is still reachable via the second fd and still + * alive. + */ + ATF_REQUIRE_EQ(0, pdgetpid(fd2, &queried)); + ATF_REQUIRE_EQ(child, queried); + ATF_REQUIRE_MSG(pdkill(fd2, 0) == 0, + "pdkill(0) after closing first fd: %s", strerror(errno)); + + /* Now kill and reap via the second fd. */ + ATF_REQUIRE_MSG(pdkill(fd2, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_MSG(pdwait(fd2, &status, WEXITED, NULL, NULL) == 0, + "pdwait: %s", strerror(errno)); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_EQ(WTERMSIG(status), SIGKILL); + + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * Two calls to pdopenpid for the same process (no pdfork) should both work. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_open_twice); +ATF_TC_BODY(pdopenpid_open_twice, tc) +{ + pid_t child, pid1, pid2; + int fd1, fd2; + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + for (;;) + pause(); + _exit(0); + } + + fd1 = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd1 >= 0, "pdopenpid(1): %s", strerror(errno)); + + fd2 = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd2 >= 0, "pdopenpid(2): %s", strerror(errno)); + + ATF_REQUIRE_EQ(0, pdgetpid(fd1, &pid1)); + ATF_REQUIRE_EQ(0, pdgetpid(fd2, &pid2)); + ATF_REQUIRE_EQ(pid1, pid2); + + /* Close the first, process should survive. */ + ATF_REQUIRE(close(fd1) == 0); + ATF_REQUIRE_MSG(pdkill(fd2, 0) == 0, + "pdkill(0) after closing first fd: %s", strerror(errno)); + + ATF_REQUIRE_MSG(pdkill(fd2, SIGKILL) == 0, + "pdkill: %s", strerror(errno)); + ATF_REQUIRE_MSG(pdwait(fd2, NULL, WEXITED, NULL, NULL) == 0, + "pdwait: %s", strerror(errno)); + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * When a process with two procdesc fds exits, only one pdwait should succeed + * in collecting the exit status. The other should get ESRCH. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_pdwait_only_one); +ATF_TC_BODY(pdopenpid_pdwait_only_one, tc) +{ + pid_t child; + int fd1, fd2, status; + + child = pdfork(&fd1, PD_DAEMON); + ATF_REQUIRE_MSG(child >= 0, "pdfork: %s", strerror(errno)); + if (child == 0) + _exit(42); + + fd2 = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd2 >= 0, "pdopenpid: %s", strerror(errno)); + + /* Collect via the first fd. */ + ATF_REQUIRE_MSG(pdwait(fd1, &status, WEXITED, NULL, NULL) == 0, + "pdwait(fd1): %s", strerror(errno)); + ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 42); + + /* The second fd should no longer be able to collect. */ + ATF_REQUIRE_ERRNO(ESRCH, pdwait(fd2, &status, WEXITED, NULL, NULL) < 0); + + ATF_REQUIRE(close(fd1) == 0); + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * Opening a process descriptor for ourselves should work. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_self); +ATF_TC_BODY(pdopenpid_self, tc) +{ + pid_t child, queried; + int childfd, fd, status; + + /* + * Closing self-referencing procdesc would steal the exit + * status from the parent. Keep one more procdesc for the + * child in the parent so that we can query the state. + * + * For the same reason we must use fork() and test in the + * child, otherwise kyua does not get the SIGCHILD nor the + * exit status. + */ + child = pdfork(&childfd, 0); + if (child == 0) { + fd = pdopenpid(getpid(), PD_DAEMON); + if (fd < 0) + _exit(21); + if (pdgetpid(fd, &queried) == -1) + _exit(22); + if (getpid() != queried) + _exit(23); + if (close(fd) != 0) + _exit(24); + _exit(0); + } + + ATF_REQUIRE(pdgetpid(childfd, &queried) == 0); + ATF_REQUIRE_EQ(queried, child); + ATF_REQUIRE(pdwait(childfd, &status, WEXITED, NULL, NULL) == 0); + + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(WEXITSTATUS(status), 0); + ATF_REQUIRE(close(childfd) == 0); +} + +/* + * pdopenpid for a process that is exiting (P_WEXIT set) should fail with EBUSY. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_exiting); +ATF_TC_BODY(pdopenpid_exiting, tc) +{ + pid_t child; + int fd, pip[2], status; + + ATF_REQUIRE_EQ(pipe(pip), 0); + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + char c; + + close(pip[1]); + (void)read(pip[0], &c, 1); + _exit(0); + } + ATF_REQUIRE(close(pip[0]) == 0); + + /* Tell the child to exit. */ + ATF_REQUIRE(close(pip[1]) == 0); + + /* Might still race. */ + usleep(1000); + fd = pdopenpid(child, 0); + if (fd >= 0) { + ATF_REQUIRE(close(fd) == 0); + } else { + ATF_REQUIRE_MSG(errno == EBUSY, + "unexpected errno %d (%s)", errno, strerror(errno)); + } + + ATF_REQUIRE(waitpid(child, &status, 0) == child); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(WEXITSTATUS(status), 0); +} + +/* + * Make sure that opening a process descriptor doesn't cause a wakeup in the + * target process. + */ +ATF_TC_WITHOUT_HEAD(pdopenpid_no_wakeup); +ATF_TC_BODY(pdopenpid_no_wakeup, tc) +{ + pid_t child; + int fd, pip[2]; + + ATF_REQUIRE(pipe(pip) == 0); + + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork: %s", strerror(errno)); + if (child == 0) { + char buf[1]; + + close(pip[1]); + (void)read(pip[0], buf, 1); + _exit(0); + } + close(pip[0]); + + wait_for_naptime(child, "piperd"); + + fd = pdopenpid(child, 0); + ATF_REQUIRE_MSG(fd >= 0, "pdopenpid: %s", strerror(errno)); + + /* + * If pdopenpid() caused a wakeup, read() returned and the child exited. + */ + wait_for_naptime(child, "piperd"); + + ATF_REQUIRE(close(pip[1]) == 0); + ATF_REQUIRE(close(fd) == 0); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, pid_recycle); ATF_TP_ADD_TC(tp, poll_close_race); ATF_TP_ADD_TC(tp, poll_exit_wakeup); + ATF_TP_ADD_TC(tp, pdopenpid_basic); + ATF_TP_ADD_TC(tp, pdopenpid_cloexec); + ATF_TP_ADD_TC(tp, pdopenpid_einval); + ATF_TP_ADD_TC(tp, pdopenpid_emfile); + ATF_TP_ADD_TC(tp, pdopenpid_esrch); + ATF_TP_ADD_TC(tp, pdopenpid_capmode); + ATF_TP_ADD_TC(tp, pdopenpid_pdfork_then_open); + ATF_TP_ADD_TC(tp, pdopenpid_fork_then_open); + ATF_TP_ADD_TC(tp, pdopenpid_close_one_of_two); + ATF_TP_ADD_TC(tp, pdopenpid_open_twice); + ATF_TP_ADD_TC(tp, pdopenpid_pdwait_only_one); + ATF_TP_ADD_TC(tp, pdopenpid_self); + ATF_TP_ADD_TC(tp, pdopenpid_exiting); + ATF_TP_ADD_TC(tp, pdopenpid_no_wakeup); + return (atf_no_error()); }
