Skip to content

Commit 3278217

Browse files
authored
Use posix_spawn_file_actions_adddup2() to clear FD_CLOEXEC. (#1147)
On [FreeBSD](https://man.freebsd.org/cgi/man.cgi?query=posix_spawn_file_actions_adddup2), [OpenBSD](https://github.com/openbsd/src/blob/master/lib/libc/gen/posix_spawn.c#L155), [Android](https://android.googlesource.com/platform/bionic/+/master/libc/bionic/spawn.cpp#103), and [Glibc ≥ 2.29](https://sourceware.org/bugzilla/show_bug.cgi?id=23640), `posix_spawn_file_actions_adddup2()` automatically clears `FD_CLOEXEC` if the file descriptors passed to it are equal. Relying on this behaviour eliminates a race condition when spawning child processes. This functionality is standardized in [POSIX.1-2024](https://pubs.opengroup.org/onlinepubs/9799919799/) thanks to [Austin Group Defect #411](https://www.austingroupbugs.net/view.php?id=411). Some older Linuxes (Amazon Linux 2 in particular) don't have this functionality, so we do a runtime check of the Glibc version. This PR is a follow-up to #1145. 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 c0108ad commit 3278217

File tree

5 files changed

+99
-98
lines changed

5 files changed

+99
-98
lines changed

Sources/Testing/ExitTests/ExitTest.swift

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

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

Sources/Testing/ExitTests/SpawnProcess.swift

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,27 @@ func spawnExecutable(
123123
guard let fd else {
124124
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")
125125
}
126-
if let standardFD {
126+
if let standardFD, standardFD != fd {
127127
_ = posix_spawn_file_actions_adddup2(fileActions, fd, standardFD)
128128
} else {
129129
#if SWT_TARGET_OS_APPLE
130130
_ = posix_spawn_file_actions_addinherit_np(fileActions, fd)
131+
#else
132+
// posix_spawn_file_actions_adddup2() will automatically clear
133+
// FD_CLOEXEC after forking but before execing even if the old and
134+
// new file descriptors are equal. This behavior is supported by
135+
// Glibc ≥ 2.29, FreeBSD, OpenBSD, and Android (Bionic) and is
136+
// standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn_file_actions_adddup2.html
137+
// and https://www.austingroupbugs.net/view.php?id=411).
138+
_ = posix_spawn_file_actions_adddup2(fileActions, fd, fd)
139+
#if canImport(Glibc)
140+
if _slowPath(glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29)) {
141+
// This system is using an older version of glibc that does not
142+
// implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(),
143+
// so we must clear it here in the parent process.
144+
try setFD_CLOEXEC(false, onFileDescriptor: fd)
145+
}
146+
#endif
131147
#endif
132148
highestFD = max(highestFD, fd)
133149
}
@@ -156,8 +172,6 @@ func spawnExecutable(
156172
#if !SWT_NO_DYNAMIC_LINKING
157173
// This platform doesn't have POSIX_SPAWN_CLOEXEC_DEFAULT, but we can at
158174
// least close all file descriptors higher than the highest inherited one.
159-
// We are assuming here that the caller didn't set FD_CLOEXEC on any of
160-
// these file descriptors.
161175
_ = _posix_spawn_file_actions_addclosefrom_np?(fileActions, highestFD + 1)
162176
#endif
163177
#elseif os(FreeBSD)
@@ -216,36 +230,42 @@ func spawnExecutable(
216230
}
217231
#elseif os(Windows)
218232
return try _withStartupInfoEx(attributeCount: 1) { startupInfo in
219-
func inherit(_ fileHandle: borrowing FileHandle, as outWindowsHANDLE: inout HANDLE?) throws {
233+
func inherit(_ fileHandle: borrowing FileHandle) throws -> HANDLE? {
220234
try fileHandle.withUnsafeWindowsHANDLE { windowsHANDLE in
221235
guard let windowsHANDLE else {
222236
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")
223237
}
224-
outWindowsHANDLE = windowsHANDLE
238+
239+
// Ensure the file handle can be inherited by the child process.
240+
guard SetHandleInformation(windowsHANDLE, DWORD(HANDLE_FLAG_INHERIT), DWORD(HANDLE_FLAG_INHERIT)) else {
241+
throw Win32Error(rawValue: GetLastError())
242+
}
243+
244+
return windowsHANDLE
225245
}
226246
}
227-
func inherit(_ fileHandle: borrowing FileHandle?, as outWindowsHANDLE: inout HANDLE?) throws {
247+
func inherit(_ fileHandle: borrowing FileHandle?) throws -> HANDLE? {
228248
if fileHandle != nil {
229-
try inherit(fileHandle!, as: &outWindowsHANDLE)
249+
return try inherit(fileHandle!)
230250
} else {
231-
outWindowsHANDLE = nil
251+
return nil
232252
}
233253
}
234254

235255
// Forward standard I/O streams.
236-
try inherit(standardInput, as: &startupInfo.pointee.StartupInfo.hStdInput)
237-
try inherit(standardOutput, as: &startupInfo.pointee.StartupInfo.hStdOutput)
238-
try inherit(standardError, as: &startupInfo.pointee.StartupInfo.hStdError)
256+
startupInfo.pointee.StartupInfo.hStdInput = try inherit(standardInput)
257+
startupInfo.pointee.StartupInfo.hStdOutput = try inherit(standardOutput)
258+
startupInfo.pointee.StartupInfo.hStdError = try inherit(standardError)
239259
startupInfo.pointee.StartupInfo.dwFlags |= STARTF_USESTDHANDLES
240260

241261
// Ensure standard I/O streams and any explicitly added file handles are
242262
// inherited by the child process.
243263
var inheritedHandles = [HANDLE?](repeating: nil, count: additionalFileHandles.count + 3)
244-
try inherit(standardInput, as: &inheritedHandles[0])
245-
try inherit(standardOutput, as: &inheritedHandles[1])
246-
try inherit(standardError, as: &inheritedHandles[2])
264+
inheritedHandles[0] = startupInfo.pointee.StartupInfo.hStdInput
265+
inheritedHandles[1] = startupInfo.pointee.StartupInfo.hStdOutput
266+
inheritedHandles[2] = startupInfo.pointee.StartupInfo.hStdError
247267
for i in 0 ..< additionalFileHandles.count {
248-
try inherit(additionalFileHandles[i].pointee, as: &inheritedHandles[i + 3])
268+
inheritedHandles[i + 3] = try inherit(additionalFileHandles[i].pointee)
249269
}
250270
inheritedHandles = inheritedHandles.compactMap(\.self)
251271

Sources/Testing/Support/FileHandle.swift

Lines changed: 36 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ struct FileHandle: ~Copyable, Sendable {
108108
///
109109
/// By default, the resulting file handle is not inherited by any child
110110
/// 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()``.
111+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
113112
init(forReadingAtPath path: String) throws {
114113
try self.init(atPath: path, mode: "reb")
115114
}
@@ -123,8 +122,7 @@ struct FileHandle: ~Copyable, Sendable {
123122
///
124123
/// By default, the resulting file handle is not inherited by any child
125124
/// 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()``.
125+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
128126
init(forWritingAtPath path: String) throws {
129127
try self.init(atPath: path, mode: "web")
130128
}
@@ -492,8 +490,7 @@ extension FileHandle {
492490
///
493491
/// By default, the resulting file handles are not inherited by any child
494492
/// 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()``.
493+
/// `HANDLE_FLAG_INHERIT` is cleared on Windows.).
497494
static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws {
498495
#if !os(Windows)
499496
var pipe2Called = false
@@ -533,8 +530,8 @@ extension FileHandle {
533530
if !pipe2Called {
534531
// pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC
535532
// to the best of our ability.
536-
try _setFileDescriptorInherited(fdReadEnd, false)
537-
try _setFileDescriptorInherited(fdWriteEnd, false)
533+
try setFD_CLOEXEC(true, onFileDescriptor: fdReadEnd)
534+
try setFD_CLOEXEC(true, onFileDescriptor: fdWriteEnd)
538535
}
539536
#endif
540537

@@ -612,72 +609,6 @@ extension FileHandle {
612609
#endif
613610
}
614611
#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-
}
681612
}
682613

683614
// MARK: - General path utilities
@@ -757,4 +688,35 @@ func canonicalizePath(_ path: String) -> String? {
757688
return nil
758689
#endif
759690
}
691+
692+
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
693+
/// Set the given file descriptor's `FD_CLOEXEC` flag.
694+
///
695+
/// - Parameters:
696+
/// - flag: The new value of `fd`'s `FD_CLOEXEC` flag.
697+
/// - fd: The file descriptor.
698+
///
699+
/// - Throws: Any error that occurred while setting the flag.
700+
func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws {
701+
switch swt_getfdflags(fd) {
702+
case -1:
703+
// An error occurred reading the flags for this file descriptor.
704+
throw CError(rawValue: swt_errno())
705+
case let oldValue:
706+
let newValue = if flag {
707+
oldValue & ~FD_CLOEXEC
708+
} else {
709+
oldValue | FD_CLOEXEC
710+
}
711+
if oldValue == newValue {
712+
// No need to make a second syscall as nothing has changed.
713+
return
714+
}
715+
if -1 == swt_setfdflags(fd, newValue) {
716+
// An error occurred setting the flags for this file descriptor.
717+
throw CError(rawValue: swt_errno())
718+
}
719+
}
720+
}
721+
#endif
760722
#endif

Sources/Testing/Support/Versions.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,30 @@ let swiftStandardLibraryVersion: String = {
153153
return "unknown"
154154
}()
155155

156+
#if canImport(Glibc)
157+
/// The (runtime, not compile-time) version of glibc in use on this system.
158+
///
159+
/// This value is not part of the public interface of the testing library.
160+
let glibcVersion: (major: Int, minor: Int) = {
161+
// Default to the statically available version number if the function call
162+
// fails for some reason.
163+
var major = Int(clamping: __GLIBC__)
164+
var minor = Int(clamping: __GLIBC_MINOR__)
165+
166+
if let strVersion = gnu_get_libc_version() {
167+
withUnsafeMutablePointer(to: &major) { major in
168+
withUnsafeMutablePointer(to: &minor) { minor in
169+
withVaList([major, minor]) { args in
170+
_ = vsscanf(strVersion, "%zd.%zd", args)
171+
}
172+
}
173+
}
174+
}
175+
176+
return (major, minor)
177+
}()
178+
#endif
179+
156180
// MARK: - sysctlbyname() Wrapper
157181

158182
#if !SWT_NO_SYSCTL && SWT_TARGET_OS_APPLE

Sources/_TestingInternals/include/Includes.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
#include <sys/fcntl.h>
5454
#endif
5555

56+
#if __has_include(<gnu/libc-version.h>)
57+
#include <gnu/libc-version.h>
58+
#endif
59+
5660
#if __has_include(<sys/resource.h>) && !defined(__wasi__)
5761
#include <sys/resource.h>
5862
#endif

0 commit comments

Comments
 (0)