git: fe74f9b33b4b - stable/15 - tests/procdesc: Add some test cases for pdopenpid()

Konstantin Belousov <[email protected]>
Newsgroups gmane.os.freebsd.devel.cvs.src
Message-ID <6a8124b3.446ef.7e651c92__23271.9342153118$1786848857$gmane$org@gitrepo.freebsd.org>
The branch stable/15 has been updated by kib:

URL: https://cgit.FreeBSD.org/src/commit/?id=fe74f9b33b4b1b3e809a5cac5f67564a4910f65e

commit fe74f9b33b4b1b3e809a5cac5f67564a4910f65e
Author:     Mark Johnston <[email protected]>
AuthorDate: 2026-07-07 14:14:39 +0000
Commit:     Konstantin Belousov <[email protected]>
CommitDate: 2026-08-16 02:41:37 +0000

    tests/procdesc: Add some test cases for pdopenpid()
    
    (cherry picked from commit 0083a4d6224f1290af31528b55ba27c51e27ba46)
---
 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());
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.