A process preparing for execve() often needs a simple invariant: descriptors above a small allowlist must not survive into the new program. Closing descriptor numbers one by one turns that invariant into an enumeration problem. Linux close_range() expresses it directly as an operation over an inclusive interval of the calling task’s file descriptor table.

The interface is Linux-specific. Its behavior belongs to Linux file-table and system-call semantics, not to the C language or a portable POSIX guarantee.

The range is numeric, not object-based

The basic call closes open descriptors whose numbers fall between first and last, inclusive:

if (close_range(3, ~0U, 0) == -1)
    /* handle the call-level error */;

Using ~0U for last avoids deriving a current maximum descriptor number. Closed slots inside the interval require no special handling. The kernel currently ignores errors associated with closing individual descriptors; the return value reports errors of the range operation itself.

That contract differs from iterating through /proc/self/fd. Directory enumeration creates its own descriptor, observes a changing view when other threads can alter the table, and depends on procfs being available. close_range() does not require userspace to construct a snapshot of open descriptor numbers.

It also differs from a loop bounded by RLIMIT_NOFILE. A resource limit is not a list of currently open descriptors, and policy expressed as “everything from 3 upward” does not require discovery of each occupied slot.

CLOSE_RANGE_CLOEXEC defers destruction to exec

With CLOSE_RANGE_CLOEXEC, the call sets close-on-exec on descriptors in the interval instead of closing them immediately. The descriptors remain usable by the current program until a successful execve() closes them.

if (close_range(3, ~0U, CLOSE_RANGE_CLOEXEC) == -1)
    /* handle error */;

/* setup that may still use those descriptors */

execve(path, argv, envp);

This separates two events that are often conflated: declaring which descriptors may cross an exec boundary and destroying descriptors in the current image. If execve() fails, descriptors marked close-on-exec remain open because no successful exec transition occurred.

The flag has practical value when pre-exec setup still needs descriptors. A program can establish inheritance policy first, perform later setup, and let successful exec apply the closure. This is descriptor policy, not a guarantee that arbitrary setup code cannot change flags again.

Shared descriptor tables create a concurrency boundary

Threads normally share a file descriptor table. Closing a descriptor from one thread can therefore affect operations in another thread. A pre-exec cleanup sequence that mutates the shared table may conflict with concurrent work.

CLOSE_RANGE_UNSHARE changes that boundary. Linux first unshares the descriptor table for the calling task and then applies the range operation. Conceptually, this corresponds to separating the caller’s descriptor table from other tasks that share it before cleanup.

if (close_range(3, ~0U, CLOSE_RANGE_UNSHARE) == -1)
    /* handle error */;

The guarantee is scoped to the descriptor table. It does not suspend other threads, serialize application state, or make the entire pre-exec sequence atomic. Other shared resources remain shared according to their own rules.

Linux can optimize this case when the upper bound extends beyond the allocated descriptor table: the new table can be constructed only for the prefix that must remain. Code should rely on the specified effects, not on a particular internal copying strategy.

Unshare and close-on-exec can express separate policies

The flags address different concerns. CLOSE_RANGE_CLOEXEC controls what happens to selected descriptors at successful exec. CLOSE_RANGE_UNSHARE controls whether the caller first separates its descriptor table from tasks that currently share it.

A design that needs both concerns can combine supported flags. The resulting semantics still require attention to later descriptor creation. A descriptor opened after the range operation is not retroactively covered merely because its number lies inside the earlier interval.

That temporal boundary matters in multithreaded or callback-heavy pre-exec code. Range cleanup establishes a property of the table at the call; it is not a permanent rule for future descriptor allocations.

Close-on-exec remains a per-descriptor flag

FD_CLOEXEC is associated with a file descriptor, not with the underlying open file description. Duplicated descriptors can therefore carry different close-on-exec state. close_range(..., CLOSE_RANGE_CLOEXEC) applies the flag to descriptor entries in the chosen numeric interval.

This distinction prevents an incorrect inference that marking one descriptor changes every duplicate referring to the same open file description. The range operation is table-oriented: it selects entries by descriptor number.

The same boundary explains its role before exec. The kernel evaluates close-on-exec state on descriptor entries during a successful exec transition. Open-file state shared through duplicates has separate lifetime semantics.

A range operation narrows the inheritance contract

Descriptor inheritance bugs often arise from an implicit contract: every descriptor not deliberately closed may cross into a new program. close_range() permits the contract to be stated in the opposite direction. A small low-numbered set can remain, while an interval containing all higher numbers is closed or marked close-on-exec.

That still does not replace careful descriptor creation. APIs such as open() with O_CLOEXEC and descriptor-creating calls with equivalent atomic flags remain important because they establish inheritance state at creation time, avoiding a separate flag-setting interval.

The two mechanisms operate at different boundaries. Atomic close-on-exec creation controls a new descriptor as it enters the table. close_range() applies policy across an existing interval, optionally after separating a shared table. Keeping those boundaries distinct produces a precise exec contract without treating descriptor enumeration as durable process state.