git: f9f4abe8c9f8 - main - tests/sys/file: add tests for the lowest fd after fork
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 02 Nov 2026 21:13:04 UTC
The branch main has been updated by maxim:
URL: https://cgit.FreeBSD.org/src/commit/?id=f9f4abe8c9f8b5a783538d2b99254d9845572e70
commit f9f4abe8c9f8b5a783538d2b99254d9845572e70
Author: Maxim Konovalov <maxim@FreeBSD.org>
AuthorDate: 2026-11-01 14:59:08 +0000
Commit: Maxim Konovalov <maxim@FreeBSD.org>
CommitDate: 2026-11-02 21:11:44 +0000
tests/sys/file: add tests for the lowest fd after fork
fdcopy() skips plain kqueues and close-on-fork descriptors when it
copies the table into the child. Check that dup(2) in the child still
returns the lowest unused descriptor, both when the skipped descriptor
is the lowest free slot and when it sits above an existing hole.
Test cases:
kqueue_no_hole a plain kqueue is the lowest descriptor; the
child's dup() reuses its slot
kqueue_two two adjacent plain kqueues; the child's dup()
returns the lower one
kqueue_above_hole a plain kqueue sits above a closed descriptor;
the child's dup() returns the hole
kqueue_cponfork a KQUEUE_CPONFORK kqueue is copied; the child's
dup() returns the next slot
clofork_no_hole an O_CLOFORK descriptor is the lowest one; the
child's dup() reuses its slot
clofork_above_hole an O_CLOFORK descriptor sits above a closed
descriptor; the child's dup() returns the hole
Without 99a58803f9a3, kqueue_no_hole, kqueue_two and
clofork_above_hole fail.
PR: 300234
Reviewed by: kib
MFC after: 1 week
MFC with: 99a58803f9a3
Co-authored-by: Claude Code Opus 5.5
Differential Revision: https://reviews.freebsd.org/D61124
---
tests/sys/file/Makefile | 1 +
tests/sys/file/fork_lowfd_test.c | 141 +++++++++++++++++++++++++++++++++++++++
2 files changed, 142 insertions(+)
diff --git a/tests/sys/file/Makefile b/tests/sys/file/Makefile
index c1fcef68d08e..173a01a1ff61 100644
--- a/tests/sys/file/Makefile
+++ b/tests/sys/file/Makefile
@@ -6,6 +6,7 @@ ATF_TESTS_C+= path_test
ATF_TESTS_C+= closefrom_test
TAP_TESTS_C+= dup_test
ATF_TESTS_C+= fcntlflags_test
+ATF_TESTS_C+= fork_lowfd_test
TAP_TESTS_SH+= flock_test
PLAIN_TESTS_C+= ftruncate_test
PLAIN_TESTS_C+= newfileops_on_fork_test
diff --git a/tests/sys/file/fork_lowfd_test.c b/tests/sys/file/fork_lowfd_test.c
new file mode 100644
index 000000000000..81d867e03255
--- /dev/null
+++ b/tests/sys/file/fork_lowfd_test.c
@@ -0,0 +1,141 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Maxim Konovalov <maxim@FreeBSD.org>
+ */
+
+/*
+ * fdcopy() does not copy some descriptors into the child (plain kqueues,
+ * close-on-fork descriptors). The child's lowest-free-descriptor hint
+ * must still let dup(2) return the lowest unused descriptor, as POSIX
+ * requires. See PR 300234.
+ */
+
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define RFD 100
+#define WFD 101
+
+/* Fork and return the descriptor the child gets from dup(0). */
+static int
+child_dup(void)
+{
+ int fds[2], fd, status;
+ pid_t pid;
+
+ /* Park the pipe high so that it does not change the layout. */
+ ATF_REQUIRE(pipe(fds) == 0);
+ ATF_REQUIRE(dup2(fds[0], RFD) == RFD);
+ ATF_REQUIRE(dup2(fds[1], WFD) == WFD);
+ ATF_REQUIRE(close(fds[0]) == 0);
+ ATF_REQUIRE(close(fds[1]) == 0);
+
+ pid = fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ fd = dup(STDIN_FILENO);
+ _exit(write(WFD, &fd, sizeof(fd)) == sizeof(fd) ? 0 : 1);
+ }
+ ATF_REQUIRE(close(WFD) == 0);
+ ATF_REQUIRE(read(RFD, &fd, sizeof(fd)) == sizeof(fd));
+ ATF_REQUIRE(close(RFD) == 0);
+ ATF_REQUIRE(waitpid(pid, &status, 0) == pid);
+ ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+ return (fd);
+}
+
+static int
+open_null(int flags)
+{
+ int fd;
+
+ fd = open("/dev/null", O_RDONLY | flags);
+ ATF_REQUIRE(fd != -1);
+ return (fd);
+}
+
+ATF_TC_WITHOUT_HEAD(kqueue_no_hole);
+ATF_TC_BODY(kqueue_no_hole, tc)
+{
+ int kq;
+
+ kq = kqueue();
+ ATF_REQUIRE(kq != -1);
+ ATF_CHECK_EQ(child_dup(), kq);
+}
+
+ATF_TC_WITHOUT_HEAD(kqueue_two);
+ATF_TC_BODY(kqueue_two, tc)
+{
+ int kq1, kq2;
+
+ kq1 = kqueue();
+ ATF_REQUIRE(kq1 != -1);
+ kq2 = kqueue();
+ ATF_REQUIRE(kq2 != -1);
+ ATF_CHECK_EQ(child_dup(), kq1);
+}
+
+ATF_TC_WITHOUT_HEAD(kqueue_above_hole);
+ATF_TC_BODY(kqueue_above_hole, tc)
+{
+ int a, b, kq;
+
+ a = open_null(0);
+ b = open_null(0);
+ kq = kqueue();
+ ATF_REQUIRE(kq > b);
+ ATF_REQUIRE(close(a) == 0);
+ ATF_CHECK_EQ(child_dup(), a);
+}
+
+ATF_TC_WITHOUT_HEAD(kqueue_cponfork);
+ATF_TC_BODY(kqueue_cponfork, tc)
+{
+ int kq;
+
+ kq = kqueuex(KQUEUE_CPONFORK);
+ ATF_REQUIRE(kq != -1);
+ ATF_CHECK_EQ(child_dup(), kq + 1);
+}
+
+ATF_TC_WITHOUT_HEAD(clofork_no_hole);
+ATF_TC_BODY(clofork_no_hole, tc)
+{
+ int fd;
+
+ fd = open_null(O_CLOFORK);
+ ATF_CHECK_EQ(child_dup(), fd);
+}
+
+ATF_TC_WITHOUT_HEAD(clofork_above_hole);
+ATF_TC_BODY(clofork_above_hole, tc)
+{
+ int a, b, c;
+
+ a = open_null(0);
+ b = open_null(0);
+ c = open_null(O_CLOFORK);
+ ATF_REQUIRE(c > b);
+ ATF_REQUIRE(close(a) == 0);
+ ATF_CHECK_EQ(child_dup(), a);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, kqueue_no_hole);
+ ATF_TP_ADD_TC(tp, kqueue_two);
+ ATF_TP_ADD_TC(tp, kqueue_above_hole);
+ ATF_TP_ADD_TC(tp, kqueue_cponfork);
+ ATF_TP_ADD_TC(tp, clofork_no_hole);
+ ATF_TP_ADD_TC(tp, clofork_above_hole);
+
+ return (atf_no_error());
+}