From a0fd8aedde51e4a896a44fa178733b4bfecb6143 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 01:54:42 -0400 Subject: [PATCH 01/10] Use `posix_spawn_file_actions_adddup2()` to clear `FD_CLOEXEC`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On FreeBSD, OpenBSD, Android, and Glibc ≥ 2.29, `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. Some older Linuxes (Amazon Linux 2 in particular) don't have this functionality, so we do a runtime check of the Glibc version. --- Sources/Testing/ExitTests/ExitTest.swift | 25 ++++++++++++++++---- Sources/Testing/ExitTests/SpawnProcess.swift | 2 ++ Sources/_TestingInternals/include/Versions.h | 11 +++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9284310db..fdb91adc8 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -803,11 +803,26 @@ extension ExitTest { #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) + // Darwin, this isn't needed because we use POSIX_SPAWN_CLOEXEC_DEFAULT. + var setFileHandlesInherited = false +#if os(Windows) + // Always set the file handles to be inherited on Windows. Our calls to + // CreateProcessW() specify which file handles are actually inherited on + // a per-child basis. + setFileHandlesInherited = true +#elseif canImport(Glibc) + // On Glibc ≥ 2.29, posix_spawn_file_actions_adddup2() automatically + // clears FD_CLOEXEC for us in the child process. + var glibcVersion: (major: CInt, minor: CInt) = (0, 0) + swt_getGlibcVersion(&glibcVersion.major, &glibcVersion.minor) + setFileHandlesInherited = glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29) +#endif + if setFileHandlesInherited { + try stdoutWriteEnd?.setInherited(true) + try stderrWriteEnd?.setInherited(true) + try backChannelWriteEnd.setInherited(true) + try capturedValuesReadEnd.setInherited(true) + } #endif // Spawn the child process. diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8f8d95db6..7e56fa4c0 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -128,6 +128,8 @@ func spawnExecutable( } else { #if SWT_TARGET_OS_APPLE _ = posix_spawn_file_actions_addinherit_np(fileActions, fd) +#else + _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) #endif highestFD = max(highestFD, fd) } diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h index 1be02ba33..bbff0fdce 100644 --- a/Sources/_TestingInternals/include/Versions.h +++ b/Sources/_TestingInternals/include/Versions.h @@ -48,6 +48,17 @@ static const char *_Nullable swt_getWASIVersion(void) { } #endif +#if defined(__GLIBC__) +/// Get the version of glibc used when compiling the testing library. +/// +/// This function is provided because `__GLIBC__` and `__GLIBC_MINOR__` are not +/// available in Swift. +static void swt_getGlibcVersion(int *outMajor, int *outMinor) { + *outMajor = __GLIBC__; + *outMinor = __GLIBC_MINOR__; +} +#endif + SWT_ASSUME_NONNULL_END #endif From 13afe50771016689ce7360847b01a86f79b27164 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 10:07:48 -0400 Subject: [PATCH 02/10] Simplify logic, check glibc runtime version --- Sources/Testing/ExitTests/ExitTest.swift | 24 ---- Sources/Testing/ExitTests/SpawnProcess.swift | 40 +++++-- Sources/Testing/Support/FileHandle.swift | 110 ++++++------------- Sources/Testing/Support/Versions.swift | 24 ++++ Sources/_TestingInternals/include/Versions.h | 11 -- 5 files changed, 88 insertions(+), 121 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index fdb91adc8..beda3eb4e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -801,30 +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 isn't needed because we use POSIX_SPAWN_CLOEXEC_DEFAULT. - var setFileHandlesInherited = false -#if os(Windows) - // Always set the file handles to be inherited on Windows. Our calls to - // CreateProcessW() specify which file handles are actually inherited on - // a per-child basis. - setFileHandlesInherited = true -#elseif canImport(Glibc) - // On Glibc ≥ 2.29, posix_spawn_file_actions_adddup2() automatically - // clears FD_CLOEXEC for us in the child process. - var glibcVersion: (major: CInt, minor: CInt) = (0, 0) - swt_getGlibcVersion(&glibcVersion.major, &glibcVersion.minor) - setFileHandlesInherited = glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29) -#endif - if setFileHandlesInherited { - 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 diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 7e56fa4c0..c1da57fa8 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -118,6 +118,9 @@ func spawnExecutable( // Forward standard I/O streams and any explicitly added file handles. var highestFD = max(STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO) +#if canImport(Glibc) + lazy var glibcVersion = glibcVersion +#endif func inherit(_ fileHandle: borrowing FileHandle, as standardFD: CInt? = nil) throws { try fileHandle.withUnsafePOSIXFileDescriptor { fd in guard let fd else { @@ -130,6 +133,13 @@ func spawnExecutable( _ = posix_spawn_file_actions_addinherit_np(fileActions, fd) #else _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) +#if canImport(Glibc) + if 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(). + try setFD_CLOEXEC(false, onFileDescriptor: fd) + } +#endif #endif highestFD = max(highestFD, fd) } @@ -218,36 +228,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) diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 4e3c17372..1c5447460 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -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") } @@ -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") } @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 1eb7f4e48..013c7e7b3 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -153,6 +153,30 @@ let swiftStandardLibraryVersion: String = { return "unknown" }() +#if canImport(Glibc) +/// Get the (runtime, not compile-time) version of glibc in use on this system. +/// +/// - Returns: The version of glibc currently in use on this system. +var 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 diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h index bbff0fdce..1be02ba33 100644 --- a/Sources/_TestingInternals/include/Versions.h +++ b/Sources/_TestingInternals/include/Versions.h @@ -48,17 +48,6 @@ static const char *_Nullable swt_getWASIVersion(void) { } #endif -#if defined(__GLIBC__) -/// Get the version of glibc used when compiling the testing library. -/// -/// This function is provided because `__GLIBC__` and `__GLIBC_MINOR__` are not -/// available in Swift. -static void swt_getGlibcVersion(int *outMajor, int *outMinor) { - *outMajor = __GLIBC__; - *outMinor = __GLIBC_MINOR__; -} -#endif - SWT_ASSUME_NONNULL_END #endif From f843ad7d14d7986b44353574c890115c4e8918dd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 10:23:33 -0400 Subject: [PATCH 03/10] Update comments to refer to POSIX 2024 and Austin Group #411 --- Sources/Testing/ExitTests/SpawnProcess.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index c1da57fa8..cb3610f0a 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -132,11 +132,18 @@ func spawnExecutable( #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, Android (Bionic) and is + // standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/ + // and https://www.austingroupbugs.net/view.php?id=411). _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) #if canImport(Glibc) - if glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29) { + 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(). + // 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 @@ -168,8 +175,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) From 09f8b0b330142df376650e894b6e3b2419246bf8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 10:28:09 -0400 Subject: [PATCH 04/10] Fix missing include --- Sources/_TestingInternals/include/Includes.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index bfc87b001..1b95151cb 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -53,6 +53,10 @@ #include #endif +#if __has_include() +#include +#endif + #if __has_include() && !defined(__wasi__) #include #endif From 012118fea74ed5cef6adcd2a4bbdfa30e8eb55e8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 10:45:02 -0400 Subject: [PATCH 05/10] Fix opengroup link --- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index cb3610f0a..0caeb689d 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -135,8 +135,8 @@ func spawnExecutable( // 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, Android (Bionic) and is - // standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/ + // 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) From a237248ef3d1291c0dbed2f69e1289ffba589b54 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 10:56:28 -0400 Subject: [PATCH 06/10] Make Windows impl unambiguous (?) --- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 0caeb689d..e14b807d5 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -233,7 +233,7 @@ func spawnExecutable( } #elseif os(Windows) return try _withStartupInfoEx(attributeCount: 1) { startupInfo in - func inherit(_ fileHandle: borrowing FileHandle) throws -> HANDLE { + 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") From 8d6e28dc5bd0a249fc5c49e6b1c7723d58e15287 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 11:36:29 -0400 Subject: [PATCH 07/10] Ensure that standard file descriptors are also affected (always currently the case though) --- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index e14b807d5..542946ff1 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -126,7 +126,7 @@ 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 From ce777c4bc242e118458e49256dbd1a235cd23c90 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 12:01:23 -0400 Subject: [PATCH 08/10] TEMPORARY: print scanned result --- Sources/Testing/Support/Versions.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 013c7e7b3..96b732b41 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -167,7 +167,12 @@ var glibcVersion: (major: Int, minor: Int) { withUnsafeMutablePointer(to: &major) { major in withUnsafeMutablePointer(to: &minor) { minor in withVaList([major, minor]) { args in - _ = vsscanf(strVersion, "%zd.%zd", args) + // TEMPORARY FOR DEBUGGING + if 2 == vsscanf(strVersion, "%zd.%zd", args) { + try! FileHandle.stderr.write("*** GLIBC VERSION SCANNED FROM '\(String(cString: strVersion))': \(major.pointee) \(minor.pointee)\n") + } else { + try! FileHandle.stderr.write("*** FAILED TO SCAN GLIBC VERSION FROM '\(String(cString: strVersion))'\n") + } } } } From 32a3504d13ed550135a2d428f1b27d07560733bb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 12:10:09 -0400 Subject: [PATCH 09/10] Make glibcVersion stored since we call it frequently --- Sources/Testing/Support/Versions.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 96b732b41..1229e80b0 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -154,10 +154,10 @@ let swiftStandardLibraryVersion: String = { }() #if canImport(Glibc) -/// Get the (runtime, not compile-time) version of glibc in use on this system. +/// The (runtime, not compile-time) version of glibc in use on this system. /// -/// - Returns: The version of glibc currently in use on this system. -var glibcVersion: (major: Int, minor: Int) { +/// 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__) @@ -167,19 +167,14 @@ var glibcVersion: (major: Int, minor: Int) { withUnsafeMutablePointer(to: &major) { major in withUnsafeMutablePointer(to: &minor) { minor in withVaList([major, minor]) { args in - // TEMPORARY FOR DEBUGGING - if 2 == vsscanf(strVersion, "%zd.%zd", args) { - try! FileHandle.stderr.write("*** GLIBC VERSION SCANNED FROM '\(String(cString: strVersion))': \(major.pointee) \(minor.pointee)\n") - } else { - try! FileHandle.stderr.write("*** FAILED TO SCAN GLIBC VERSION FROM '\(String(cString: strVersion))'\n") - } + _ = vsscanf(strVersion, "%zd.%zd", args) } } } } return (major, minor) -} +}() #endif // MARK: - sysctlbyname() Wrapper From b70612f2a039e864f036cc7c091e3dcae8619ae4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 13:28:46 -0400 Subject: [PATCH 10/10] Don't need to precache glibcVersion --- Sources/Testing/ExitTests/SpawnProcess.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 542946ff1..647e62dd9 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -118,9 +118,6 @@ func spawnExecutable( // Forward standard I/O streams and any explicitly added file handles. var highestFD = max(STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO) -#if canImport(Glibc) - lazy var glibcVersion = glibcVersion -#endif func inherit(_ fileHandle: borrowing FileHandle, as standardFD: CInt? = nil) throws { try fileHandle.withUnsafePOSIXFileDescriptor { fd in guard let fd else {