file: let dup_fd() drop only close-on-exec descriptors

Add FD_RANGE_CLOEXEC_ONLY. When set dup_fd() leaves everything behind
except for fds that are close-on-exec. Without FD_RANGE_EXCEPT the
clone loses the close-on-exec descriptors in the range. With it the
clone keeps the range and loses the close-on-exec descriptors everywhere
else. Descriptors without the flag are carried over either way.

Nothing passes the flag yet.

Link: https://patch.msgid.link/20260921-work-file-close_range_except-v1-8-c20d0b49270d@kernel.org
Reviewed-by: Jann Horn <jannh@google.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner 2026-09-21 16:15:36 +02:00
parent 4ee7588bbb
commit 4587f99cc2
No known key found for this signature in database
GPG Key ID: 91C61BC06578DCA2
2 changed files with 37 additions and 9 deletions

View File

@ -365,7 +365,8 @@ static unsigned long fd_range_word(struct fd_range *range, unsigned int i)
}
/* Bits of word @i that dup_fd() leaves behind. */
static unsigned long dup_fd_dropped_word(unsigned int i, struct fd_range *range)
static unsigned long dup_fd_dropped_word(struct fdtable *fdt, unsigned int i,
struct fd_range *range)
{
unsigned long dropped;
@ -374,6 +375,8 @@ static unsigned long dup_fd_dropped_word(unsigned int i, struct fd_range *range)
dropped = fd_range_word(range, i);
if (range->flags & FD_RANGE_EXCEPT)
dropped = ~dropped;
if (range->flags & FD_RANGE_CLOEXEC_ONLY)
dropped &= fdt->close_on_exec[i];
return dropped;
}
@ -393,15 +396,37 @@ static unsigned int sane_fdtable_size(struct fdtable *fdt, struct fd_range *rang
if (last == fdt->max_fds)
return NR_OPEN_DEFAULT;
/* Only words up to the last open descriptor can hold a kept one. */
i = last / BITS_PER_LONG + 1;
while (i--) {
unsigned long dropped = dup_fd_dropped_word(i, range);
if (!range)
return ALIGN(last + 1, BITS_PER_LONG);
if (fdt->open_fds[i] & ~dropped)
return (i + 1) * BITS_PER_LONG;
if (range->flags & FD_RANGE_CLOEXEC_ONLY) {
/* The close-on-exec bits decide what is dropped, walk the words. */
i = last / BITS_PER_LONG + 1;
while (i--) {
unsigned long dropped = dup_fd_dropped_word(fdt, i, range);
if (fdt->open_fds[i] & ~dropped)
return (i + 1) * BITS_PER_LONG;
}
return NR_OPEN_DEFAULT;
}
return NR_OPEN_DEFAULT;
if (range->flags & FD_RANGE_EXCEPT) {
/* Only the range is carried over. */
if (last > range->to) {
last = find_last_bit(fdt->open_fds, range->to + 1);
if (last > range->to)
return NR_OPEN_DEFAULT;
}
if (last < range->from)
return NR_OPEN_DEFAULT;
} else if (last >= range->from && last <= range->to) {
/* The last open descriptor goes, the kept ones sit below the range. */
last = find_last_bit(fdt->open_fds, range->from);
if (last == range->from)
return NR_OPEN_DEFAULT;
}
return ALIGN(last + 1, BITS_PER_LONG);
}
/*
@ -487,7 +512,7 @@ struct files_struct *dup_fd(struct files_struct *oldf, struct fd_range *range)
struct file *f = rcu_dereference_raw(*old_fds++);
if (!(fd % BITS_PER_LONG))
dropped = dup_fd_dropped_word(fd / BITS_PER_LONG, range);
dropped = dup_fd_dropped_word(old_fdt, fd / BITS_PER_LONG, range);
if (f && !(dropped & BIT_MASK(fd))) {
get_file(f);
} else {

View File

@ -104,6 +104,9 @@ int unshare_files(void);
enum fd_range_flags {
/* Leave behind all descriptors outside of the specified range. */
FD_RANGE_EXCEPT = (1U << 0),
/* Only select descriptors that have close-on-exec set. */
FD_RANGE_CLOEXEC_ONLY = (1U << 1),
};
struct fd_range {