From 48629a611531379bc5b792dc6f63ac9bb56ca537 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 21 Sep 2026 16:15:33 +0200 Subject: close_range: turn the flags into an enum Turn the close_range() flags into an enum. Makes debugging a lot easier. No functional changes. Link: https://patch.msgid.link/20260921-work-file-close_range_except-v1-5-c20d0b49270d@kernel.org Reviewed-by: Jann Horn Signed-off-by: Christian Brauner (Amutable) --- include/uapi/linux/close_range.h | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'include/uapi/linux') diff --git a/include/uapi/linux/close_range.h b/include/uapi/linux/close_range.h index 2d804281554c..b1b637d74deb 100644 --- a/include/uapi/linux/close_range.h +++ b/include/uapi/linux/close_range.h @@ -2,11 +2,24 @@ #ifndef _UAPI_LINUX_CLOSE_RANGE_H #define _UAPI_LINUX_CLOSE_RANGE_H -/* Unshare the file descriptor table before closing file descriptors. */ -#define CLOSE_RANGE_UNSHARE (1U << 1) +/* + * A macro of one of these names defined before this header is parsed, by + * a libc or by a program's own fallback, would replace the enumerator. + */ +#undef CLOSE_RANGE_UNSHARE +#undef CLOSE_RANGE_CLOEXEC -/* Set the FD_CLOEXEC bit instead of closing the file descriptor. */ -#define CLOSE_RANGE_CLOEXEC (1U << 2) +enum close_range_flags { + /* Unshare the file descriptor table before closing file descriptors. */ + CLOSE_RANGE_UNSHARE = (1U << 1), + + /* Set the FD_CLOEXEC bit instead of closing the file descriptor. */ + CLOSE_RANGE_CLOEXEC = (1U << 2), +}; + +/* Keep #ifdef working and let glibc skip its own definitions. */ +#define CLOSE_RANGE_UNSHARE CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_CLOEXEC CLOSE_RANGE_CLOEXEC #endif /* _UAPI_LINUX_CLOSE_RANGE_H */ -- cgit v1.2.3 From 6939539684ce6ee5cea49c9f2b22177a27c73c67 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 21 Sep 2026 16:15:34 +0200 Subject: file: add CLOSE_RANGE_EXCEPT close_range() operates on [fd, max_fd]. Add CLOSE_RANGE_EXCEPT. This new flag instructs it to operate on all file descriptors outside of the range instead. Without any other flags that means it closes everything except the file descriptors in the specified range. Together with CLOSE_RANGE_CLOEXEC it marks every file descriptor except the ones in the range as close-on-exec. With CLOSE_RANGE_UNSHARE dup_fd() never takes a reference on what is dropped. A task between clone(CLONE_FILES | CLONE_VM | CLONE_VFORK) and execve() that has the descriptors for the child in one window can shed the rest of the shared table in one call: close_range(lo, hi, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_EXCEPT) Nothing outside of the window is referenced by the child at any point. Link: https://patch.msgid.link/20260921-work-file-close_range_except-v1-6-c20d0b49270d@kernel.org Reviewed-by: Jann Horn Signed-off-by: Christian Brauner (Amutable) --- fs/file.c | 76 +++++++++++++++++++++++++++++++--------- include/uapi/linux/close_range.h | 5 +++ 2 files changed, 64 insertions(+), 17 deletions(-) (limited to 'include/uapi/linux') diff --git a/fs/file.c b/fs/file.c index ae7d021398dd..39651c7a992e 100644 --- a/fs/file.c +++ b/fs/file.c @@ -794,34 +794,67 @@ static inline unsigned last_fd(struct fdtable *fdt) } static inline void __range_cloexec(struct files_struct *cur_fds, - unsigned int fd, unsigned int max_fd) + struct fd_range *range) { struct fdtable *fdt; + unsigned int last; - /* make sure we're using the correct maximum value */ spin_lock(&cur_fds->file_lock); fdt = files_fdtable(cur_fds); - max_fd = min(last_fd(fdt), max_fd); - if (fd <= max_fd) - bitmap_set(fdt->close_on_exec, fd, max_fd - fd + 1); + /* make sure we're using the correct maximum value */ + last = last_fd(fdt); + if (!(range->flags & FD_RANGE_EXCEPT)) { + if (range->from <= last) + bitmap_set(fdt->close_on_exec, range->from, + min(range->to, last) - range->from + 1); + } else { + if (range->from > 0) + bitmap_set(fdt->close_on_exec, 0, + min(range->from - 1, last) + 1); + if (range->to < last) + bitmap_set(fdt->close_on_exec, range->to + 1, + last - range->to); + } spin_unlock(&cur_fds->file_lock); } -static inline void __range_close(struct files_struct *files, unsigned int fd, - unsigned int max_fd) +/* Next open descriptor in [fd, max_fd] that @range selects. */ +static inline unsigned int next_fd_to_close(struct fdtable *fdt, + unsigned int fd, unsigned int max_fd, + struct fd_range *range) +{ + fd = find_next_bit(fdt->open_fds, max_fd + 1, fd); + /* Hop over the window the range keeps. */ + if ((range->flags & FD_RANGE_EXCEPT) && + fd >= range->from && fd <= range->to) { + if (range->to >= max_fd) + return max_fd + 1; + fd = find_next_bit(fdt->open_fds, max_fd + 1, range->to + 1); + } + return fd; +} + +static inline void __range_close(struct files_struct *files, + struct fd_range *range) { struct file *file; struct fdtable *fdt; - unsigned n; + unsigned int fd, max_fd; spin_lock(&files->file_lock); fdt = files_fdtable(files); - n = last_fd(fdt); - max_fd = min(max_fd, n); + if (range->flags & FD_RANGE_EXCEPT) { + /* Outside of the range means the whole table. */ + fd = 0; + max_fd = last_fd(fdt); + } else { + fd = range->from; + max_fd = min(range->to, last_fd(fdt)); + } - for (fd = find_next_bit(fdt->open_fds, max_fd + 1, fd); + for (fd = next_fd_to_close(fdt, fd, max_fd, range); fd <= max_fd; - fd = find_next_bit(fdt->open_fds, max_fd + 1, fd + 1)) { + fd = next_fd_to_close(fdt, fd + 1, max_fd, range)) { file = file_close_fd_locked(files, fd); if (file) { spin_unlock(&files->file_lock); @@ -849,21 +882,30 @@ static inline void __range_close(struct files_struct *files, unsigned int fd, * This closes a range of file descriptors. All file descriptors * from @fd up to and including @max_fd are closed. * Currently, errors to close a given file descriptor are ignored. + * + * With CLOSE_RANGE_EXCEPT the range names what to leave alone instead: + * every open file descriptor outside of [@fd, @max_fd] is closed, or + * marked close-on-exec with CLOSE_RANGE_CLOEXEC. */ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd, unsigned int, flags) { struct task_struct *me = current; struct files_struct *cur_fds = me->files, *fds = NULL; + struct fd_range range = {fd, max_fd}; - if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC)) + if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC | + CLOSE_RANGE_EXCEPT)) return -EINVAL; if (fd > max_fd) return -EINVAL; + if (flags & CLOSE_RANGE_EXCEPT) + range.flags |= FD_RANGE_EXCEPT; + if ((flags & CLOSE_RANGE_UNSHARE) && atomic_read(&cur_fds->count) > 1) { - struct fd_range range = {fd, max_fd}, *drop = ⦥ + struct fd_range *drop = ⦥ /* * If the caller requested all fds to be made cloexec we always @@ -884,10 +926,10 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd, } if (flags & CLOSE_RANGE_CLOEXEC) { - __range_cloexec(cur_fds, fd, max_fd); + __range_cloexec(cur_fds, &range); } else if (!fds) { - /* If we unshared, dup_fd() left the range behind already. */ - __range_close(cur_fds, fd, max_fd); + /* If we unshared, dup_fd() already left behind what we'd close. */ + __range_close(cur_fds, &range); } if (fds) { diff --git a/include/uapi/linux/close_range.h b/include/uapi/linux/close_range.h index b1b637d74deb..39eddb3ab613 100644 --- a/include/uapi/linux/close_range.h +++ b/include/uapi/linux/close_range.h @@ -8,6 +8,7 @@ */ #undef CLOSE_RANGE_UNSHARE #undef CLOSE_RANGE_CLOEXEC +#undef CLOSE_RANGE_EXCEPT enum close_range_flags { /* Unshare the file descriptor table before closing file descriptors. */ @@ -15,11 +16,15 @@ enum close_range_flags { /* Set the FD_CLOEXEC bit instead of closing the file descriptor. */ CLOSE_RANGE_CLOEXEC = (1U << 2), + + /* Act on every file descriptor outside of the given range instead. */ + CLOSE_RANGE_EXCEPT = (1U << 3), }; /* Keep #ifdef working and let glibc skip its own definitions. */ #define CLOSE_RANGE_UNSHARE CLOSE_RANGE_UNSHARE #define CLOSE_RANGE_CLOEXEC CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_EXCEPT CLOSE_RANGE_EXCEPT #endif /* _UAPI_LINUX_CLOSE_RANGE_H */ -- cgit v1.2.3 From 06074fbbf092b1a6292c108eef2b94f43333ea1c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 21 Sep 2026 16:15:37 +0200 Subject: file: add CLOSE_RANGE_CLOEXEC_ONLY CLOSE_RANGE_CLOEXEC marks a range close-on-exec. Nothing happens to these fds unless an exec happens. Add CLOSE_RANGE_CLOEXEC_ONLY which closes the close-on-exec file descriptors in the range. When combined with CLOSE_RANGE_EXCEPT it names the close-on-exec file descriptors that are supposed to survive. Every close-on-exec fd outside of the range is closed. File descriptors without that flag are left alone. Whatever the caller deliberately passes down, stdio, LISTEN_FDS, an inherited pipe, remains where it is. The handful of close-on-exec descriptors the caller still needs for the exec such as the executable, an error pipe, sit in a specific range. That is what a task between clone(CLONE_FILES) and execve() actually wants: clone(CLONE_FILES | CLONE_VM | CLONE_VFORK) child: close_range(lo, hi, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC_ONLY | CLOSE_RANGE_EXCEPT) child: rearrange descriptors in the now private table child: execve() Between the clone and the close_range() the child holds no reference of its own on any file because copy_files() only bumps the fdtable refcount. So a close() in the parent takes effect immediately. The unshare also never takes a reference on the fds it leaves behind either. The range is expressed in the parent's numbering. So a caller that cannot name the file descriptors it keeps contiguously picks a range wide enough to cover them, unshares, and tidies up with a second close_range() on the table it now owns alone. That one is cheap. To keep nothing, name a range that cannot hold an open descriptor, e.g. close_range(~0U, ~0U, ...). CLOSE_RANGE_CLOEXEC and CLOSE_RANGE_CLOEXEC_ONLY are mutually exclusive. Link: https://patch.msgid.link/20260921-work-file-close_range_except-v1-9-c20d0b49270d@kernel.org Reviewed-by: Jann Horn Signed-off-by: Christian Brauner (Amutable) --- fs/file.c | 30 +++++++++++++++++++++++++++--- include/uapi/linux/close_range.h | 5 +++++ 2 files changed, 32 insertions(+), 3 deletions(-) (limited to 'include/uapi/linux') diff --git a/fs/file.c b/fs/file.c index 8952efd2cf3a..37f0ba740c39 100644 --- a/fs/file.c +++ b/fs/file.c @@ -843,18 +843,29 @@ static inline void __range_cloexec(struct files_struct *cur_fds, spin_unlock(&cur_fds->file_lock); } +/* Next open descriptor in [fd, max_fd], or the next close-on-exec one. */ +static inline unsigned int next_open_fd(struct fdtable *fdt, unsigned int fd, + unsigned int max_fd, + struct fd_range *range) +{ + if (range->flags & FD_RANGE_CLOEXEC_ONLY) + return find_next_and_bit(fdt->open_fds, fdt->close_on_exec, + max_fd + 1, fd); + return find_next_bit(fdt->open_fds, max_fd + 1, fd); +} + /* Next open descriptor in [fd, max_fd] that @range selects. */ static inline unsigned int next_fd_to_close(struct fdtable *fdt, unsigned int fd, unsigned int max_fd, struct fd_range *range) { - fd = find_next_bit(fdt->open_fds, max_fd + 1, fd); + fd = next_open_fd(fdt, fd, max_fd, range); /* Hop over the window the range keeps. */ if ((range->flags & FD_RANGE_EXCEPT) && fd >= range->from && fd <= range->to) { if (range->to >= max_fd) return max_fd + 1; - fd = find_next_bit(fdt->open_fds, max_fd + 1, range->to + 1); + fd = next_open_fd(fdt, range->to + 1, max_fd, range); } return fd; } @@ -911,6 +922,12 @@ static inline void __range_close(struct files_struct *files, * With CLOSE_RANGE_EXCEPT the range names what to leave alone instead: * every open file descriptor outside of [@fd, @max_fd] is closed, or * marked close-on-exec with CLOSE_RANGE_CLOEXEC. + * + * With CLOSE_RANGE_CLOEXEC_ONLY only file descriptors that have + * close-on-exec set are closed. Together with CLOSE_RANGE_EXCEPT the + * range names the close-on-exec file descriptors to keep. To keep none + * of them, name a range that cannot hold an open file descriptor, e.g. + * close_range(~0U, ~0U, ...). */ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd, unsigned int, flags) @@ -920,7 +937,12 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd, struct fd_range range = {fd, max_fd}; if (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC | - CLOSE_RANGE_EXCEPT)) + CLOSE_RANGE_EXCEPT | CLOSE_RANGE_CLOEXEC_ONLY)) + return -EINVAL; + + /* One marks close-on-exec, the other closes what is marked. */ + if (hweight32(flags & (CLOSE_RANGE_CLOEXEC | + CLOSE_RANGE_CLOEXEC_ONLY)) > 1) return -EINVAL; if (fd > max_fd) @@ -928,6 +950,8 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd, if (flags & CLOSE_RANGE_EXCEPT) range.flags |= FD_RANGE_EXCEPT; + if (flags & CLOSE_RANGE_CLOEXEC_ONLY) + range.flags |= FD_RANGE_CLOEXEC_ONLY; if ((flags & CLOSE_RANGE_UNSHARE) && atomic_read(&cur_fds->count) > 1) { struct fd_range *drop = ⦥ diff --git a/include/uapi/linux/close_range.h b/include/uapi/linux/close_range.h index 39eddb3ab613..7da9ed95258a 100644 --- a/include/uapi/linux/close_range.h +++ b/include/uapi/linux/close_range.h @@ -9,6 +9,7 @@ #undef CLOSE_RANGE_UNSHARE #undef CLOSE_RANGE_CLOEXEC #undef CLOSE_RANGE_EXCEPT +#undef CLOSE_RANGE_CLOEXEC_ONLY enum close_range_flags { /* Unshare the file descriptor table before closing file descriptors. */ @@ -19,12 +20,16 @@ enum close_range_flags { /* Act on every file descriptor outside of the given range instead. */ CLOSE_RANGE_EXCEPT = (1U << 3), + + /* Only close file descriptors that have the FD_CLOEXEC bit set. */ + CLOSE_RANGE_CLOEXEC_ONLY = (1U << 4), }; /* Keep #ifdef working and let glibc skip its own definitions. */ #define CLOSE_RANGE_UNSHARE CLOSE_RANGE_UNSHARE #define CLOSE_RANGE_CLOEXEC CLOSE_RANGE_CLOEXEC #define CLOSE_RANGE_EXCEPT CLOSE_RANGE_EXCEPT +#define CLOSE_RANGE_CLOEXEC_ONLY CLOSE_RANGE_CLOEXEC_ONLY #endif /* _UAPI_LINUX_CLOSE_RANGE_H */ -- cgit v1.2.3