Skip to content

Commit b0a1efd

Browse files
authored
Disable inheritance of file descriptors created by Swift Testing by default. (#1145)
This PR makes file descriptors created by Swift Testing `FD_CLOEXEC` by default (on Windows, `~HANDLE_FLAG_INHERIT`.) We then clear `FD_CLOEXEC` for specific file descriptors that should be inherited. On Darwin, this is effectively ignored because we use `POSIX_SPAWN_CLOEXEC_DEFAULT`, and on Windows we use `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` which has [much the same effect](https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873). (On non-Darwin POSIX platforms, there's no reliable way to ensure only one child process inherits a particular file descriptor.) This change is speculative. No additional unit tests are added because existing test coverage _should be_ sufficient; the reported issue is on a platform (Ubuntu 20.04) where we don't have any CI jobs. Resolves #1140. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 25b61ef commit b0a1efd

File tree

4 files changed

+161
-7
lines changed

4 files changed

+161
-7
lines changed

Sources/Testing/Attachments/Attachment.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ extension Attachment where AttachableValue: ~Copyable {
462462
// file exists at this path (note "x" in the mode string), an error will
463463
// be thrown and we'll try again by adding a suffix.
464464
let preferredPath = appendPathComponent(preferredName, to: directoryPath)
465-
file = try FileHandle(atPath: preferredPath, mode: "wxb")
465+
file = try FileHandle(atPath: preferredPath, mode: "wxeb")
466466
result = preferredPath
467467
} catch {
468468
// Split the extension(s) off the preferred name. The first component in
@@ -478,7 +478,7 @@ extension Attachment where AttachableValue: ~Copyable {
478478
// Propagate any error *except* EEXIST, which would indicate that the
479479
// name was already in use (so we should try again with a new suffix.)
480480
do {
481-
file = try FileHandle(atPath: preferredPath, mode: "wxb")
481+
file = try FileHandle(atPath: preferredPath, mode: "wxeb")
482482
result = preferredPath
483483
break
484484
} catch let error as CError where error.rawValue == swt_EEXIST() {

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,15 @@ extension ExitTest {
801801
childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable
802802
}
803803

804+
#if !SWT_TARGET_OS_APPLE
805+
// Set inherited those file handles that the child process needs. On
806+
// Darwin, this is a no-op because we use POSIX_SPAWN_CLOEXEC_DEFAULT.
807+
try stdoutWriteEnd?.setInherited(true)
808+
try stderrWriteEnd?.setInherited(true)
809+
try backChannelWriteEnd.setInherited(true)
810+
try capturedValuesReadEnd.setInherited(true)
811+
#endif
812+
804813
// Spawn the child process.
805814
let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in
806815
try withUnsafePointer(to: capturedValuesReadEnd) { capturedValuesReadEnd in

Sources/Testing/Support/FileHandle.swift

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ struct FileHandle: ~Copyable, Sendable {
7373
return
7474
}
7575

76+
// On Windows, "N" is used rather than "e" to signify that a file handle is
77+
// not inherited.
78+
var mode = mode
79+
if let eIndex = mode.firstIndex(of: "e") {
80+
mode.replaceSubrange(eIndex ... eIndex, with: "N")
81+
}
82+
7683
// Windows deprecates fopen() as insecure, so call _wfopen_s() instead.
7784
let fileHandle = try path.withCString(encodedAs: UTF16.self) { path in
7885
try mode.withCString(encodedAs: UTF16.self) { mode in
@@ -98,8 +105,13 @@ struct FileHandle: ~Copyable, Sendable {
98105
/// - path: The path to read from.
99106
///
100107
/// - Throws: Any error preventing the stream from being opened.
108+
///
109+
/// By default, the resulting file handle is not inherited by any child
110+
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
111+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call
112+
/// ``setInherited()``.
101113
init(forReadingAtPath path: String) throws {
102-
try self.init(atPath: path, mode: "rb")
114+
try self.init(atPath: path, mode: "reb")
103115
}
104116

105117
/// Initialize an instance of this type to write to the given path.
@@ -108,8 +120,13 @@ struct FileHandle: ~Copyable, Sendable {
108120
/// - path: The path to write to.
109121
///
110122
/// - Throws: Any error preventing the stream from being opened.
123+
///
124+
/// By default, the resulting file handle is not inherited by any child
125+
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
126+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call
127+
/// ``setInherited()``.
111128
init(forWritingAtPath path: String) throws {
112-
try self.init(atPath: path, mode: "wb")
129+
try self.init(atPath: path, mode: "web")
113130
}
114131

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

465+
#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING
466+
/// Create a pipe with flags.
467+
///
468+
/// This function declaration is provided because `pipe2()` is only declared if
469+
/// `_GNU_SOURCE` is set, but setting it causes build errors due to conflicts
470+
/// with Swift's Glibc module.
471+
private let _pipe2 = symbol(named: "pipe2").map {
472+
castCFunction(at: $0, to: (@convention(c) (UnsafeMutablePointer<CInt>, CInt) -> CInt).self)
473+
}
474+
#endif
475+
448476
extension FileHandle {
449477
/// Make a pipe connecting two new file handles.
450478
///
@@ -461,15 +489,37 @@ extension FileHandle {
461489
/// - Bug: This function should return a tuple containing the file handles
462490
/// instead of returning them via `inout` arguments. Swift does not support
463491
/// tuples with move-only elements. ([104669935](rdar://104669935))
492+
///
493+
/// By default, the resulting file handles are not inherited by any child
494+
/// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and
495+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make them inheritable,
496+
/// call ``setInherited()``.
464497
static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws {
498+
#if !os(Windows)
499+
var pipe2Called = false
500+
#endif
501+
465502
var (fdReadEnd, fdWriteEnd) = try withUnsafeTemporaryAllocation(of: CInt.self, capacity: 2) { fds in
466503
#if os(Windows)
467-
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY) else {
504+
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY | _O_NOINHERIT) else {
468505
throw CError(rawValue: swt_errno())
469506
}
470507
#else
471-
guard 0 == pipe(fds.baseAddress!) else {
472-
throw CError(rawValue: swt_errno())
508+
#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING
509+
if let _pipe2 {
510+
guard 0 == _pipe2(fds.baseAddress!, O_CLOEXEC) else {
511+
throw CError(rawValue: swt_errno())
512+
}
513+
pipe2Called = true
514+
}
515+
#endif
516+
517+
if !pipe2Called {
518+
// pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC
519+
// to the best of our ability.
520+
guard 0 == pipe(fds.baseAddress!) else {
521+
throw CError(rawValue: swt_errno())
522+
}
473523
}
474524
#endif
475525
return (fds[0], fds[1])
@@ -479,6 +529,15 @@ extension FileHandle {
479529
Self._close(fdWriteEnd)
480530
}
481531

532+
#if !os(Windows)
533+
if !pipe2Called {
534+
// pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC
535+
// to the best of our ability.
536+
try _setFileDescriptorInherited(fdReadEnd, false)
537+
try _setFileDescriptorInherited(fdWriteEnd, false)
538+
}
539+
#endif
540+
482541
do {
483542
defer {
484543
fdReadEnd = -1
@@ -553,6 +612,72 @@ extension FileHandle {
553612
#endif
554613
}
555614
#endif
615+
616+
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
617+
/// Set whether or not the given file descriptor is inherited by child processes.
618+
///
619+
/// - Parameters:
620+
/// - fd: The file descriptor.
621+
/// - inherited: Whether or not `fd` is inherited by child processes
622+
/// (ignoring overriding functionality such as Apple's
623+
/// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.)
624+
///
625+
/// - Throws: Any error that occurred while setting the flag.
626+
private static func _setFileDescriptorInherited(_ fd: CInt, _ inherited: Bool) throws {
627+
switch swt_getfdflags(fd) {
628+
case -1:
629+
// An error occurred reading the flags for this file descriptor.
630+
throw CError(rawValue: swt_errno())
631+
case let oldValue:
632+
let newValue = if inherited {
633+
oldValue & ~FD_CLOEXEC
634+
} else {
635+
oldValue | FD_CLOEXEC
636+
}
637+
if oldValue == newValue {
638+
// No need to make a second syscall as nothing has changed.
639+
return
640+
}
641+
if -1 == swt_setfdflags(fd, newValue) {
642+
// An error occurred setting the flags for this file descriptor.
643+
throw CError(rawValue: swt_errno())
644+
}
645+
}
646+
}
647+
#endif
648+
649+
/// Set whether or not this file handle is inherited by child processes.
650+
///
651+
/// - Parameters:
652+
/// - inherited: Whether or not this file handle is inherited by child
653+
/// processes (ignoring overriding functionality such as Apple's
654+
/// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.)
655+
///
656+
/// - Throws: Any error that occurred while setting the flag.
657+
func setInherited(_ inherited: Bool) throws {
658+
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
659+
try withUnsafePOSIXFileDescriptor { fd in
660+
guard let fd else {
661+
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")
662+
}
663+
try withLock {
664+
try Self._setFileDescriptorInherited(fd, inherited)
665+
}
666+
}
667+
#elseif os(Windows)
668+
return try withUnsafeWindowsHANDLE { handle in
669+
guard let handle else {
670+
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")
671+
}
672+
let newValue = inherited ? DWORD(HANDLE_FLAG_INHERIT) : 0
673+
guard SetHandleInformation(handle, DWORD(HANDLE_FLAG_INHERIT), newValue) else {
674+
throw Win32Error(rawValue: GetLastError())
675+
}
676+
}
677+
#else
678+
#warning("Platform-specific implementation missing: cannot set whether a file handle is inherited")
679+
#endif
680+
}
556681
}
557682

558683
// MARK: - General path utilities

Sources/_TestingInternals/include/Stubs.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,26 @@ static int swt_EEXIST(void) {
151151
return EEXIST;
152152
}
153153

154+
#if defined(F_GETFD)
155+
/// Call `fcntl(F_GETFD)`.
156+
///
157+
/// This function is provided because `fcntl()` is a variadic function and
158+
/// cannot be imported directly into Swift.
159+
static int swt_getfdflags(int fd) {
160+
return fcntl(fd, F_GETFD);
161+
}
162+
#endif
163+
164+
#if defined(F_SETFD)
165+
/// Call `fcntl(F_SETFD)`.
166+
///
167+
/// This function is provided because `fcntl()` is a variadic function and
168+
/// cannot be imported directly into Swift.
169+
static int swt_setfdflags(int fd, int flags) {
170+
return fcntl(fd, F_SETFD, flags);
171+
}
172+
#endif
173+
154174
SWT_ASSUME_NONNULL_END
155175

156176
#endif

0 commit comments

Comments
 (0)