git: 0083a4d6224f - main - tests/procdesc: Add some test cases for pdopenpid()
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 07 Jul 2026 14:16:10 UTC
The branch main has been updated by markj:
URL: https://cgit.FreeBSD.org/src/commit/?id=0083a4d6224f1290af31528b55ba27c51e27ba46
commit 0083a4d6224f1290af31528b55ba27c51e27ba46
Author: Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2026-07-07 14:14:39 +0000
Commit: Mark Johnston <markj@FreeBSD.org>
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());
}