Skip to content

Commit 16c4269

Browse files
authored
Do not chdir before calling posix_spawn (#4606)
We allow users to set the working directory of the child process when they use the Process API. Unfortunately, this was implemented by calling chdir in the parent process, which has nasty global effects on the process and cannot safely be used concurrently. glibc has a non-POSIX extension to posix_spawn that can call chdir in the child process, so we add that call instead where it's available, and add a regression test. Resolves swiftlang/swift#59610.
1 parent 5fff57e commit 16c4269

File tree

4 files changed

+72
-0
lines changed

4 files changed

+72
-0
lines changed

CoreFoundation/Base.subproj/CFPlatform.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,10 @@ CF_EXPORT int _CFPosixSpawnFileActionsAddClose(_CFPosixSpawnFileActionsRef file_
22102210
return posix_spawn_file_actions_addclose((posix_spawn_file_actions_t *)file_actions, filedes);
22112211
}
22122212

2213+
CF_EXPORT int _CFPosixSpawnFileActionsAddChdirNP(_CFPosixSpawnFileActionsRef file_actions, const char *path) {
2214+
return posix_spawn_file_actions_addchdir_np((posix_spawn_file_actions_t *)file_actions, path);
2215+
}
2216+
22132217
CF_EXPORT int _CFPosixSpawn(pid_t *_CF_RESTRICT pid, const char *_CF_RESTRICT path, _CFPosixSpawnFileActionsRef file_actions, _CFPosixSpawnAttrRef _Nullable _CF_RESTRICT attrp, char *_Nullable const argv[_Nullable _CF_RESTRICT], char *_Nullable const envp[_Nullable _CF_RESTRICT]) {
22142218
return posix_spawn(pid, path, (posix_spawn_file_actions_t *)file_actions, (posix_spawnattr_t *)attrp, argv, envp);
22152219
}

CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ CF_EXPORT int _CFPosixSpawn(pid_t *_CF_RESTRICT pid, const char *_CF_RESTRICT pa
709709
#else
710710
CF_EXPORT int _CFPosixSpawn(pid_t *_CF_RESTRICT pid, const char *_CF_RESTRICT path, _CFPosixSpawnFileActionsRef file_actions, _CFPosixSpawnAttrRef _Nullable _CF_RESTRICT attrp, char *_Nullable const argv[_Nullable _CF_RESTRICT], char *_Nullable const envp[_Nullable _CF_RESTRICT]);
711711
#endif // __cplusplus
712+
713+
#if TARGET_OS_LINUX
714+
CF_EXPORT int _CFPosixSpawnFileActionsAddChdirNP(_CFPosixSpawnFileActionsRef file_actions, const char *path);
715+
#endif // TARGET_OS_LINUX
712716
#endif // !TARGET_OS_WIN32
713717

714718
_CF_EXPORT_SCOPE_END

Sources/Foundation/Process.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,17 @@ open class Process: NSObject {
944944
}
945945
#endif
946946

947+
#if os(Linux)
948+
if let dir = currentDirectoryURL?.path {
949+
try _throwIfPosixError(_CFPosixSpawnFileActionsAddChdirNP(fileActions, dir))
950+
}
951+
#else
952+
// This is an unfortunate workaround: posix_spawn has no POSIX-specified way to set the working directory
953+
// of the child process. glibc has a non-POSIX API option, which we use above. Here we take a brute-force
954+
// approach of just changing our current working directory. This is not a great implementation and it's likely
955+
// to cause subtle issues in those environments. However, the Apple Foundation library doesn't have this problem,
956+
// and this library does the right thing on Linux and Windows, so the overwhelming majority of users are
957+
// well-served.
947958
let fileManager = FileManager()
948959
let previousDirectoryPath = fileManager.currentDirectoryPath
949960
if let dir = currentDirectoryURL?.path, !fileManager.changeCurrentDirectoryPath(dir) {
@@ -954,6 +965,7 @@ open class Process: NSObject {
954965
// Reset the previous working directory path.
955966
fileManager.changeCurrentDirectoryPath(previousDirectoryPath)
956967
}
968+
#endif
957969

958970
// Launch
959971
var pid = pid_t()

Tests/Foundation/Tests/TestProcess.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10+
import Dispatch
11+
1012
class TestProcess : XCTestCase {
1113

1214
func test_exit0() throws {
@@ -699,6 +701,55 @@ class TestProcess : XCTestCase {
699701
}
700702
}
701703

704+
func test_currentDirectoryDoesNotChdirParentProcess() throws {
705+
// This test only behaves correctly on Linux and Windows: other platforms don't have an
706+
// appropriate API for this in posix_spawn or similar.
707+
#if os(Linux) || os(Windows)
708+
let backgroundQueue = DispatchQueue(label: "background-processor")
709+
let group = DispatchGroup()
710+
let startSemaphore = DispatchSemaphore(value: 0)
711+
let currentWorkingDirectory = FileManager.default.currentDirectoryPath
712+
var shouldRun = true
713+
let shouldRunLock = NSLock()
714+
715+
XCTAssertNotEqual(currentWorkingDirectory, "/")
716+
717+
// Kick off the background task. This will spin on our current working directory and confirm
718+
// it doesn't change.
719+
backgroundQueue.async(group: group) {
720+
startSemaphore.signal()
721+
722+
while true {
723+
let newCWD = FileManager.default.currentDirectoryPath
724+
XCTAssertEqual(newCWD, currentWorkingDirectory)
725+
726+
shouldRunLock.lock()
727+
if shouldRun {
728+
shouldRunLock.unlock()
729+
} else {
730+
shouldRunLock.unlock()
731+
break
732+
}
733+
}
734+
}
735+
736+
startSemaphore.wait()
737+
738+
// We run the task 50 times just to try to encourage it to fail.
739+
for _ in 0..<50 {
740+
XCTAssertNoThrow(try runTask([xdgTestHelperURL().path, "--getcwd"], currentDirectoryPath: "/"))
741+
}
742+
743+
shouldRunLock.lock()
744+
shouldRun = false
745+
shouldRunLock.unlock()
746+
747+
group.wait()
748+
#else
749+
throw XCTSkip()
750+
#endif
751+
}
752+
702753
#if !os(Windows)
703754
func test_fileDescriptorsAreNotInherited() throws {
704755
let task = Process()
@@ -866,6 +917,7 @@ class TestProcess : XCTestCase {
866917
("test_currentDirectory", test_currentDirectory),
867918
("test_pipeCloseBeforeLaunch", test_pipeCloseBeforeLaunch),
868919
("test_multiProcesses", test_multiProcesses),
920+
("test_currentDirectoryDoesNotChdirParentProcess", test_currentDirectoryDoesNotChdirParentProcess),
869921
]
870922

871923
#if !os(Windows)

0 commit comments

Comments
 (0)