* [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS
@ 2026-07-15 5:41 Baban
2026-07-15 5:41 ` [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Baban
` (2 more replies)
0 siblings, 3 replies; 11+ messages in thread
From: Baban @ 2026-07-15 5:41 UTC (permalink / raw)
To: yocto-patches
Cc: Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert,
Babanpreet Singh
pseudo's close_range() wrapper has returned ENOSYS unconditionally since
35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34"). That
was a deliberate call, and the commit message says why: "This one is
straight forward as it allows ENOSYS to be returned and the caller has to
handle it so lets do that."
It held for five years. It doesn't any more. systemd's close_all_fds() has
called close_range() since v247, but v260 raised its kernel baseline to
5.10, concluded close_range() (5.9) is always present, deleted its
/proc/self/fd fallback and started treating a failure as fatal.
safe_fork_full() then aborts the child, so under pseudo every fork+exec
dies:
Failed to close all file descriptors: Function not implemented
'(mkfs)' failed with exit status 1.
That is [YOCTO #16339], reported by Vincent Haupert against a
systemd-repart image build.
Vincent - Randy asked on the bug whether you wanted to send a patch for
this, and that question was addressed to you, not to the list. It had been
quiet for a few days and this looked worth picking up, but if you already
have it in progress please say so and ignore this series. I'm happy to
defer.
1/2 implements it. The stub was protecting something real: pseudo keeps its
own descriptors, including the socket to its server, inside the range a
caller asks to close, and the kernel does the closing, so pseudo cannot ask
it to skip one. Returning ENOSYS pushed callers onto the /proc loop, which
closes one fd at a time through close() - a path pseudo already wraps and
can defend itself inside. With the fallback gone, that no longer works.
So this follows the shape 21ff2fb ("ports/linux/guts: Add closefrom support
for glibc 2.34") already established for the same problem: a client-side op
works out the first fd above every descriptor pseudo keeps, closes the ones
below it by hand while stepping around its own, and hands that fd back so
the kernel is only turned loose on the tail that is safe. close_range() has
a top end where closefrom() does not, so both the manual loop and the path
table cleanup are bounded by maxfd.
The flags are settled before any of that. CLOSE_RANGE_UNSHARE has to take
effect first, or descriptors get closed for everyone still sharing the
table rather than just for us. CLOSE_RANGE_CLOEXEC closes nothing and
pseudo's own fds are close-on-exec already, so there is nothing to protect
and it goes straight to the kernel. An unknown flag or an inverted range is
refused up front, matching the kernel.
2/2 adds a test. It probes for kernel support with PSEUDO_DISABLED=1 first,
so that a wrapper which goes back to returning ENOSYS fails the test rather
than quietly skipping it.
Tested on x86_64 (Ubuntu 24.04, glibc 2.39). close_range() under pseudo is
identical to a run without pseudo across eight cases: single fd, bounded
range, both flags, an inverted range, an unknown flag, an already-closed
fd, and an unbounded range. pseudo also comes through close_range(3, ~0U, 0)
with its server connection intact and still tracking ownership, which is
the case that actually matters here. run_tests.sh is unchanged from master
apart from the new test passing (two pre-existing flaky parallel-* failures
show up either way).
Comparing against the real kernel is what caught the argument validation:
without it, close_range(3, 3, 0x80) returned success and closed fd 3, on a
call the kernel rejects outright.
Not tested: x86_64 only, no 32-bit/aarch64/riscv build; no end-to-end
systemd-repart reproduction, the ENOSYS-to-working transition is shown
directly instead; and CLOSE_RANGE_UNSHARE is exercised for return value and
closing behaviour but not against a real process sharing the descriptor
table, so its isolating effect rests on unshare(CLONE_FILES) semantics
rather than observation.
One question while I was in here. OP_CLOSEFROM steps around five of pseudo's
descriptors, but there are others it doesn't mention - pseudo_prefix_dir_fd,
pseudo_pwd_lck_fd, pseudo_util_evlog_fd. I kept the new op consistent with
the existing five rather than change closefrom()'s behaviour as a drive-by,
but if that list is short then it's short in both places and I'm happy to
send a follow-up.
Babanpreet Singh (2):
ports/linux/guts: Implement close_range() instead of returning ENOSYS
tests: Add close_range() test
enums/op.in | 1 +
ports/linux/guts/close_range.c | 55 +++++++++--
ports/linux/portdefs.h | 16 ++++
pseudo_client.c | 60 ++++++++++++
test/test-close-range.c | 167 +++++++++++++++++++++++++++++++++
test/test-close-range.sh | 16 ++++
6 files changed, 308 insertions(+), 7 deletions(-)
create mode 100644 test/test-close-range.c
create mode 100755 test/test-close-range.sh
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS 2026-07-15 5:41 [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS Baban @ 2026-07-15 5:41 ` Baban 2026-07-15 19:51 ` [yocto-patches] " Paul Barker 2026-07-15 5:41 ` [pseudo] [PATCH 2/2] tests: Add close_range() test Baban 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh 2 siblings, 1 reply; 11+ messages in thread From: Baban @ 2026-07-15 5:41 UTC (permalink / raw) To: yocto-patches Cc: Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh close_range() has been wrapped since 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34"), but the wrapper only sets ENOSYS, on the grounds that callers have to handle that anyway. That was true when it was written. It no longer is: systemd's close_all_fds() has used close_range() since v247, and v260 raised its kernel baseline to 5.10, dropped the /proc fallback and began treating a failure as fatal. safe_fork_full() aborts the child when it fails, so under pseudo every fork+exec dies, eg for "pseudo systemd-repart" spawning mkfs: Failed to close all file descriptors: Function not implemented '(mkfs)' failed with exit status 1. Implement it the way closefrom() already is, per 21ff2fb ("ports/linux/ guts: Add closefrom support for glibc 2.34"): a client side op works out the first fd above every descriptor pseudo keeps for itself, closes the ones below it by hand while stepping around its own, and hands that fd back so the caller can turn the kernel loose on the rest. close_range() has a top end where closefrom() does not, so both the manual loop and the path table cleanup are bounded by maxfd. The flags are dealt with before any of that. CLOSE_RANGE_UNSHARE has to take effect first, or descriptors would be closed for everyone still sharing the table rather than just for us. CLOSE_RANGE_CLOEXEC closes nothing, and pseudo's own fds are close-on-exec already, so there is nothing to protect and it can go straight to the kernel. An unknown flag or an inverted range is refused before anything is touched, matching the kernel: otherwise a call which should have failed cleanly takes descriptors with it on the way out. A range starting above INT_MAX cannot hold any of pseudo's own fds and is passed through, since the client op takes the low end as an int. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh <bbnpreetsingh@gmail.com> --- enums/op.in | 1 + ports/linux/guts/close_range.c | 55 +++++++++++++++++++++++++++---- ports/linux/portdefs.h | 16 +++++++++ pseudo_client.c | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/enums/op.in b/enums/op.in index 5b5e21b..5013892 100644 --- a/enums/op.in +++ b/enums/op.in @@ -28,3 +28,4 @@ set-xattr, 0 create-xattr, 1 replace-xattr, 1 closefrom, 0 +close-range, 0 diff --git a/ports/linux/guts/close_range.c b/ports/linux/guts/close_range.c index 4bd2fe1..15258be 100644 --- a/ports/linux/guts/close_range.c +++ b/ports/linux/guts/close_range.c @@ -6,14 +6,55 @@ * int close_range(unsigned int lowfd, unsigned int maxfd, int flags) * int rc = -1; */ + pseudo_msg_t *msg; - (void) lowfd; - (void) maxfd; - (void) flags; - /* for now pretend the kernel doesn't support it regardless - which users are supposed to be able to handle */ - errno = ENOSYS; - rc = -1; + /* The kernel rejects both of these outright, so check before doing + * anything: otherwise a call which should have failed cleanly takes + * descriptors with it on the way out. + */ + if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC)) { + errno = EINVAL; + return -1; + } + if (lowfd > maxfd) { + errno = EINVAL; + return -1; + } + + /* CLOSE_RANGE_UNSHARE has to take effect before anything is closed: + * while the descriptor table is still shared, closing a descriptor + * would close it for everyone sharing the table, not just for us. + */ + if (flags & CLOSE_RANGE_UNSHARE) { + if (unshare(CLONE_FILES) == -1) + return -1; + flags &= ~CLOSE_RANGE_UNSHARE; + } + + /* CLOSE_RANGE_CLOEXEC closes nothing, it only marks descriptors, and + * pseudo's own are close-on-exec already (pseudo_fd() sets that on + * every one of them), so there is nothing here to protect. + */ + if (flags & CLOSE_RANGE_CLOEXEC) + return real_close_range(lowfd, maxfd, flags); + + /* Descriptors are ints, so a range starting above INT_MAX cannot hold + * any of pseudo's own and there is nothing to step around. Worth its + * own case because pseudo_client_op() takes the low end as an int. + */ + if (lowfd > INT_MAX) + return real_close_range(lowfd, maxfd, flags); + + /* Same shape as closefrom(): the client op closes the descriptors its + * own are mixed in with by hand, stepping around the ones pseudo needs + * to keep, and hands back the first fd the kernel can safely be turned + * loose on. + */ + msg = pseudo_client_op(OP_CLOSE_RANGE, 0, lowfd, -1, 0, 0, maxfd); + if (maxfd >= (unsigned int) msg->fd) + rc = real_close_range(msg->fd, maxfd, flags); + else + rc = 0; /* return rc; * } diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h index 19bb232..1f1a41a 100644 --- a/ports/linux/portdefs.h +++ b/ports/linux/portdefs.h @@ -35,6 +35,22 @@ GLIBC_COMPAT_SYMBOL(memcpy,2.0); #include <sys/prctl.h> #include <linux/seccomp.h> +/* close_range()'s flags, and unshare(), are only declared by glibc under + * _GNU_SOURCE, which pseudo does not build with. <linux/close_range.h> is + * not an option either: it is absent on hosts with pre-5.9 kernel headers, + * the same problem SYS_openat2 has below. Both values are kernel ABI. + */ +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif +#ifndef CLONE_FILES +#define CLONE_FILES 0x00000400 +#endif +extern int unshare(int flags); + #ifndef _STAT_VER #if defined (__aarch64__) || defined (__riscv) #define _STAT_VER 0 diff --git a/pseudo_client.c b/pseudo_client.c index 7041366..1acd948 100644 --- a/pseudo_client.c +++ b/pseudo_client.c @@ -993,6 +993,28 @@ pseudo_client_closefrom(int fd) { } } +/* Like pseudo_client_closefrom(), but bounded: close_range() has a top end, + * so entries above it have to be left alone. + */ +static void +pseudo_client_close_range(int lowfd, unsigned int maxfd) { + int i, top; + + if (lowfd < 0 || lowfd >= nfds) + return; + + top = (maxfd >= (unsigned int) nfds) ? nfds - 1 : (int) maxfd; + for (i = lowfd; i <= top; ++i) { + free(fd_paths[i]); + fd_paths[i] = 0; + + if (i < linked_nfds) { + free(linked_fd_paths[i]); + linked_fd_paths[i] = 0; + } + } +} + /* spawn server */ static int client_spawn_server(void) { @@ -1633,6 +1655,7 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path static size_t alloced_len = 0; int strip_slash; int startfd, i; + unsigned int close_range_maxfd = 0; #ifdef PSEUDO_PROFILING struct timeval tv1_op, tv2_op; @@ -1753,6 +1776,13 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path } #endif + if (op == OP_CLOSE_RANGE) { + va_list ap; + va_start(ap, buf); + close_range_maxfd = va_arg(ap, unsigned int); + va_end(ap); + } + if (op == OP_RENAME) { va_list ap; if (!path) { @@ -1959,6 +1989,36 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path msg.fd = startfd; do_request = 0; break; + case OP_CLOSE_RANGE: + /* no request needed */ + startfd = fd; + if (pseudo_util_debug_fd > startfd) + startfd = pseudo_util_debug_fd + 1; + if (pseudo_localstate_dir_fd > startfd) + startfd = pseudo_localstate_dir_fd + 1; + if (pseudo_pwd_fd > startfd) + startfd = pseudo_pwd_fd + 1; + if (pseudo_grp_fd > startfd) + startfd = pseudo_grp_fd + 1; + if (connect_fd > startfd) + startfd = connect_fd + 1; + /* the fds below startfd are the ones our own are mixed in + * with, so close those by hand and skip the ones we need + */ + for (i = fd; i < startfd && (unsigned int) i <= close_range_maxfd; ++i) { + if (i == pseudo_util_debug_fd || i == pseudo_localstate_dir_fd || i == pseudo_pwd_fd || + i == pseudo_grp_fd || i == connect_fd) + continue; + pseudo_client_close(i); + close(i); + } + if (close_range_maxfd >= (unsigned int) startfd) + pseudo_client_close_range(startfd, close_range_maxfd); + /* tell the caller to start at startfd instead of fd */ + result = &msg; + msg.fd = startfd; + do_request = 0; + break; case OP_CLOSE: /* no request needed */ if (fd >= 0) { -- 2.43.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [yocto-patches] [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS 2026-07-15 5:41 ` [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Baban @ 2026-07-15 19:51 ` Paul Barker 0 siblings, 0 replies; 11+ messages in thread From: Paul Barker @ 2026-07-15 19:51 UTC (permalink / raw) To: yocto-patches Cc: Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh [-- Attachment #1: Type: text/plain, Size: 2530 bytes --] On Wed, 2026-07-15 at 05:41 +0000, Baban via lists.yoctoproject.org wrote: > close_range() has been wrapped since 35433e6 ("ports/linux/guts: Add > close_range wrapper for glibc 2.34"), but the wrapper only sets ENOSYS, > on the grounds that callers have to handle that anyway. That was true > when it was written. It no longer is: systemd's close_all_fds() has used > close_range() since v247, and v260 raised its kernel baseline to 5.10, > dropped the /proc fallback and began treating a failure as fatal. > safe_fork_full() aborts the child when it fails, so under pseudo every > fork+exec dies, eg for "pseudo systemd-repart" spawning mkfs: > > Failed to close all file descriptors: Function not implemented > '(mkfs)' failed with exit status 1. > > Implement it the way closefrom() already is, per 21ff2fb ("ports/linux/ > guts: Add closefrom support for glibc 2.34"): a client side op works out > the first fd above every descriptor pseudo keeps for itself, closes the > ones below it by hand while stepping around its own, and hands that fd > back so the caller can turn the kernel loose on the rest. close_range() > has a top end where closefrom() does not, so both the manual loop and > the path table cleanup are bounded by maxfd. > > The flags are dealt with before any of that. CLOSE_RANGE_UNSHARE has to > take effect first, or descriptors would be closed for everyone still > sharing the table rather than just for us. CLOSE_RANGE_CLOEXEC closes > nothing, and pseudo's own fds are close-on-exec already, so there is > nothing to protect and it can go straight to the kernel. An unknown flag > or an inverted range is refused before anything is touched, matching the > kernel: otherwise a call which should have failed cleanly takes > descriptors with it on the way out. > > A range starting above INT_MAX cannot hold any of pseudo's own fds and > is passed through, since the client op takes the low end as an int. > > [YOCTO #16339] > > AI-Generated: Uses Claude (claude-opus-4-8) > Signed-off-by: Babanpreet Singh <bbnpreetsingh@gmail.com> Hi, I've done some initial review and this looks mostly ok to me. I'll let Mark/Richard give any further review as well. One thing that bothers me: "... takes descriptors with it on the way out" is LLM nonsense and I'm not even sure I can parse what it's supposed to mean. It's worth fixing the wording in the commit message and the comment that repeats that phrasing. Best regards, -- Paul Barker [-- Attachment #2: This is a digitally signed message part --] [-- Type: application/pgp-signature, Size: 252 bytes --] ^ permalink raw reply [flat|nested] 11+ messages in thread
* [pseudo] [PATCH 2/2] tests: Add close_range() test 2026-07-15 5:41 [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS Baban 2026-07-15 5:41 ` [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Baban @ 2026-07-15 5:41 ` Baban 2026-07-15 20:03 ` [yocto-patches] " Paul Barker 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh 2 siblings, 1 reply; 11+ messages in thread From: Baban @ 2026-07-15 5:41 UTC (permalink / raw) To: yocto-patches Cc: Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh 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 diff --git a/test/test-close-range.c b/test/test-close-range.c new file mode 100644 index 0000000..61f729b --- /dev/null +++ b/test/test-close-range.c @@ -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; +} diff --git a/test/test-close-range.sh b/test/test-close-range.sh new file mode 100755 index 0000000..65be016 --- /dev/null +++ b/test/test-close-range.sh @@ -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 -- 2.43.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [yocto-patches] [pseudo] [PATCH 2/2] tests: Add close_range() test 2026-07-15 5:41 ` [pseudo] [PATCH 2/2] tests: Add close_range() test Baban @ 2026-07-15 20:03 ` Paul Barker 0 siblings, 0 replies; 11+ messages in thread From: Paul Barker @ 2026-07-15 20:03 UTC (permalink / raw) To: yocto-patches Cc: Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh [-- Attachment #1: Type: text/plain, Size: 1262 bytes --] On Wed, 2026-07-15 at 05:41 +0000, Baban via lists.yoctoproject.org wrote: > 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> Hi, Thanks for including a test case! I have a couple of bits of feedback. The close_range() syscall was added in Linux 5.9, the CLOSE_RANGE_CLOEXEC flag was added in Linux 5.11. So on Linux 5.9/5.10, the probe will succeed but the test will fail. There is also no test coverage for CLOSE_RANGE_UNSHARED. Otherwise this looks pretty good, please send a v2. Best regards, -- Paul Barker [-- Attachment #2: This is a digitally signed message part --] [-- Type: application/pgp-signature, Size: 252 bytes --] ^ permalink raw reply [flat|nested] 11+ messages in thread
* [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS 2026-07-15 5:41 [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS Baban 2026-07-15 5:41 ` [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Baban 2026-07-15 5:41 ` [pseudo] [PATCH 2/2] tests: Add close_range() test Baban @ 2026-07-16 5:56 ` Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Babanpreet Singh ` (2 more replies) 2 siblings, 3 replies; 11+ messages in thread From: Babanpreet Singh @ 2026-07-16 5:56 UTC (permalink / raw) To: yocto-patches Cc: Paul Barker, Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh pseudo's close_range() wrapper has returned ENOSYS unconditionally since 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34"). systemd v260 deleted its /proc/self/fd fallback and treats a failure as fatal, so under pseudo every fork+exec dies. That is [YOCTO #16339]. Full story in v1: https://lore.kernel.org/yocto-patches/20260715054142.7-1-bbnpreetsingh@gmail.com/ Paul, thanks for the review. Changes in v2, both from it: 1/2: no code change apart from one comment. The commit message is rewritten in plainer language throughout; the "takes descriptors with it on the way out" sentence and the comment that repeated it now simply say that invalid arguments are rejected with EINVAL before anything is closed. 2/2: two real gaps closed. - CLOSE_RANGE_CLOEXEC needs a 5.11 kernel while the syscall itself needs 5.9, so probing only for the syscall made the test fail on 5.9/5.10. The shell wrapper now probes for the flag separately, the same PSEUDO_DISABLED=1 way, and on such a kernel only the CLOEXEC part of the test is left out. - CLOSE_RANGE_UNSHARE is now tested against a real sharer, not argued from unshare() semantics: a child cloned with CLONE_FILES closes a shared descriptor with the flag set, and the parent must still hold it afterwards. A second child closes the same descriptor without the flag and the parent must see it gone, so a clone that quietly stopped sharing the table cannot make the first check pass vacuously. The new test was checked for sensitivity by deliberately removing the unshare() call from the wrapper: it fails with "child's CLOSE_RANGE_UNSHARE closed the parent's fd 3", and passes with the real implementation. The 5.9/5.10 probe path cannot be exercised end to end on this host (6.17 kernel); the --no-cloexec branch it selects was run directly under pseudo and passes. Full run_tests.sh is unchanged from master apart from the new test passing (the two pre-existing flaky parallel-* failures show up either way). The v1 question about OP_CLOSEFROM stepping around five of pseudo's descriptors while others exist (pseudo_prefix_dir_fd, pseudo_pwd_lck_fd, pseudo_util_evlog_fd) still stands; happy to send a follow-up if that list is short in both places. Babanpreet Singh (2): ports/linux/guts: Implement close_range() instead of returning ENOSYS tests: Add close_range() test enums/op.in | 1 + ports/linux/guts/close_range.c | 54 ++++++- ports/linux/portdefs.h | 16 ++ pseudo_client.c | 60 +++++++ test/test-close-range.c | 283 +++++++++++++++++++++++++++++++++ test/test-close-range.sh | 24 +++ 6 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 test/test-close-range.c create mode 100755 test/test-close-range.sh -- 2.43.0 ^ permalink raw reply [flat|nested] 11+ messages in thread
* [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh @ 2026-07-16 5:56 ` Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 2/2] tests: Add close_range() test Babanpreet Singh 2026-07-16 10:46 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Richard Purdie 2 siblings, 0 replies; 11+ messages in thread From: Babanpreet Singh @ 2026-07-16 5:56 UTC (permalink / raw) To: yocto-patches Cc: Paul Barker, Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh The close_range() wrapper added in 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34") returns ENOSYS on the assumption that callers handle that. systemd v260 no longer does: it removed its /proc/self/fd fallback and treats a close_range() failure as fatal, so every fork+exec under pseudo aborts: Failed to close all file descriptors: Function not implemented '(mkfs)' failed with exit status 1. A client side op closes the low descriptors one at a time, skipping the ones pseudo needs for itself, and returns the first fd above pseudo's own so the caller can pass the rest of the range to the kernel directly. CLOSE_RANGE_UNSHARE is handled by calling unshare(CLONE_FILES) before closing anything, so the closes only affect the caller and not other processes sharing the descriptor table. Unknown flags and an inverted range are rejected with EINVAL before anything is closed. A range starting entirely above INT_MAX cannot contain any of pseudo's fds and is also passed straight through. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh <bbnpreetsingh@gmail.com> --- enums/op.in | 1 + ports/linux/guts/close_range.c | 54 ++++++++++++++++++++++++++---- ports/linux/portdefs.h | 16 +++++++++ pseudo_client.c | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/enums/op.in b/enums/op.in index 5b5e21b..5013892 100644 --- a/enums/op.in +++ b/enums/op.in @@ -28,3 +28,4 @@ set-xattr, 0 create-xattr, 1 replace-xattr, 1 closefrom, 0 +close-range, 0 diff --git a/ports/linux/guts/close_range.c b/ports/linux/guts/close_range.c index 4bd2fe1..c3ca71d 100644 --- a/ports/linux/guts/close_range.c +++ b/ports/linux/guts/close_range.c @@ -6,14 +6,54 @@ * int close_range(unsigned int lowfd, unsigned int maxfd, int flags) * int rc = -1; */ + pseudo_msg_t *msg; - (void) lowfd; - (void) maxfd; - (void) flags; - /* for now pretend the kernel doesn't support it regardless - which users are supposed to be able to handle */ - errno = ENOSYS; - rc = -1; + /* The kernel rejects both of these outright and closes nothing when + * it does, so validate before touching anything. + */ + if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC)) { + errno = EINVAL; + return -1; + } + if (lowfd > maxfd) { + errno = EINVAL; + return -1; + } + + /* CLOSE_RANGE_UNSHARE has to take effect before anything is closed: + * while the descriptor table is still shared, closing a descriptor + * would close it for everyone sharing the table, not just for us. + */ + if (flags & CLOSE_RANGE_UNSHARE) { + if (unshare(CLONE_FILES) == -1) + return -1; + flags &= ~CLOSE_RANGE_UNSHARE; + } + + /* CLOSE_RANGE_CLOEXEC closes nothing, it only marks descriptors, and + * pseudo's own are close-on-exec already (pseudo_fd() sets that on + * every one of them), so there is nothing here to protect. + */ + if (flags & CLOSE_RANGE_CLOEXEC) + return real_close_range(lowfd, maxfd, flags); + + /* Descriptors are ints, so a range starting above INT_MAX cannot hold + * any of pseudo's own and there is nothing to step around. Worth its + * own case because pseudo_client_op() takes the low end as an int. + */ + if (lowfd > INT_MAX) + return real_close_range(lowfd, maxfd, flags); + + /* Same shape as closefrom(): the client op closes the descriptors its + * own are mixed in with by hand, stepping around the ones pseudo needs + * to keep, and hands back the first fd the kernel can safely be turned + * loose on. + */ + msg = pseudo_client_op(OP_CLOSE_RANGE, 0, lowfd, -1, 0, 0, maxfd); + if (maxfd >= (unsigned int) msg->fd) + rc = real_close_range(msg->fd, maxfd, flags); + else + rc = 0; /* return rc; * } diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h index 19bb232..1f1a41a 100644 --- a/ports/linux/portdefs.h +++ b/ports/linux/portdefs.h @@ -35,6 +35,22 @@ GLIBC_COMPAT_SYMBOL(memcpy,2.0); #include <sys/prctl.h> #include <linux/seccomp.h> +/* close_range()'s flags, and unshare(), are only declared by glibc under + * _GNU_SOURCE, which pseudo does not build with. <linux/close_range.h> is + * not an option either: it is absent on hosts with pre-5.9 kernel headers, + * the same problem SYS_openat2 has below. Both values are kernel ABI. + */ +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif +#ifndef CLONE_FILES +#define CLONE_FILES 0x00000400 +#endif +extern int unshare(int flags); + #ifndef _STAT_VER #if defined (__aarch64__) || defined (__riscv) #define _STAT_VER 0 diff --git a/pseudo_client.c b/pseudo_client.c index 7041366..1acd948 100644 --- a/pseudo_client.c +++ b/pseudo_client.c @@ -993,6 +993,28 @@ pseudo_client_closefrom(int fd) { } } +/* Like pseudo_client_closefrom(), but bounded: close_range() has a top end, + * so entries above it have to be left alone. + */ +static void +pseudo_client_close_range(int lowfd, unsigned int maxfd) { + int i, top; + + if (lowfd < 0 || lowfd >= nfds) + return; + + top = (maxfd >= (unsigned int) nfds) ? nfds - 1 : (int) maxfd; + for (i = lowfd; i <= top; ++i) { + free(fd_paths[i]); + fd_paths[i] = 0; + + if (i < linked_nfds) { + free(linked_fd_paths[i]); + linked_fd_paths[i] = 0; + } + } +} + /* spawn server */ static int client_spawn_server(void) { @@ -1633,6 +1655,7 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path static size_t alloced_len = 0; int strip_slash; int startfd, i; + unsigned int close_range_maxfd = 0; #ifdef PSEUDO_PROFILING struct timeval tv1_op, tv2_op; @@ -1753,6 +1776,13 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path } #endif + if (op == OP_CLOSE_RANGE) { + va_list ap; + va_start(ap, buf); + close_range_maxfd = va_arg(ap, unsigned int); + va_end(ap); + } + if (op == OP_RENAME) { va_list ap; if (!path) { @@ -1959,6 +1989,36 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path msg.fd = startfd; do_request = 0; break; + case OP_CLOSE_RANGE: + /* no request needed */ + startfd = fd; + if (pseudo_util_debug_fd > startfd) + startfd = pseudo_util_debug_fd + 1; + if (pseudo_localstate_dir_fd > startfd) + startfd = pseudo_localstate_dir_fd + 1; + if (pseudo_pwd_fd > startfd) + startfd = pseudo_pwd_fd + 1; + if (pseudo_grp_fd > startfd) + startfd = pseudo_grp_fd + 1; + if (connect_fd > startfd) + startfd = connect_fd + 1; + /* the fds below startfd are the ones our own are mixed in + * with, so close those by hand and skip the ones we need + */ + for (i = fd; i < startfd && (unsigned int) i <= close_range_maxfd; ++i) { + if (i == pseudo_util_debug_fd || i == pseudo_localstate_dir_fd || i == pseudo_pwd_fd || + i == pseudo_grp_fd || i == connect_fd) + continue; + pseudo_client_close(i); + close(i); + } + if (close_range_maxfd >= (unsigned int) startfd) + pseudo_client_close_range(startfd, close_range_maxfd); + /* tell the caller to start at startfd instead of fd */ + result = &msg; + msg.fd = startfd; + do_request = 0; + break; case OP_CLOSE: /* no request needed */ if (fd >= 0) { -- 2.43.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* [pseudo] [PATCH v2 2/2] tests: Add close_range() test 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Babanpreet Singh @ 2026-07-16 5:56 ` Babanpreet Singh 2026-07-16 10:46 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Richard Purdie 2 siblings, 0 replies; 11+ messages in thread From: Babanpreet Singh @ 2026-07-16 5:56 UTC (permalink / raw) To: yocto-patches Cc: Paul Barker, Richard Purdie, Mark Hatle, Randy MacLeod, Vincent Haupert, Babanpreet Singh 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_UNSHARE is exercised against a child cloned with CLONE_FILES: the child closes a shared descriptor with the flag set, and the parent must still hold that descriptor afterwards, which is what the wrapper's unshare-before-closing order exists to preserve. A second child then closes the same descriptor without the flag and the parent must see it gone. close_range() needs a 5.9 kernel and CLOSE_RANGE_CLOEXEC a 5.11 one, so the shell wrapper probes for each with PSEUDO_DISABLED=1 before running. On 5.9 and 5.10 the syscall probe succeeds, the flag probe fails, and only the CLOSE_RANGE_CLOEXEC part of the test is left out. [YOCTO #16339] AI-Generated: Uses Claude (claude-opus-4-8) Signed-off-by: Babanpreet Singh <bbnpreetsingh@gmail.com> --- test/test-close-range.c | 283 +++++++++++++++++++++++++++++++++++++++ test/test-close-range.sh | 24 ++++ 2 files changed, 307 insertions(+) create mode 100644 test/test-close-range.c create mode 100755 test/test-close-range.sh diff --git a/test/test-close-range.c b/test/test-close-range.c new file mode 100644 index 0000000..482c85e --- /dev/null +++ b/test/test-close-range.c @@ -0,0 +1,283 @@ +/* + * 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, that CLOSE_RANGE_UNSHARE keeps a + * process sharing the descriptor table unaffected, 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 <sched.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +/* <linux/close_range.h> is missing on hosts with pre-5.9 kernel headers */ +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#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; +} + +/* CLOSE_RANGE_CLOEXEC is newer than the syscall: 5.11 rather than 5.9. Probe + * for it separately, the same way, so that on a 5.9 or 5.10 kernel only the + * CLOSE_RANGE_CLOEXEC part of the test is left out, not the whole thing. + */ +static int probe_cloexec(void) { + int fd = open_null(); + int rc; + + if (fd < 0) + return 1; + rc = close_range(fd, fd, CLOSE_RANGE_CLOEXEC); + if (rc == -1) { + int err = errno; + close(fd); + return (err == EINVAL || err == ENOSYS) ? SKIP : 1; + } + close(fd); + return 0; +} + +/* for the CLONE_FILES children: close one descriptor, with or without + * CLOSE_RANGE_UNSHARE, and report whether it is gone for the child itself. + */ +struct child_close { + int fd; + int flags; +}; + +static char child_stack[65536]; + +static int child_close_one(void *arg) { + struct child_close *cc = arg; + + if (close_range(cc->fd, cc->fd, cc->flags) == -1) + return 2; + return is_closed(cc->fd) ? 0 : 3; +} + +int main(int argc, char **argv) { + struct child_close cc; + struct stat st; + int a, b, c; + int status; + int test_cloexec = 1; + pid_t pid; + + if (argc > 1 && !strcmp(argv[1], "--probe")) + return probe(); + if (argc > 1 && !strcmp(argv[1], "--probe-cloexec")) + return probe_cloexec(); + if (argc > 1 && !strcmp(argv[1], "--no-cloexec")) + test_cloexec = 0; + + /* 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. The + * flag needs a 5.11 kernel; the shell wrapper passes --no-cloexec + * when the probe says this kernel has the syscall but not the flag. + */ + if (test_cloexec) { + 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 a refused call closes nothing */ + 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; + } + + /* CLOSE_RANGE_UNSHARE unshares the descriptor table before closing: + * when a child cloned with CLONE_FILES closes our shared descriptor + * with the flag set, we must still hold that descriptor afterwards. + * This is what the wrapper's unshare-first order preserves; closing + * by hand before unsharing would go through the still-shared table. + */ + a = open_null(); + if (a < 0) + return 1; + cc.fd = a; + cc.flags = CLOSE_RANGE_UNSHARE; + pid = clone(child_close_one, child_stack + sizeof(child_stack), + CLONE_FILES | SIGCHLD, &cc); + if (pid == -1) { + perror("clone(CLONE_FILES)"); + return 1; + } + if (waitpid(pid, &status, 0) == -1) { + perror("waitpid"); + return 1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "CLOSE_RANGE_UNSHARE failed in the child (status %d)\n", + status); + return 1; + } + if (is_closed(a)) { + fprintf(stderr, "child's CLOSE_RANGE_UNSHARE closed the parent's fd %d\n", a); + return 1; + } + + /* the control for the test above: the same close without the flag + * happens in the shared table, so this time fd a must be gone for + * us as well. A clone that quietly stopped sharing the table would + * pass the test above no matter what the wrapper did; it fails here + * instead. + */ + cc.flags = 0; + pid = clone(child_close_one, child_stack + sizeof(child_stack), + CLONE_FILES | SIGCHLD, &cc); + if (pid == -1) { + perror("clone(CLONE_FILES)"); + return 1; + } + if (waitpid(pid, &status, 0) == -1) { + perror("waitpid"); + return 1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "close_range() failed in the CLONE_FILES child (status %d)\n", + status); + return 1; + } + if (!is_closed(a)) { + fprintf(stderr, "close in a CLONE_FILES child did not reach the parent's fd %d\n", a); + 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; +} diff --git a/test/test-close-range.sh b/test/test-close-range.sh new file mode 100755 index 0000000..578102c --- /dev/null +++ b/test/test-close-range.sh @@ -0,0 +1,24 @@ +#!/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 + +# CLOSE_RANGE_CLOEXEC needs 5.11. On a 5.9 or 5.10 kernel the syscall probe +# above succeeds but the flag does not exist yet, so probe for it the same +# way and leave only the CLOSE_RANGE_CLOEXEC part of the test out. +PSEUDO_DISABLED=1 ./test/test-close-range --probe-cloexec +case $? in +0) ./test/test-close-range ;; +77) ./test/test-close-range --no-cloexec ;; +*) exit 1 ;; +esac -- 2.43.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 2/2] tests: Add close_range() test Babanpreet Singh @ 2026-07-16 10:46 ` Richard Purdie 2026-07-16 15:46 ` Babanpreet Singh 2 siblings, 1 reply; 11+ messages in thread From: Richard Purdie @ 2026-07-16 10:46 UTC (permalink / raw) To: Babanpreet Singh, yocto-patches Cc: Paul Barker, Mark Hatle, Randy MacLeod, Vincent Haupert, Mark Hatle On Thu, 2026-07-16 at 05:56 +0000, Babanpreet Singh wrote: > pseudo's close_range() wrapper has returned ENOSYS unconditionally since > 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34"). > systemd v260 deleted its /proc/self/fd fallback and treats a failure as > fatal, so under pseudo every fork+exec dies. That is [YOCTO #16339]. Full > story in v1: > > https://lore.kernel.org/yocto-patches/20260715054142.7-1-bbnpreetsingh@gmail.com/ I had a quick look through this and it looks ok to me, thanks! > The v1 question about OP_CLOSEFROM stepping around five of pseudo's > descriptors while others exist (pseudo_prefix_dir_fd, pseudo_pwd_lck_fd, > pseudo_util_evlog_fd) still stands; happy to send a follow-up if that list > is short in both places. I had a quick look and yes, we should really be protecting these too. That said, I couldn't see what pseudo_prefix_dir_fd actually helps with and I couldn't see why we need that... Cheers, Richard ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS 2026-07-16 10:46 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Richard Purdie @ 2026-07-16 15:46 ` Babanpreet Singh 2026-07-16 16:55 ` Richard Purdie 0 siblings, 1 reply; 11+ messages in thread From: Babanpreet Singh @ 2026-07-16 15:46 UTC (permalink / raw) To: Richard Purdie Cc: yocto-patches, Paul Barker, Mark Hatle, Randy MacLeod, Vincent Haupert, Mark Hatle [-- Attachment #1: Type: text/plain, Size: 1815 bytes --] Thanks! > That said, I couldn't see what pseudo_prefix_dir_fd actually helps with > and I couldn't see why we need that... I flagged it since it is held open and a close_range() sweep will wipe it out. I hadn't checked if anything still reads it untill now. It seems like it is just kept open and alive and the socket connection + fchdir() has been moved to pseudo_localstate_dir_fd. So, for the follow-up I'd purpose: 1/2: remove pseudo_prefix_dir_fd 2/2: add pseudo_pwd_lck_fd and pseudo_util_evlog_fd to the descriptors OP_CLOSEFROM and OP_CLOSE_RANGE step around If you'd rather keep the fd, I'll drop the removal and add it to the protected set in 2/2 instead. Thanks for reviewing, Baban. On Thu, 16 Jul 2026 at 03:46, Richard Purdie < richard.purdie@linuxfoundation.org> wrote: > On Thu, 2026-07-16 at 05:56 +0000, Babanpreet Singh wrote: > > pseudo's close_range() wrapper has returned ENOSYS unconditionally since > > 35433e6 ("ports/linux/guts: Add close_range wrapper for glibc 2.34"). > > systemd v260 deleted its /proc/self/fd fallback and treats a failure as > > fatal, so under pseudo every fork+exec dies. That is [YOCTO #16339]. Full > > story in v1: > > > > > https://lore.kernel.org/yocto-patches/20260715054142.7-1-bbnpreetsingh@gmail.com/ > > I had a quick look through this and it looks ok to me, thanks! > > > The v1 question about OP_CLOSEFROM stepping around five of pseudo's > > descriptors while others exist (pseudo_prefix_dir_fd, pseudo_pwd_lck_fd, > > pseudo_util_evlog_fd) still stands; happy to send a follow-up if that > list > > is short in both places. > > I had a quick look and yes, we should really be protecting these too. > > That said, I couldn't see what pseudo_prefix_dir_fd actually helps with > and I couldn't see why we need that... > > Cheers, > > Richard > [-- Attachment #2: Type: text/html, Size: 2478 bytes --] ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS 2026-07-16 15:46 ` Babanpreet Singh @ 2026-07-16 16:55 ` Richard Purdie 0 siblings, 0 replies; 11+ messages in thread From: Richard Purdie @ 2026-07-16 16:55 UTC (permalink / raw) To: Babanpreet Singh Cc: yocto-patches, Paul Barker, Mark Hatle, Randy MacLeod, Vincent Haupert, Mark Hatle On Thu, 2026-07-16 at 08:46 -0700, Babanpreet Singh wrote: > Thanks! > > > That said, I couldn't see what pseudo_prefix_dir_fd actually helps with > > and I couldn't see why we need that... > > I flagged it since it is held open and a close_range() sweep will wipe it out. I hadn't checked if anything still reads it untill now. It seems like it is just kept open and alive and the socket connection + fchdir() has been moved to pseudo_localstate_dir_fd. > > So, for the follow-up I'd purpose: > > 1/2: remove pseudo_prefix_dir_fd > 2/2: add pseudo_pwd_lck_fd and pseudo_util_evlog_fd to the descriptors OP_CLOSEFROM and OP_CLOSE_RANGE step around > > If you'd rather keep the fd, I'll drop the removal and add it to the protected set in 2/2 instead. I'd suggest making the series the other way around, protect it for now, send a removal patch on top and we can look at the removal patch and test and check it isn't really used anywhere. It is possible I'm missing some usage of it and I'm curious what Mark thinks. The protection patch makes sense and should be easy to merge compared to that. Cheers, Richard ^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-07-16 16:56 UTC | newest] Thread overview: 11+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-15 5:41 [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS Baban 2026-07-15 5:41 ` [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Baban 2026-07-15 19:51 ` [yocto-patches] " Paul Barker 2026-07-15 5:41 ` [pseudo] [PATCH 2/2] tests: Add close_range() test Baban 2026-07-15 20:03 ` [yocto-patches] " Paul Barker 2026-07-16 5:56 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS Babanpreet Singh 2026-07-16 5:56 ` [pseudo] [PATCH v2 2/2] tests: Add close_range() test Babanpreet Singh 2026-07-16 10:46 ` [pseudo] [PATCH v2 0/2] close_range: implement it rather than return ENOSYS Richard Purdie 2026-07-16 15:46 ` Babanpreet Singh 2026-07-16 16:55 ` Richard Purdie
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.