Skip to content

files,close_range: add CLOSE_RANGE_{CLOEXEC_ONLY,EXCEPT} - #2655

Open
vfsci-bot[bot] wants to merge 10 commits into
vfs.base.cifrom
pw/1170443/vfs.base.ci
Open

vfsci-bot[bot] wants to merge 10 commits into
vfs.base.cifrom
pw/1170443/vfs.base.ci

Conversation

@vfsci-bot

@vfsci-bot vfsci-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown

Series: https://patchwork.kernel.org/project/linux-fsdevel/list/?series=1170443
Submitter: Christian Brauner
Version: 1
Patches: 10/10
Message-ID: <20260921-work-file-close_range_except-v1-0-c20d0b49270d@kernel.org>
Base: vfs.base.ci
Lore: https://lore.kernel.org/linux-fsdevel/20260921-work-file-close_range_except-v1-0-c20d0b49270d@kernel.org


Automated by ml2pr

close_range(CLOSE_RANGE_UNSHARE) passed the range it is about to close
to dup_fd() so the clone is done without that range. This only works
when the last open descriptor falls into the range. A range in the
middle of the table is copied like everything else. That's wasteful.

Don't copy them. Descriptors in a skipped range stay open in the source
fdtable. They are never copied into the new table and no reference is
taken on them. Figuring out the size of the table follows the same rule.
That drops the special-case it has now.

This also means we stop calling ->flush() on fds from a table they
were never part of. With the range at the top of the table that was
already mostly the case.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Cover a range in the middle of a full word:

- the descriptors in it are gone from the clone and the ones around it
  are still there

- the slots are handed out again, the word is not left marked full

- the table the child cloned from is untouched

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
dup_fd() will be taught what to do with the range that was passed to it.
It won't always be dropped. Rename it to a plain "range" from
"punch_hole". close_range() keeps a pointer named drop for the one case
it has today.

No functional changes.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Add FD_RANGE_EXCEPT. It turns the meaning of range around. Instead of
indicating that the descriptors in the range are the ones that are left
out of the copy they indicate the range that makes it into the copy. All
other files are left behind. The clone only has to reach the last open
descriptor inside the range.

Nothing passes the flag yet.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Turn the close_range() flags into an enum. Makes debugging a lot easier.

No functional changes.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
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.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Cover the inverted range:

- with plain close the window stays and everything outside of it goes,
  stdio included

- a window at the top, one that cannot hold a descriptor and one of a
  single descriptor keep just what they name, and so does the unshare
  form on a table that is not shared

- with CLOSE_RANGE_CLOEXEC everything outside of the window is marked
  and the window is not, in place and in a clone, for a
  window in the middle, at the bottom, at the top and above the table

- with CLOSE_RANGE_UNSHARE the table the child cloned from is untouched,
  a window near the top of the table comes back whole, one at the bottom
  keeps stdio, one that cannot hold a descriptor keeps nothing, and the
  slots left behind are handed out again from the bottom

- the bounds are checked before the range is turned around

The extra cases came out of walking the window positions that
__range_close(), __range_cloexec() and dup_fd() tell apart: at the
bottom, in the middle, at the top, above the table, a single descriptor
and none, on a shared and on a private table.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
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.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
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.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Cover closing what is marked:

- close-on-exec descriptors in the range go, the ones outside stay, and
  a range above the table closes nothing

- with CLOSE_RANGE_EXCEPT it is the other way around, and the kept ones
  keep their flag, for a window in the middle, at the top and at the
  bottom

- descriptors without close-on-exec are never touched, in or out of the
  range

- a range that cannot hold an open descriptor keeps nothing, one that
  covers everything keeps everything, in place and in a clone

- the unshare form leaves the table it was cloned from alone, with and
  without CLOSE_RANGE_EXCEPT

- the unshare form keeps the descriptors without the flag that sit in
  the range it drops from, and hands the dropped slots out again

- a kept range above the last descriptor without close-on-exec still
  comes back, so the clone is sized off the range too

Also check that asking for CLOSE_RANGE_CLOEXEC at the same time is
refused, whatever else is asked for, and that the bounds are still
checked.

The extra cases came out of the same walk with the close-on-exec mask
on top: a marked and an unmarked descriptor on each side of every window
position, and the size of the clone when only unmarked ones are left in
the range it drops from.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Reviewed-by: Jann Horn <jannh@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant