A process preparing to execute another program often needs a simple boundary: descriptors 0, 1, and 2 remain available, while every higher descriptor must disappear. Repeating close() over a guessed numeric limit or enumerating /proc/self/fd turns that boundary into a userspace scan.
Linux close_range() expresses the interval directly. The kernel applies one operation to every open file descriptor from first through last, inclusive. With flags, the same interface can isolate a shared descriptor table or mark the interval close-on-exec instead of closing it immediately.
The interval is numeric, not an inventory
The basic form accepts two unsigned descriptor numbers:
if (close_range(3, ~0U, 0) == -1) {
perror("close_range");
}~0U supplies the largest value representable by unsigned int, so the call does not need the process’s current highest descriptor number. Descriptors that are not open do not need separate handling.
This differs from enumerating /proc/self/fd. Enumeration first constructs an inventory and then acts on individual entries. close_range() states the desired numeric boundary directly, without requiring /proc to be mounted or readable.
The call reports an error for invalid arguments such as first > last or unsupported flag bits. Errors from closing individual descriptors in the selected interval are not reported separately by the Linux interface.
Range cleanup narrows the inheritance surface
File descriptors normally survive execve() unless their close-on-exec flag is set. That property matters for launchers, service managers, shells, and other processes that assemble an execution environment for another program.
A cleanup immediately before execve() can retain the conventional standard streams and remove everything above them:
close_range(3, ~0U, 0);
execve(path, argv, envp);The operation does not decide which descriptors are semantically safe to inherit. The caller still chooses the boundary. Its value is that a contiguous policy can be represented without discovering each open descriptor first.
This is also distinct from closing the underlying open file description in every context. File descriptors are references held in a descriptor table; other processes or descriptors can still refer to the same open file description after one descriptor is removed.
CLOSE_RANGE_UNSHARE isolates the descriptor table first
Threads created with a shared file-descriptor table can observe changes made through that table. Closing a range while another execution context shares the table can therefore affect more than the calling thread.
CLOSE_RANGE_UNSHARE changes that boundary:
close_range(3, ~0U, CLOSE_RANGE_UNSHARE);For the selected operation, Linux unshares the caller’s descriptor table before applying the closure. The documented semantics are conceptually equivalent to:
unshare(CLONE_FILES);
close_range(first, last, 0);The combined operation can avoid work that a literal two-call sequence would perform. In the common case where last extends beyond the allocated descriptor table, the kernel can construct the unshared table up to first and avoid copying entries that are going to be discarded.
This flag addresses descriptor-table sharing. It does not make arbitrary activity in other threads safe, and it does not replace synchronization for application state outside that table.
CLOSE_RANGE_CLOEXEC defers the actual close
CLOSE_RANGE_CLOEXEC changes the operation from immediate removal to flagging:
close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);Descriptors in the interval remain usable in the current process, but they are marked close-on-exec. A later successful execve() closes them as part of the execution transition.
That separation matters when pre-exec setup still needs some of those descriptors. For example, security setup can occur after the descriptor interval is marked without forcing immediate closure first. The final descriptor removal then follows the normal close-on-exec semantics.
The flag does not guarantee that execve() will happen. If execution continues without a successful exec, the descriptors remain open in the process.
Descriptor creation races still depend on the chosen policy
A loop such as this has a visible sequence of individual operations:
for (int fd = 3; fd < limit; fd++) {
close(fd);
}Another thread sharing the table can allocate descriptors while that loop runs. Enumeration-based cleanup also separates discovery from later closure.
close_range() removes the per-descriptor userspace loop, but concurrency semantics still depend on flags and surrounding process design. CLOSE_RANGE_UNSHARE is the mechanism intended for isolating the caller from a shared descriptor table before the range is closed. Without that flag, the call operates on the table the caller currently shares.
The distinction is structural: atomic-looking source code is not itself a concurrency guarantee. The relevant kernel object is the descriptor table and whether it remains shared.
CLOEXEC and immediate closure solve different ordering problems
Immediate closure is appropriate when no later pre-exec step needs descriptors in the interval. Marking them with CLOSE_RANGE_CLOEXEC is useful when setup ordering requires the descriptors to remain available until the execution boundary.
These two sequences therefore express different policies:
close_range(3, ~0U, CLOSE_RANGE_UNSHARE);
execve(path, argv, envp);close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
/* additional setup */
execve(path, argv, envp);The first isolates and removes the range before exec. The second leaves the range open temporarily and asks the exec transition to remove it.
CLOSE_RANGE_UNSHARE and CLOSE_RANGE_CLOEXEC can also be combined on Linux. In that form, the descriptor table is unshared and the selected descriptors in the caller’s table are marked close-on-exec rather than immediately closed.
The boundary is deliberately coarse
A contiguous interval is a good fit when a process can reserve a small known set of descriptors and reject inheritance for everything else. It is less expressive when many sparse descriptors must survive.
That limitation is part of the API shape. close_range() does not accept an exclusion set. A launcher that must preserve descriptors 3, 9, and 27 needs a separate strategy, such as relocating preserved descriptors, arranging descriptor numbers before cleanup, or applying close-on-exec policy when descriptors are created.
The system call is therefore strongest when the process architecture has a clear descriptor boundary. It converts that boundary from an inventory-and-loop procedure into a kernel operation, while CLOSE_RANGE_UNSHARE and CLOSE_RANGE_CLOEXEC define whether table sharing and exec ordering are part of the same transition.