git: 0083a4d6224f - main - tests/procdesc: Add some test cases for pdopenpid()

From: Mark Johnston <markj_at_FreeBSD.org>
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());
 }