[Pitch] SwiftSystem: dup3 and pipe2 cover APIs

SwiftSystem: dup3 and pipe2 cover APIs

Introduction

Swift System today provides FileDescriptor.duplicate(), FileDescriptor.duplicate(as:) and FileDescriptor.pipe() cover APIs for the POSIX dup, dup2, and pipe functions, respectively.

This proposal adds additional FileDescriptor overloads to cover new POSIX 2024 functions in this family of APIs.

Motivation

It is considered best practice to set the O_CLOEXEC (close-on-exec) bit on newly created file descriptors to prevent them from being inherited by subprocesses.

Some POSIX functions such as open provide a "flags" parameter allowing this the close-on-exec bit to be set atomically on the newly created file descriptor. However, others provide no such flags parameter, and require the caller to use fcntl to set the close-on-exec bit after the fact. This can lead to race conditions and security bugs where file descriptors can be inherited between calling the function creating the file descriptor, and calling fcntl.

POSIX 2024 corrects this deficiency for dup2 and pipe by introducing dup3 and pipe2 variants that allow the close-on-exec bit to be set atomically.

While it's also considered best practice for subprocess spawning code to close all open file descriptors in the newly created subprocess (swiftlang/swift-subprocess does this for example), there is no guarantee that a user of Swift System is using a mechanism which does so throughout their entire process, and might be using process spawning code they don't control. Therefore, adding the proposed overloads provides defense in depth.

Proposed solution

This proposal adds new FileDescriptor.duplicate(as:) and FileDescriptor.pipe() overloads to cover the dup3 and pipe2 functions.

Detailed design

FileDescriptor will add the following overloads and types:

struct FileDescriptor {
    /// Creates a unidirectional data channel, which can be used for interprocess communication.
    ///
    /// - Parameters:
    ///   - options: The behavior for creating the pipe.
    ///
    /// - Returns: The pair of file descriptors.
    ///
    /// The corresponding C function is `pipe2`.
    public static func pipe(options: PipeOptions) throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor)

    /// Duplicates this file descriptor and return the newly created copy.
    ///
    /// - Parameters:
    ///   - `target`: The desired target file descriptor.
    ///   - `options`: The behavior for creating the target file descriptor.
    ///   - retryOnInterrupt: Whether to retry the write operation
    ///      if it throws ``Errno/interrupted``. The default is `true`.
    ///      Pass `false` to try only once and throw an error upon interruption.
    /// - Returns: The new file descriptor.
    ///
    /// If the `target` descriptor is already in use, then it is first
    /// deallocated as if a close(2) call had been done first.
    ///
    /// File descriptors are merely references to some underlying system resource.
    /// The system does not distinguish between the original and the new file
    /// descriptor in any way. For example, read, write and seek operations on
    /// one of them also affect the logical file position in the other, and
    /// append mode, non-blocking I/O and asynchronous I/O options are shared
    /// between the references. If a separate pointer into the file is desired,
    /// a different object reference to the file must be obtained by issuing an
    /// additional call to `open`.
    ///
    /// However, each file descriptor maintains its own close-on-exec flag.
    ///
    ///
    /// The corresponding C function is `dup3`.
    public func duplicate(as target: FileDescriptor, options: DuplicateOptions, retryOnInterrupt: Bool = true) throws -> FileDescriptor
    
    /// Options that specify behavior for a newly-created pipe.
    public struct PipeOptions: OptionSet, Sendable, Hashable, Codable {
        /// The raw C options.
        public var rawValue: CInt

        /// Create a strongly-typed options value from raw C options.
        public init(rawValue: CInt)

        /// Indicates that all
        /// subsequent input and output operations on the pipe's file descriptors will be nonblocking.
        ///
        /// The corresponding C constant is `O_NONBLOCK`.
        public static var nonBlocking: OpenOptions

        /// Indicates that executing a program closes the file.
        ///
        /// Normally, file descriptors remain open
        /// across calls to the `exec(2)` family of functions.
        /// If you specify this option,
        /// the file descriptor is closed when replacing this process
        /// with another process.
        ///
        /// The state of the file
        /// descriptor flags can be inspected using `F_GETFD`,
        /// as described in the `fcntl(2)` man page.
        ///
        /// The corresponding C constant is `O_CLOEXEC`.
        public static var closeOnExec: OpenOptions

        /// Indicates that forking a program closes the file.
        ///
        /// Normally, file descriptors remain open
        /// across calls to the `fork(2)` function.
        /// If you specify this option,
        /// the file descriptor is closed when forking this process
        /// into another process.
        ///
        /// The state of the file
        /// descriptor flags can be inspected using `F_GETFD`,
        /// as described in the `fcntl(2)` man page.
        ///
        /// The corresponding C constant is `O_CLOFORK`.
        public static var closeOnFork: OpenOptions
    }
    
    /// Options that specify behavior for a duplicated file descriptor.
    public struct DuplicateOptions: OptionSet, Sendable, Hashable, Codable {
        /// The raw C options.
        public var rawValue: CInt

        /// Create a strongly-typed options value from raw C options.
        public init(rawValue: CInt)

        /// Indicates that executing a program closes the file.
        ///
        /// Normally, file descriptors remain open
        /// across calls to the `exec(2)` family of functions.
        /// If you specify this option,
        /// the file descriptor is closed when replacing this process
        /// with another process.
        ///
        /// The state of the file
        /// descriptor flags can be inspected using `F_GETFD`,
        /// as described in the `fcntl(2)` man page.
        ///
        /// The corresponding C constant is `O_CLOEXEC`.
        public static var closeOnExec: OpenOptions

        /// Indicates that forking a program closes the file.
        ///
        /// Normally, file descriptors remain open
        /// across calls to the `fork(2)` function.
        /// If you specify this option,
        /// the file descriptor is closed when forking this process
        /// into another process.
        ///
        /// The state of the file
        /// descriptor flags can be inspected using `F_GETFD`,
        /// as described in the `fcntl(2)` man page.
        ///
        /// The corresponding C constant is `O_CLOFORK`.
        public static var closeOnFork: OpenOptions
    }
}

These API additions are unavailable on Windows and Darwin, as the underlying dup3 and pipe2 APIs do not exist.

closeOnFork is unavailable on Linux and Android, as the underlying O_CLOFORK constant does not exist.

Source compatibility

This change is additive only.

5 Likes

Makes sense to me! Ship it!

2 Likes