Skip to content

Commit 53303da

Browse files
authored
Merge pull request #2607 from weissi/jw-SR-11354-52
[5.2] SR-11354: Foundation.Process inherits file descriptors into the child process
2 parents 914a8ae + a7b307a commit 53303da

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

Foundation/Process.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
import CoreFoundation
1111

12+
#if canImport(Darwin)
13+
import Darwin
14+
#endif
15+
1216
extension Process {
1317
public enum TerminationReason : Int {
1418
case exit
@@ -56,6 +60,43 @@ extension CFSocketError {
5660
}
5761
#endif
5862

63+
#if !canImport(Darwin) && !os(Windows)
64+
private func findMaximumOpenFromProcSelfFD() -> CInt? {
65+
guard let dirPtr = opendir("/proc/self/fd") else {
66+
return nil
67+
}
68+
defer {
69+
closedir(dirPtr)
70+
}
71+
var highestFDSoFar = CInt(0)
72+
73+
while let dirEntPtr = readdir(dirPtr) {
74+
var entryName = dirEntPtr.pointee.d_name
75+
let thisFD = withUnsafeBytes(of: &entryName) { entryNamePtr -> CInt? in
76+
CInt(String(decoding: entryNamePtr.prefix(while: { $0 != 0 }), as: Unicode.UTF8.self))
77+
}
78+
highestFDSoFar = max(thisFD ?? -1, highestFDSoFar)
79+
}
80+
81+
return highestFDSoFar
82+
}
83+
84+
func findMaximumOpenFD() -> CInt {
85+
if let maxFD = findMaximumOpenFromProcSelfFD() {
86+
// the precise method worked, let's return this fd.
87+
return maxFD
88+
}
89+
90+
// We don't have /proc, let's go with the best estimate.
91+
#if os(Linux)
92+
return getdtablesize()
93+
#else
94+
return 4096
95+
#endif
96+
}
97+
#endif
98+
99+
59100
private func emptyRunLoopCallback(_ context : UnsafeMutableRawPointer?) -> Void {}
60101

61102

@@ -872,6 +913,21 @@ open class Process: NSObject {
872913
posix(_CFPosixSpawnFileActionsAddClose(fileActions, fd))
873914
}
874915

916+
#if canImport(Darwin)
917+
var spawnAttrs: posix_spawnattr_t? = nil
918+
posix_spawnattr_init(&spawnAttrs)
919+
posix_spawnattr_setflags(&spawnAttrs, .init(POSIX_SPAWN_CLOEXEC_DEFAULT))
920+
#else
921+
for fd in 3 ... findMaximumOpenFD() {
922+
guard adddup2[fd] == nil &&
923+
!addclose.contains(fd) &&
924+
fd != taskSocketPair[1] else {
925+
continue // Do not double-close descriptors, or close those pertaining to Pipes or FileHandles we want inherited.
926+
}
927+
posix(_CFPosixSpawnFileActionsAddClose(fileActions, fd))
928+
}
929+
#endif
930+
875931
let fileManager = FileManager()
876932
let previousDirectoryPath = fileManager.currentDirectoryPath
877933
if let dir = currentDirectoryURL?.path, !fileManager.changeCurrentDirectoryPath(dir) {
@@ -885,9 +941,16 @@ open class Process: NSObject {
885941

886942
// Launch
887943
var pid = pid_t()
944+
#if os(macOS)
945+
guard _CFPosixSpawn(&pid, launchPath, fileActions, &spawnAttrs, argv, envp) == 0 else {
946+
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
947+
}
948+
#else
888949
guard _CFPosixSpawn(&pid, launchPath, fileActions, nil, argv, envp) == 0 else {
889950
throw _NSErrorWithErrno(errno, reading: true, path: launchPath)
890951
}
952+
#endif
953+
891954

892955
// Close the write end of the input and output pipes.
893956
if let pipe = standardInput as? Pipe {

TestFoundation/TestProcess.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,45 @@ class TestProcess : XCTestCase {
750750
}
751751
}
752752

753+
#if !os(Windows)
754+
func test_fileDescriptorsAreNotInherited() throws {
755+
let task = Process()
756+
let someExtraFDs = [dup(1), dup(1), dup(1), dup(1), dup(1), dup(1), dup(1)]
757+
task.executableURL = xdgTestHelperURL()
758+
task.arguments = ["--print-open-file-descriptors"]
759+
task.standardInput = FileHandle.nullDevice
760+
let stdoutPipe = Pipe()
761+
task.standardOutput = stdoutPipe.fileHandleForWriting
762+
task.standardError = FileHandle.nullDevice
763+
XCTAssertNoThrow(try task.run())
764+
765+
try stdoutPipe.fileHandleForWriting.close()
766+
let stdoutData = try stdoutPipe.fileHandleForReading.readToEnd()
767+
task.waitUntilExit()
768+
let stdoutString = String(decoding: stdoutData ?? Data(), as: Unicode.UTF8.self)
769+
#if os(macOS)
770+
XCTAssertEqual("0\n1\n2\n", stdoutString)
771+
#else
772+
// on Linux we may also have a /dev/urandom open as well as some socket that Process uses for something.
773+
774+
// we should definitely have stdin (0), stdout (1), and stderr (2) open
775+
XCTAssert(stdoutString.utf8.starts(with: "0\n1\n2\n".utf8))
776+
777+
// in total we should have 6 or fewer lines:
778+
// 1. stdin
779+
// 2. stdout
780+
// 3. stderr
781+
// 4. /dev/urandom (optional)
782+
// 5. communication socket (optional)
783+
// 6. trailing new line
784+
XCTAssertLessThanOrEqual(stdoutString.components(separatedBy: "\n").count, 6, "\(stdoutString)")
785+
#endif
786+
for fd in someExtraFDs {
787+
close(fd)
788+
}
789+
}
790+
#endif
791+
753792
static var allTests: [(String, (TestProcess) -> () throws -> Void)] {
754793
var tests = [
755794
("test_exit0" , test_exit0),
@@ -786,6 +825,7 @@ class TestProcess : XCTestCase {
786825
tests += [
787826
("test_interrupt", test_interrupt),
788827
("test_suspend_resume", test_suspend_resume),
828+
("test_fileDescriptorsAreNotInherited", test_fileDescriptorsAreNotInherited),
789829
]
790830
#endif
791831
return tests

TestFoundation/xdgTestHelper/main.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@ func cat(_ args: ArraySlice<String>.Iterator) {
196196
exit(exitCode)
197197
}
198198

199+
#if !os(Windows)
200+
func printOpenFileDescriptors() {
201+
let reasonableMaxFD: CInt
202+
#if os(Linux) || os(macOS)
203+
reasonableMaxFD = getdtablesize()
204+
#else
205+
reasonableMaxFD = 4096
206+
#endif
207+
for fd in 0..<reasonableMaxFD {
208+
if fcntl(fd, F_GETFD) != -1 {
209+
print(fd)
210+
}
211+
}
212+
exit(0)
213+
}
214+
#endif
215+
199216
// -----
200217

201218
var arguments = ProcessInfo.processInfo.arguments.dropFirst().makeIterator()
@@ -254,8 +271,11 @@ case "--nspathfor":
254271
#if !os(Windows)
255272
case "--signal-test":
256273
signalTest()
274+
275+
case "--print-open-file-descriptors":
276+
printOpenFileDescriptors()
257277
#endif
258-
278+
259279
default:
260280
fatalError("These arguments are not recognized. Only run this from a unit test.")
261281
}

0 commit comments

Comments
 (0)