January 20, 2018·4 min read·engineeringperlthreadsio

Perl 5.27.8 closes the descriptor inheritance race

Perl 5.27.8 moves close-on-exec into descriptor creation where supported, removing a race between opening files and executing another program.

A file descriptor can escape into an executed program even when the code that opened it intends to keep it private. Setting its close-on-exec flag after creation leaves an interval in which another thread can execute a program and pass the descriptor along. The Perl 5.27.8 release notes describe a change that removes this interval where the operating system supports atomic creation with the flag set. The release also sets the flag on descriptors that previously missed it.

This development release is dated January 20 in its own history document. Its significance lies in the relationship between Perl operations and process-wide resources. An apparently local file operation participates in the behaviour of every concurrent execution of another program. Correcting that relationship requires work below the language syntax.

I still favour Perl for short text-processing work. A change like this matters because a small program can use facilities whose correctness depends on the interpreter, even when its own code contains no complicated resource management. The convenience of the language depends partly on these less visible implementation decisions.

The interval between opening and protecting

The race described in the release notes requires two operations to overlap. One thread opens a descriptor and then sets its close-on-exec flag. Another thread initiates an exec during the interval between those steps. The executed program can receive the descriptor before the first thread has applied the intended policy. Setting the flag afterwards does not undo that transfer.

Moving the flag into creation changes the sequence. The descriptor exists with the intended close-on-exec status already attached. There is no separate protection step for another thread to overtake. This is the particular thread-safety improvement the release promises. It does not imply that every use of a filehandle becomes safe to share between threads.

The tagged implementation in doio.c shows several ways Perl requests that behaviour. File opening uses O_CLOEXEC when available. Pipe creation can use pipe2 with the same flag. Socket creation has a corresponding SOCK_CLOEXEC path. Descriptor duplication also has dedicated paths where the platform supplies the necessary operation.

These are distinct entry points into descriptor creation. Improving only ordinary file opening would leave other ways to obtain descriptors with the original two-step behaviour. The implementation applies a shared strategy to several operations, including temporary-file creation and duplication. That scope explains why the release notes discuss descriptors generally instead of limiting the claim to Perl filehandles opened with a particular spelling.

Support depends on the running platform

The implementation does more than check whether a constant exists at compile time. Its shared macro begins with an experimental strategy. After a successful atomic opening attempt, it checks the resulting descriptor flags with fcntl and F_GETFD. A descriptor with FD_CLOEXEC set allows the code to remember the atomic strategy for subsequent calls through that path.

If the attempted operation fails with EINVAL or ENOSYS, the code tries the normal operation and sets the flag afterwards when that succeeds. It also falls back when the resulting descriptor does not have the expected flag. Platforms without the necessary compile-time facilities use the separate flag-setting path directly. Those branches preserve the ability to open a resource on systems that cannot provide the stronger guarantee.

This distinction affects how to read the announcement. Perl generally requests atomic close-on-exec where supported, but the release does not promise that every operating system can perform it. The fallback improves flag-setting coverage while retaining the interval between creation and flag assignment. Applications on such systems still lack the atomic guarantee described for the supported path.

The source also identifies limits in the interpreter interfaces. Some atomic operations are unavailable on PERL_IMPLICIT_SYS builds because the corresponding interface structures do not cover the required calls. This is an implementation boundary, separate from whether an operating system has the relevant function. Portability code must account for both the system and the interface through which Perl reaches it.

Existing inheritance policy still matters

Perl already documents descriptor inheritance through $^F. The variable documentation shipped with this release defines it as the maximum system file descriptor, ordinarily 2. System descriptors pass to executed processes, while higher descriptors do not. It also states that close-on-exec status follows the value at the time the corresponding file, pipe, or socket opens.

Changing $^F just before exec therefore does not retroactively apply a new policy to all existing handles. The decision belongs to resource creation. The new atomic paths make that timing more reliable under concurrency, while the documented threshold still describes which descriptors should remain inheritable.

The source reflects that distinction with helpers that set close-on-exec for non-system descriptors and helpers that restore inheritance for system descriptors. Atomic creation cannot simply make every descriptor permanently private. Perl has to preserve its established inheritance rules as it adopts the safer opening mechanism.

I regard this as a useful form of compatibility work. The language keeps the documented control over inheritance while improving how the interpreter enforces it. For a tool frequently used to connect other programs, changing that enforcement without silently discarding the existing policy is a substantial part of the engineering.

There are consequently two improvements to distinguish in 5.27.8. Atomic creation removes a concurrency interval on supported paths. More reliable flag assignment covers descriptors that previously escaped the intended policy, including paths that still need a separate assignment. The tagged source makes both changes visible, and the platform-specific fallback explains why their guarantees differ.