Skip to content

Use posix_spawn_file_actions_adddup2() to clear FD_CLOEXEC. #1147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 10, 2025
9 changes: 0 additions & 9 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -801,15 +801,6 @@ extension ExitTest {
childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable
}

#if !SWT_TARGET_OS_APPLE
// Set inherited those file handles that the child process needs. On
// Darwin, this is a no-op because we use POSIX_SPAWN_CLOEXEC_DEFAULT.
try stdoutWriteEnd?.setInherited(true)
try stderrWriteEnd?.setInherited(true)
try backChannelWriteEnd.setInherited(true)
try capturedValuesReadEnd.setInherited(true)
#endif

// Spawn the child process.
let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in
try withUnsafePointer(to: capturedValuesReadEnd) { capturedValuesReadEnd in
Expand Down
50 changes: 35 additions & 15 deletions Sources/Testing/ExitTests/SpawnProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,27 @@ func spawnExecutable(
guard let fd else {
throw SystemError(description: "A child process cannot inherit a file handle without an associated file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
}
if let standardFD {
if let standardFD, standardFD != fd {
_ = posix_spawn_file_actions_adddup2(fileActions, fd, standardFD)
} else {
#if SWT_TARGET_OS_APPLE
_ = posix_spawn_file_actions_addinherit_np(fileActions, fd)
#else
// posix_spawn_file_actions_adddup2() will automatically clear
// FD_CLOEXEC after forking but before execing even if the old and
// new file descriptors are equal. This behavior is supported by
// Glibc ≥ 2.29, FreeBSD, OpenBSD, and Android (Bionic) and is
// standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn_file_actions_adddup2.html
// and https://www.austingroupbugs.net/view.php?id=411).
_ = posix_spawn_file_actions_adddup2(fileActions, fd, fd)
#if canImport(Glibc)
if _slowPath(glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29)) {
// This system is using an older version of glibc that does not
// implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(),
// so we must clear it here in the parent process.
try setFD_CLOEXEC(false, onFileDescriptor: fd)
}
#endif
#endif
highestFD = max(highestFD, fd)
}
Expand Down Expand Up @@ -156,8 +172,6 @@ func spawnExecutable(
#if !SWT_NO_DYNAMIC_LINKING
// This platform doesn't have POSIX_SPAWN_CLOEXEC_DEFAULT, but we can at
// least close all file descriptors higher than the highest inherited one.
// We are assuming here that the caller didn't set FD_CLOEXEC on any of
// these file descriptors.
_ = _posix_spawn_file_actions_addclosefrom_np?(fileActions, highestFD + 1)
#endif
#elseif os(FreeBSD)
Expand Down Expand Up @@ -216,36 +230,42 @@ func spawnExecutable(
}
#elseif os(Windows)
return try _withStartupInfoEx(attributeCount: 1) { startupInfo in
func inherit(_ fileHandle: borrowing FileHandle, as outWindowsHANDLE: inout HANDLE?) throws {
func inherit(_ fileHandle: borrowing FileHandle) throws -> HANDLE? {
try fileHandle.withUnsafeWindowsHANDLE { windowsHANDLE in
guard let windowsHANDLE else {
throw SystemError(description: "A child process cannot inherit a file handle without an associated Windows handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
}
outWindowsHANDLE = windowsHANDLE

// Ensure the file handle can be inherited by the child process.
guard SetHandleInformation(windowsHANDLE, DWORD(HANDLE_FLAG_INHERIT), DWORD(HANDLE_FLAG_INHERIT)) else {
throw Win32Error(rawValue: GetLastError())
}

return windowsHANDLE
}
}
func inherit(_ fileHandle: borrowing FileHandle?, as outWindowsHANDLE: inout HANDLE?) throws {
func inherit(_ fileHandle: borrowing FileHandle?) throws -> HANDLE? {
if fileHandle != nil {
try inherit(fileHandle!, as: &outWindowsHANDLE)
return try inherit(fileHandle!)
} else {
outWindowsHANDLE = nil
return nil
}
}

// Forward standard I/O streams.
try inherit(standardInput, as: &startupInfo.pointee.StartupInfo.hStdInput)
try inherit(standardOutput, as: &startupInfo.pointee.StartupInfo.hStdOutput)
try inherit(standardError, as: &startupInfo.pointee.StartupInfo.hStdError)
startupInfo.pointee.StartupInfo.hStdInput = try inherit(standardInput)
startupInfo.pointee.StartupInfo.hStdOutput = try inherit(standardOutput)
startupInfo.pointee.StartupInfo.hStdError = try inherit(standardError)
startupInfo.pointee.StartupInfo.dwFlags |= STARTF_USESTDHANDLES

// Ensure standard I/O streams and any explicitly added file handles are
// inherited by the child process.
var inheritedHandles = [HANDLE?](repeating: nil, count: additionalFileHandles.count + 3)
try inherit(standardInput, as: &inheritedHandles[0])
try inherit(standardOutput, as: &inheritedHandles[1])
try inherit(standardError, as: &inheritedHandles[2])
inheritedHandles[0] = startupInfo.pointee.StartupInfo.hStdInput
inheritedHandles[1] = startupInfo.pointee.StartupInfo.hStdOutput
inheritedHandles[2] = startupInfo.pointee.StartupInfo.hStdError
for i in 0 ..< additionalFileHandles.count {
try inherit(additionalFileHandles[i].pointee, as: &inheritedHandles[i + 3])
inheritedHandles[i + 3] = try inherit(additionalFileHandles[i].pointee)
}
inheritedHandles = inheritedHandles.compactMap(\.self)

Expand Down
110 changes: 36 additions & 74 deletions Sources/Testing/Support/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ struct FileHandle: ~Copyable, Sendable {
///
/// By default, the resulting file handle is not inherited by any child
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call
/// ``setInherited()``.
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
init(forReadingAtPath path: String) throws {
try self.init(atPath: path, mode: "reb")
}
Expand All @@ -123,8 +122,7 @@ struct FileHandle: ~Copyable, Sendable {
///
/// By default, the resulting file handle is not inherited by any child
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call
/// ``setInherited()``.
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
init(forWritingAtPath path: String) throws {
try self.init(atPath: path, mode: "web")
}
Expand Down Expand Up @@ -492,8 +490,7 @@ extension FileHandle {
///
/// By default, the resulting file handles are not inherited by any child
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make them inheritable,
/// call ``setInherited()``.
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws {
#if !os(Windows)
var pipe2Called = false
Expand Down Expand Up @@ -533,8 +530,8 @@ extension FileHandle {
if !pipe2Called {
// pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC
// to the best of our ability.
try _setFileDescriptorInherited(fdReadEnd, false)
try _setFileDescriptorInherited(fdWriteEnd, false)
try setFD_CLOEXEC(true, onFileDescriptor: fdReadEnd)
try setFD_CLOEXEC(true, onFileDescriptor: fdWriteEnd)
}
#endif

Expand Down Expand Up @@ -612,72 +609,6 @@ extension FileHandle {
#endif
}
#endif

#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
/// Set whether or not the given file descriptor is inherited by child processes.
///
/// - Parameters:
/// - fd: The file descriptor.
/// - inherited: Whether or not `fd` is inherited by child processes
/// (ignoring overriding functionality such as Apple's
/// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.)
///
/// - Throws: Any error that occurred while setting the flag.
private static func _setFileDescriptorInherited(_ fd: CInt, _ inherited: Bool) throws {
switch swt_getfdflags(fd) {
case -1:
// An error occurred reading the flags for this file descriptor.
throw CError(rawValue: swt_errno())
case let oldValue:
let newValue = if inherited {
oldValue & ~FD_CLOEXEC
} else {
oldValue | FD_CLOEXEC
}
if oldValue == newValue {
// No need to make a second syscall as nothing has changed.
return
}
if -1 == swt_setfdflags(fd, newValue) {
// An error occurred setting the flags for this file descriptor.
throw CError(rawValue: swt_errno())
}
}
}
#endif

/// Set whether or not this file handle is inherited by child processes.
///
/// - Parameters:
/// - inherited: Whether or not this file handle is inherited by child
/// processes (ignoring overriding functionality such as Apple's
/// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.)
///
/// - Throws: Any error that occurred while setting the flag.
func setInherited(_ inherited: Bool) throws {
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
try withUnsafePOSIXFileDescriptor { fd in
guard let fd else {
throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
}
try withLock {
try Self._setFileDescriptorInherited(fd, inherited)
}
}
#elseif os(Windows)
return try withUnsafeWindowsHANDLE { handle in
guard let handle else {
throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a Windows file handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
}
let newValue = inherited ? DWORD(HANDLE_FLAG_INHERIT) : 0
guard SetHandleInformation(handle, DWORD(HANDLE_FLAG_INHERIT), newValue) else {
throw Win32Error(rawValue: GetLastError())
}
}
#else
#warning("Platform-specific implementation missing: cannot set whether a file handle is inherited")
#endif
}
}

// MARK: - General path utilities
Expand Down Expand Up @@ -757,4 +688,35 @@ func canonicalizePath(_ path: String) -> String? {
return nil
#endif
}

#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
/// Set the given file descriptor's `FD_CLOEXEC` flag.
///
/// - Parameters:
/// - flag: The new value of `fd`'s `FD_CLOEXEC` flag.
/// - fd: The file descriptor.
///
/// - Throws: Any error that occurred while setting the flag.
func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws {
switch swt_getfdflags(fd) {
case -1:
// An error occurred reading the flags for this file descriptor.
throw CError(rawValue: swt_errno())
case let oldValue:
let newValue = if flag {
oldValue & ~FD_CLOEXEC
} else {
oldValue | FD_CLOEXEC
}
if oldValue == newValue {
// No need to make a second syscall as nothing has changed.
return
}
if -1 == swt_setfdflags(fd, newValue) {
// An error occurred setting the flags for this file descriptor.
throw CError(rawValue: swt_errno())
}
}
}
#endif
#endif
24 changes: 24 additions & 0 deletions Sources/Testing/Support/Versions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,30 @@ let swiftStandardLibraryVersion: String = {
return "unknown"
}()

#if canImport(Glibc)
/// The (runtime, not compile-time) version of glibc in use on this system.
///
/// This value is not part of the public interface of the testing library.
let glibcVersion: (major: Int, minor: Int) = {
// Default to the statically available version number if the function call
// fails for some reason.
var major = Int(clamping: __GLIBC__)
var minor = Int(clamping: __GLIBC_MINOR__)

if let strVersion = gnu_get_libc_version() {
withUnsafeMutablePointer(to: &major) { major in
withUnsafeMutablePointer(to: &minor) { minor in
withVaList([major, minor]) { args in
_ = vsscanf(strVersion, "%zd.%zd", args)
}
}
}
}

return (major, minor)
}()
#endif

// MARK: - sysctlbyname() Wrapper

#if !SWT_NO_SYSCTL && SWT_TARGET_OS_APPLE
Expand Down
4 changes: 4 additions & 0 deletions Sources/_TestingInternals/include/Includes.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
#include <sys/fcntl.h>
#endif

#if __has_include(<gnu/libc-version.h>)
#include <gnu/libc-version.h>
#endif

#if __has_include(<sys/resource.h>) && !defined(__wasi__)
#include <sys/resource.h>
#endif
Expand Down