Skip to content

Disable inheritance of file descriptors created by Swift Testing by default. #1145

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 6 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/Testing/Attachments/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ extension Attachment where AttachableValue: ~Copyable {
// file exists at this path (note "x" in the mode string), an error will
// be thrown and we'll try again by adding a suffix.
let preferredPath = appendPathComponent(preferredName, to: directoryPath)
file = try FileHandle(atPath: preferredPath, mode: "wxb")
file = try FileHandle(atPath: preferredPath, mode: "wxeb")
result = preferredPath
} catch {
// Split the extension(s) off the preferred name. The first component in
Expand All @@ -478,7 +478,7 @@ extension Attachment where AttachableValue: ~Copyable {
// Propagate any error *except* EEXIST, which would indicate that the
// name was already in use (so we should try again with a new suffix.)
do {
file = try FileHandle(atPath: preferredPath, mode: "wxb")
file = try FileHandle(atPath: preferredPath, mode: "wxeb")
result = preferredPath
break
} catch let error as CError where error.rawValue == swt_EEXIST() {
Expand Down
9 changes: 9 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,15 @@ 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
135 changes: 130 additions & 5 deletions Sources/Testing/Support/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ struct FileHandle: ~Copyable, Sendable {
return
}

// On Windows, "N" is used rather than "e" to signify that a file handle is
// not inherited.
var mode = mode
if let eIndex = mode.firstIndex(of: "e") {
mode.replaceSubrange(eIndex ... eIndex, with: "N")
}

// Windows deprecates fopen() as insecure, so call _wfopen_s() instead.
let fileHandle = try path.withCString(encodedAs: UTF16.self) { path in
try mode.withCString(encodedAs: UTF16.self) { mode in
Expand All @@ -98,8 +105,13 @@ struct FileHandle: ~Copyable, Sendable {
/// - path: The path to read from.
///
/// - Throws: Any error preventing the stream from being opened.
///
/// 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()``.
init(forReadingAtPath path: String) throws {
try self.init(atPath: path, mode: "rb")
try self.init(atPath: path, mode: "reb")
}

/// Initialize an instance of this type to write to the given path.
Expand All @@ -108,8 +120,13 @@ struct FileHandle: ~Copyable, Sendable {
/// - path: The path to write to.
///
/// - Throws: Any error preventing the stream from being opened.
///
/// 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()``.
init(forWritingAtPath path: String) throws {
try self.init(atPath: path, mode: "wb")
try self.init(atPath: path, mode: "web")
}

/// Initialize an instance of this type with an existing C file handle.
Expand Down Expand Up @@ -445,6 +462,17 @@ extension FileHandle {
#if !SWT_NO_PIPES
// MARK: - Pipes

#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING
/// Create a pipe with flags.
///
/// This function declaration is provided because `pipe2()` is only declared if
/// `_GNU_SOURCE` is set, but setting it causes build errors due to conflicts
/// with Swift's Glibc module.
private let _pipe2 = symbol(named: "pipe2").map {
castCFunction(at: $0, to: (@convention(c) (UnsafeMutablePointer<CInt>, CInt) -> CInt).self)
}
#endif

extension FileHandle {
/// Make a pipe connecting two new file handles.
///
Expand All @@ -461,15 +489,37 @@ extension FileHandle {
/// - Bug: This function should return a tuple containing the file handles
/// instead of returning them via `inout` arguments. Swift does not support
/// tuples with move-only elements. ([104669935](rdar://104669935))
///
/// 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()``.
static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws {
#if !os(Windows)
var pipe2Called = false
#endif

var (fdReadEnd, fdWriteEnd) = try withUnsafeTemporaryAllocation(of: CInt.self, capacity: 2) { fds in
#if os(Windows)
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY) else {
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY | _O_NOINHERIT) else {
throw CError(rawValue: swt_errno())
}
#else
guard 0 == pipe(fds.baseAddress!) else {
throw CError(rawValue: swt_errno())
#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING
if let _pipe2 {
guard 0 == _pipe2(fds.baseAddress!, O_CLOEXEC) else {
throw CError(rawValue: swt_errno())
}
pipe2Called = true
}
#endif

if !pipe2Called {
// pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC
// to the best of our ability.
guard 0 == pipe(fds.baseAddress!) else {
throw CError(rawValue: swt_errno())
}
}
#endif
return (fds[0], fds[1])
Expand All @@ -479,6 +529,15 @@ extension FileHandle {
Self._close(fdWriteEnd)
}

#if !os(Windows)
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)
}
#endif

do {
defer {
fdReadEnd = -1
Expand Down Expand Up @@ -553,6 +612,72 @@ 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
20 changes: 20 additions & 0 deletions Sources/_TestingInternals/include/Stubs.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,26 @@ static int swt_EEXIST(void) {
return EEXIST;
}

#if defined(F_GETFD)
/// Call `fcntl(F_GETFD)`.
///
/// This function is provided because `fcntl()` is a variadic function and
/// cannot be imported directly into Swift.
static int swt_getfdflags(int fd) {
return fcntl(fd, F_GETFD);
}
#endif

#if defined(F_SETFD)
/// Call `fcntl(F_SETFD)`.
///
/// This function is provided because `fcntl()` is a variadic function and
/// cannot be imported directly into Swift.
static int swt_setfdflags(int fd, int flags) {
return fcntl(fd, F_SETFD, flags);
}
#endif

SWT_ASSUME_NONNULL_END

#endif