From: "Baban" <bbnpreetsingh@gmail.com>
To: yocto-patches@lists.yoctoproject.org
Cc: Richard Purdie <richard.purdie@linuxfoundation.org>,
Mark Hatle <mark.hatle@amd.com>,
Randy MacLeod <randy.macleod@windriver.com>,
Vincent Haupert <mail@vincent-haupert.de>,
Babanpreet Singh <bbnpreetsingh@gmail.com>
Subject: [pseudo] [PATCH 1/2] ports/linux/guts: Implement close_range() instead of returning ENOSYS
Date: Wed, 15 Jul 2026 05:41:41 +0000 [thread overview]
Message-ID: <20260715054142.7-2-bbnpreetsingh@gmail.com> (raw)
In-Reply-To: <20260715054142.7-1-bbnpreetsingh@gmail.com>
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
next prev parent reply other threads:[~2026-07-15 9:06 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-15 5:41 [pseudo] [PATCH 0/2] close_range: implement it rather than return ENOSYS Baban
2026-07-15 5:41 ` Baban [this message]
2026-07-15 5:41 ` [pseudo] [PATCH 2/2] tests: Add close_range() test Baban
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260715054142.7-2-bbnpreetsingh@gmail.com \
--to=bbnpreetsingh@gmail.com \
--cc=mail@vincent-haupert.de \
--cc=mark.hatle@amd.com \
--cc=randy.macleod@windriver.com \
--cc=richard.purdie@linuxfoundation.org \
--cc=yocto-patches@lists.yoctoproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.