new file mode 100644
@@ -0,0 +1,167 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * close_range() was stubbed out to return ENOSYS unconditionally, on the
+ * assumption that callers all cope with that. Current systemd does not: it
+ * dropped its /proc fallback once its kernel baseline reached 5.10, so every
+ * fork+exec under pseudo failed. Check the wrapper works, that it agrees
+ * with the kernel about bad arguments, and that pseudo still functions after
+ * a caller closes every descriptor from 3 up.
+ */
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/* <linux/close_range.h> is missing on hosts with pre-5.9 kernel headers */
+#ifndef CLOSE_RANGE_CLOEXEC
+#define CLOSE_RANGE_CLOEXEC (1U << 2)
+#endif
+
+/* matches test-openat2-syscall: the shell wrapper turns 77 into "skipped" */
+#define SKIP 77
+
+#define TESTFILE "test-close-range-file"
+
+static int open_null(void) {
+ int fd = open("/dev/null", O_RDONLY);
+ if (fd < 0)
+ perror("open /dev/null");
+ return fd;
+}
+
+static int is_closed(int fd) {
+ return fcntl(fd, F_GETFD) == -1 && errno == EBADF;
+}
+
+/* Is close_range() there at all? The wrapper runs this with PSEUDO_DISABLED
+ * set, so the answer is the kernel's and a stubbed wrapper cannot pass
+ * itself off as an old kernel.
+ */
+static int probe(void) {
+ int fd = open_null();
+
+ if (fd < 0)
+ return 1;
+ if (close_range(fd, fd, 0) == -1) {
+ int err = errno;
+ close(fd);
+ return (err == ENOSYS) ? SKIP : 1;
+ }
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ struct stat st;
+ int a, b, c;
+
+ if (argc > 1 && !strcmp(argv[1], "--probe"))
+ return probe();
+
+ /* the reported bug: this used to fail with ENOSYS every time */
+ a = open_null();
+ if (a < 0)
+ return 1;
+ if (close_range(a, a, 0) == -1) {
+ fprintf(stderr, "close_range(%d, %d, 0): %s\n", a, a, strerror(errno));
+ return 1;
+ }
+ if (!is_closed(a)) {
+ fprintf(stderr, "close_range() left fd %d open\n", a);
+ return 1;
+ }
+
+ /* a bounded range closes all of itself */
+ a = open_null();
+ b = open_null();
+ c = open_null();
+ if (a < 0 || b < 0 || c < 0)
+ return 1;
+ if (close_range(a, c, 0) == -1) {
+ perror("close_range(a, c, 0)");
+ return 1;
+ }
+ if (!is_closed(a) || !is_closed(b) || !is_closed(c)) {
+ fprintf(stderr, "close_range(%d, %d, 0) left descriptors open\n", a, c);
+ return 1;
+ }
+
+ /* CLOSE_RANGE_CLOEXEC marks descriptors, it does not close them */
+ a = open_null();
+ if (a < 0)
+ return 1;
+ if (close_range(a, a, CLOSE_RANGE_CLOEXEC) == -1) {
+ perror("close_range(a, a, CLOSE_RANGE_CLOEXEC)");
+ return 1;
+ }
+ if (is_closed(a)) {
+ fprintf(stderr, "CLOSE_RANGE_CLOEXEC closed fd %d\n", a);
+ return 1;
+ }
+ if (!(fcntl(a, F_GETFD) & FD_CLOEXEC)) {
+ fprintf(stderr, "CLOSE_RANGE_CLOEXEC did not set FD_CLOEXEC on fd %d\n", a);
+ return 1;
+ }
+ close(a);
+
+ /* bad arguments are refused, and nothing is closed on the way out */
+ a = open_null();
+ if (a < 0)
+ return 1;
+ if (close_range(a, a, 0x80) != -1 || errno != EINVAL) {
+ fprintf(stderr, "close_range() with an unknown flag should fail EINVAL\n");
+ return 1;
+ }
+ if (is_closed(a)) {
+ fprintf(stderr, "a rejected close_range() closed fd %d anyway\n", a);
+ return 1;
+ }
+ if (close_range(a + 1, a, 0) != -1 || errno != EINVAL) {
+ fprintf(stderr, "close_range() with lowfd > maxfd should fail EINVAL\n");
+ return 1;
+ }
+ close(a);
+
+ /* a range entirely above INT_MAX holds no descriptors at all, but it
+ * still has to be accepted rather than mangled on the way through
+ */
+ if (close_range((unsigned int) INT_MAX + 1, ~0U, 0) == -1) {
+ fprintf(stderr, "close_range(INT_MAX+1, ~0U, 0): %s\n", strerror(errno));
+ return 1;
+ }
+
+ /* And the point of the care in the wrapper: pseudo keeps descriptors
+ * of its own in this range and has to come out of it still working.
+ */
+ a = open(TESTFILE, O_CREAT | O_WRONLY, 0644);
+ if (a < 0) {
+ perror("open " TESTFILE);
+ return 1;
+ }
+ close(a);
+ if (chown(TESTFILE, 0, 0) == -1) {
+ perror("chown " TESTFILE);
+ return 1;
+ }
+ if (close_range(3, ~0U, 0) == -1) {
+ fprintf(stderr, "close_range(3, ~0U, 0): %s\n", strerror(errno));
+ return 1;
+ }
+ if (stat(TESTFILE, &st) == -1) {
+ perror("stat after close_range");
+ return 1;
+ }
+ if (st.st_uid != 0 || st.st_gid != 0) {
+ fprintf(stderr, "pseudo lost track after close_range: uid %d, gid %d\n",
+ (int) st.st_uid, (int) st.st_gid);
+ return 1;
+ }
+ unlink(TESTFILE);
+
+ return 0;
+}
new file mode 100755
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+# close_range() needs a 5.9 kernel. Ask with pseudo out of the way, so that a
+# wrapper which has gone back to returning ENOSYS cannot pass itself off as an
+# old kernel and quietly skip the test it is supposed to fail.
+PSEUDO_DISABLED=1 ./test/test-close-range --probe
+case $? in
+0) ;;
+77) exit 255 ;;
+*) exit 1 ;;
+esac
+
+./test/test-close-range
Covers the ENOSYS regression itself, that the wrapper agrees with the kernel about bad arguments, and the part that actually needs the care: pseudo keeps its own descriptors in the range, including the connection to its server, and has to come out of a close_range(3, ~0U, 0) still tracking ownership. close_range() needs a 5.9 kernel, so the shell wrapper probes for it with PSEUDO_DISABLED=1 before running anything. Asking with pseudo out of the way keeps a wrapper which has gone back to returning ENOSYS from passing itself off as an old kernel and quietly skipping the test it is supposed to fail. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh <bbnpreetsingh@gmail.com> --- test/test-close-range.c | 167 +++++++++++++++++++++++++++++++++++++++ test/test-close-range.sh | 16 ++++ 2 files changed, 183 insertions(+) create mode 100644 test/test-close-range.c create mode 100755 test/test-close-range.sh