diff --git a/Foundation/Process.swift b/Foundation/Process.swift index c89a244a92..6c0e30e1ce 100644 --- a/Foundation/Process.swift +++ b/Foundation/Process.swift @@ -9,6 +9,10 @@ import CoreFoundation +#if canImport(Darwin) +import Darwin +#endif + extension Process { public enum TerminationReason : Int { case exit @@ -837,6 +841,21 @@ open class Process: NSObject { posix(_CFPosixSpawnFileActionsAddClose(fileActions, fd)) } + #if canImport(Darwin) + var spawnAttrs: posix_spawnattr_t? = nil + posix_spawnattr_init(&spawnAttrs) + posix_spawnattr_setflags(&spawnAttrs, .init(POSIX_SPAWN_CLOEXEC_DEFAULT)) + #else + for fd in 3 ..< getdtablesize() { + guard adddup2[fd] == nil && + !addclose.contains(fd) && + fd != taskSocketPair[1] else { + continue // Do not double-close descriptors, or close those pertaining to Pipes or FileHandles we want inherited. + } + posix(_CFPosixSpawnFileActionsAddClose(fileActions, fd)) + } + #endif + let fileManager = FileManager() let previousDirectoryPath = fileManager.currentDirectoryPath if !fileManager.changeCurrentDirectoryPath(currentDirectoryURL.path) { @@ -850,9 +869,16 @@ open class Process: NSObject { // Launch var pid = pid_t() + #if os(macOS) + guard _CFPosixSpawn(&pid, launchPath, fileActions, &spawnAttrs, argv, envp) == 0 else { + throw _NSErrorWithErrno(errno, reading: true, path: launchPath) + } + #else guard _CFPosixSpawn(&pid, launchPath, fileActions, nil, argv, envp) == 0 else { throw _NSErrorWithErrno(errno, reading: true, path: launchPath) } + #endif + // Close the write end of the input and output pipes. if let pipe = standardInput as? Pipe { diff --git a/TestFoundation/TestProcess.swift b/TestFoundation/TestProcess.swift index 935a7c3735..3f6ccae85a 100644 --- a/TestFoundation/TestProcess.swift +++ b/TestFoundation/TestProcess.swift @@ -565,6 +565,32 @@ class TestProcess : XCTestCase { XCTAssertEqual(String(data: stdoutData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), "No files specified.") } + func test_fileDescriptorsAreNotInherited() throws { + let task = Process() + let someExtraFDs = [dup(1), dup(1), dup(1), dup(1), dup(1), dup(1), dup(1)] + task.executableURL = xdgTestHelperURL() + task.arguments = ["--print-open-file-descriptors"] + task.standardInput = FileHandle.nullDevice + let stdoutPipe = Pipe() + task.standardOutput = stdoutPipe.fileHandleForWriting + task.standardError = FileHandle.nullDevice + XCTAssertNoThrow(try task.run()) + + try stdoutPipe.fileHandleForWriting.close() + let stdoutData = try stdoutPipe.fileHandleForReading.readToEnd() + task.waitUntilExit() + let stdoutString = String(decoding: stdoutData ?? Data(), as: Unicode.UTF8.self) + #if os(macOS) + XCTAssertEqual("0\n1\n2\n", stdoutString) + #else + // on Linux we should also have a /dev/urandom open as well as some socket that Process uses for something. + XCTAssert(stdoutString.utf8.starts(with: "0\n1\n2\n3\n".utf8)) + XCTAssertEqual(stdoutString.components(separatedBy: "\n").count, 6, "\(stdoutString)") + #endif + for fd in someExtraFDs { + close(fd) + } + } static var allTests: [(String, (TestProcess) -> () throws -> Void)] { var tests = [ @@ -599,6 +625,7 @@ class TestProcess : XCTestCase { tests += [ ("test_interrupt", test_interrupt), ("test_suspend_resume", test_suspend_resume), + ("test_fileDescriptorsAreNotInherited", test_fileDescriptorsAreNotInherited), ] #endif return tests diff --git a/TestFoundation/xdgTestHelper/main.swift b/TestFoundation/xdgTestHelper/main.swift index 1950db2750..3f4e115ce3 100644 --- a/TestFoundation/xdgTestHelper/main.swift +++ b/TestFoundation/xdgTestHelper/main.swift @@ -206,6 +206,17 @@ func cat(_ args: ArraySlice.Iterator) { exit(exitCode) } +#if !os(Windows) +func printOpenFileDescriptors() { + for fd in 0..