A thread preparing to cross an execve() boundary can need to remove every file descriptor above a small preserved set while other threads still share its descriptor table. Closing descriptors one by one creates a race: another thread can allocate a descriptor into the interval while cleanup is in progress. Linux close_range() with CLOSE_RANGE_UNSHARE changes the table-sharing boundary before applying the range operation.
This behavior matters because a file descriptor number is only an index into a process descriptor table. With CLONE_FILES, multiple tasks can refer to the same table, so a close performed through one task changes descriptor visibility for all tasks sharing it. CLOSE_RANGE_UNSHARE gives the calling task a private descriptor table as part of the operation.
Range closure replaces descriptor-by-descriptor enumeration
The system call has this interface:
int close_range(unsigned int first, unsigned int last, int flags);With flags equal to zero, Linux closes open descriptors whose numbers fall in the inclusive interval from first through last. A common process-launch boundary is:
close_range(3, ~0U, 0);That preserves standard input, output, and error while closing higher-numbered descriptors.
The range form avoids scanning /proc/self/fd and issuing a sequence of close() calls. It also avoids depending on a procfs mount merely to enumerate descriptors. Errors associated with closing individual descriptors are ignored by close_range(); failure of the system call itself is reported through its return value and errno.
A plain range close does not solve every concurrency case. If another task shares the descriptor table, both allocation and closure still operate on the same table.
UNSHARE moves cleanup onto a private table
CLOSE_RANGE_UNSHARE asks the kernel to unshare the caller’s descriptor table before closing the requested range. Conceptually, the operation corresponds to:
unshare(CLONE_FILES);
close_range(first, last, 0);The combined operation has a stronger race boundary than implementing those steps through a user-space loop. Once the caller has its private table, descriptor activity in tasks that retained the old shared table cannot repopulate the caller’s table.
For the common upper bound ~0U, Linux can optimize the operation. When the requested range extends beyond the currently allocated descriptor table, the kernel can create a private table containing only descriptors below first rather than copying a full table and then closing each higher entry. That is an implementation optimization, not a portable API property.
CLOSE_RANGE_UNSHARE is Linux-specific. close_range() appeared in Linux 5.9, while the API has no POSIX standard status. Code that depends on this boundary therefore needs a platform-specific path or an explicit fallback policy.
CLOEXEC changes timing instead of closing immediately
CLOSE_RANGE_CLOEXEC changes the operation from immediate closure to setting the close-on-exec flag across the interval:
close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);The descriptors remain usable before execve(), but successful execution of a new program image closes descriptors carrying the flag. This is useful when setup between range marking and execve() still requires descriptors that must not survive into the new image.
The flag also changes interactions with syscall filtering. A process can mark a broad range CLOEXEC before installing a seccomp policy, then let execve() enforce the descriptor boundary. That avoids requiring descriptor-closing syscalls after the filter is active.
CLOSE_RANGE_CLOEXEC and CLOSE_RANGE_UNSHARE address different properties. The former controls closure timing; the latter controls descriptor-table sharing. Their semantics should not be collapsed into a generic descriptor-cleanup concept.
Unsharing does not revoke references held elsewhere
Detaching the caller’s descriptor table does not invalidate file descriptions referenced by other descriptor tables. A task that retained the original shared table can continue using its descriptors. The operation changes which descriptor entries the caller can reach; it is not a revocation mechanism for kernel objects.
This distinction follows the normal Linux descriptor model. Closing an entry removes one descriptor reference. The underlying open file description remains alive while other references still exist, including duplicated descriptors or descriptors in another table.
For the same reason, CLOSE_RANGE_UNSHARE is a local isolation boundary. It is suited to preparing one task’s descriptor namespace for a transition such as execve(), not to forcing peer tasks to release resources.
The boundary belongs next to process-transition policy
Descriptor cleanup is often described as a loop over integers, but the difficult property is ownership of the table during that loop. close_range() expresses the affected set directly, and CLOSE_RANGE_UNSHARE moves that set onto a table no longer shared with peer tasks.
That combination makes the process-transition boundary explicit: preserve the descriptors below first, isolate the table when sharing is possible, and close or mark the remaining interval according to the required execve() semantics. The resulting guarantee is about the caller’s descriptor table, not global object lifetime.