Skip to content

Commit 3e47e05

Browse files
committed
Emulate POSIX_SPAWN_CLOEXEC_DEFAULT in fork/exec.
POSIX_SPAWN_CLOEXEC_DEFAULT is only available on Darwin. Emulate POSIX_SPAWN_CLOEXEC_DEFAULT on other platforms by calling close after fork, before exec. This commit also removes _subprocess_posix_spawn_fallback because we can't emulate POSIX_SPAWN_CLOEXEC_DEFAULT in a thread-safe manner while using posix_spawn.
1 parent 80bd50f commit 3e47e05

File tree

2 files changed

+110
-133
lines changed

2 files changed

+110
-133
lines changed

Sources/_SubprocessCShims/process_shims.c

Lines changed: 81 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@
3131
#include <string.h>
3232
#include <fcntl.h>
3333
#include <pthread.h>
34-
34+
#include <dirent.h>
3535
#include <stdio.h>
3636

37+
#if __has_include(<linux/close_range.h>)
38+
#include <linux/close_range.h>
39+
#endif
40+
41+
#endif // TARGET_OS_WINDOWS
42+
3743
#if __has_include(<crt_externs.h>)
3844
#include <crt_externs.h>
3945
#elif defined(_WIN32)
@@ -364,93 +370,57 @@ static int _subprocess_addchdir_np(
364370
#endif
365371
}
366372

367-
static int _subprocess_posix_spawn_fallback(
368-
pid_t * _Nonnull pid,
369-
const char * _Nonnull exec_path,
370-
const char * _Nullable working_directory,
371-
const int file_descriptors[_Nonnull],
372-
char * _Nullable const args[_Nonnull],
373-
char * _Nullable const env[_Nullable],
374-
gid_t * _Nullable process_group_id
375-
) {
376-
// Setup stdin, stdout, and stderr
377-
posix_spawn_file_actions_t file_actions;
378-
379-
int rc = posix_spawn_file_actions_init(&file_actions);
380-
if (rc != 0) { return rc; }
381-
if (file_descriptors[0] >= 0) {
382-
rc = posix_spawn_file_actions_adddup2(
383-
&file_actions, file_descriptors[0], STDIN_FILENO
384-
);
385-
if (rc != 0) { return rc; }
386-
}
387-
if (file_descriptors[2] >= 0) {
388-
rc = posix_spawn_file_actions_adddup2(
389-
&file_actions, file_descriptors[2], STDOUT_FILENO
390-
);
391-
if (rc != 0) { return rc; }
392-
}
393-
if (file_descriptors[4] >= 0) {
394-
rc = posix_spawn_file_actions_adddup2(
395-
&file_actions, file_descriptors[4], STDERR_FILENO
396-
);
397-
if (rc != 0) { return rc; }
398-
}
399-
// Setup working directory
400-
if (working_directory != NULL) {
401-
rc = _subprocess_addchdir_np(&file_actions, working_directory);
402-
if (rc != 0) {
403-
return rc;
373+
static int _positive_int_parse(const char *str) {
374+
int out = 0;
375+
char c = 0;
376+
377+
while ((c = *str++) != 0) {
378+
out *= 10;
379+
if (c >= '0' && c <= '9') {
380+
out += c - '0';
381+
} else {
382+
return -1;
404383
}
405384
}
385+
return out;
386+
}
406387

407-
// Close parent side
408-
if (file_descriptors[1] >= 0) {
409-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]);
410-
if (rc != 0) { return rc; }
411-
}
412-
if (file_descriptors[3] >= 0) {
413-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]);
414-
if (rc != 0) { return rc; }
415-
}
416-
if (file_descriptors[5] >= 0) {
417-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]);
418-
if (rc != 0) { return rc; }
388+
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
389+
int highest_fd_so_far = 0;
390+
DIR *dir_ptr = opendir(fd_dir);
391+
if (dir_ptr == NULL) {
392+
return -1;
419393
}
420394

421-
// Setup spawnattr
422-
posix_spawnattr_t spawn_attr;
423-
rc = posix_spawnattr_init(&spawn_attr);
424-
if (rc != 0) { return rc; }
425-
// Masks
426-
sigset_t no_signals;
427-
sigset_t all_signals;
428-
sigemptyset(&no_signals);
429-
sigfillset(&all_signals);
430-
rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals);
431-
if (rc != 0) { return rc; }
432-
rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals);
433-
if (rc != 0) { return rc; }
434-
// Flags
435-
short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF;
436-
if (process_group_id != NULL) {
437-
flags |= POSIX_SPAWN_SETPGROUP;
438-
rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id);
439-
if (rc != 0) { return rc; }
395+
struct dirent *dir_entry = NULL;
396+
while ((dir_entry = readdir(dir_ptr)) != NULL) {
397+
char *entry_name = dir_entry->d_name;
398+
int number = _positive_int_parse(entry_name);
399+
if (number > (long)highest_fd_so_far) {
400+
highest_fd_so_far = number;
401+
}
440402
}
441-
rc = posix_spawnattr_setflags(&spawn_attr, flags);
442403

443-
// Spawn!
444-
rc = posix_spawn(
445-
pid, exec_path,
446-
&file_actions, &spawn_attr,
447-
args, env
448-
);
449-
posix_spawn_file_actions_destroy(&file_actions);
450-
posix_spawnattr_destroy(&spawn_attr);
451-
return rc;
404+
closedir(dir_ptr);
405+
return highest_fd_so_far;
406+
}
407+
408+
static int _highest_possibly_open_fd(void) {
409+
#if defined(__APPLE__)
410+
int hi = _highest_possibly_open_fd_dir("/dev/fd");
411+
if (hi < 0) {
412+
hi = getdtablesize();
413+
}
414+
#elif defined(__linux__)
415+
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
416+
if (hi < 0) {
417+
hi = getdtablesize();
418+
}
419+
#else
420+
int hi = 1024;
421+
#endif
422+
return hi;
452423
}
453-
#endif // _POSIX_SPAWN
454424

455425
int _subprocess_fork_exec(
456426
pid_t * _Nonnull pid,
@@ -471,32 +441,6 @@ int _subprocess_fork_exec(
471441
close(pipefd[1]); \
472442
_exit(EXIT_FAILURE)
473443

474-
int require_pre_fork = _subprocess_is_addchdir_np_available() == 0 ||
475-
uid != NULL ||
476-
gid != NULL ||
477-
process_group_id != NULL ||
478-
(number_of_sgroups > 0 && sgroups != NULL) ||
479-
create_session ||
480-
configurator != NULL;
481-
482-
#if _POSIX_SPAWN
483-
// If posix_spawn is available on this platform and
484-
// we do not require prefork, use posix_spawn if possible.
485-
//
486-
// (Glibc's posix_spawn does not support
487-
// `POSIX_SPAWN_SETEXEC` therefore we have to keep
488-
// using fork/exec if `require_pre_fork` is true.
489-
if (require_pre_fork == 0) {
490-
return _subprocess_posix_spawn_fallback(
491-
pid, exec_path,
492-
working_directory,
493-
file_descriptors,
494-
args, env,
495-
process_group_id
496-
);
497-
}
498-
#endif
499-
500444
// Setup pipe to catch exec failures from child
501445
int pipefd[2];
502446
if (pipe(pipefd) != 0) {
@@ -557,8 +501,6 @@ int _subprocess_fork_exec(
557501

558502
if (childPid == 0) {
559503
// Child process
560-
close(pipefd[0]); // Close unused read end
561-
562504
// Reset signal handlers
563505
for (int signo = 1; signo < _SUBPROCESS_SIG_MAX; signo++) {
564506
if (signo == SIGKILL || signo == SIGSTOP) {
@@ -620,40 +562,46 @@ int _subprocess_fork_exec(
620562
// Bind stdin, stdout, and stderr
621563
if (file_descriptors[0] >= 0) {
622564
rc = dup2(file_descriptors[0], STDIN_FILENO);
623-
if (rc < 0) {
624-
write_error_and_exit;
625-
}
565+
} else {
566+
rc = close(STDIN_FILENO);
567+
}
568+
if (rc < 0) {
569+
write_error_and_exit;
626570
}
571+
627572
if (file_descriptors[2] >= 0) {
628573
rc = dup2(file_descriptors[2], STDOUT_FILENO);
629-
if (rc < 0) {
630-
write_error_and_exit;
631-
}
574+
} else {
575+
rc = close(STDOUT_FILENO);
632576
}
577+
if (rc < 0) {
578+
write_error_and_exit;
579+
}
580+
633581
if (file_descriptors[4] >= 0) {
634582
rc = dup2(file_descriptors[4], STDERR_FILENO);
635-
if (rc < 0) {
636-
int error = errno;
637-
write(pipefd[1], &error, sizeof(error));
638-
close(pipefd[1]);
639-
_exit(EXIT_FAILURE);
640-
}
641-
}
642-
// Close parent side
643-
if (file_descriptors[1] >= 0) {
644-
rc = close(file_descriptors[1]);
583+
} else {
584+
rc = close(STDERR_FILENO);
645585
}
646-
if (file_descriptors[3] >= 0) {
647-
rc = close(file_descriptors[3]);
648-
}
649-
if (file_descriptors[5] >= 0) {
650-
rc = close(file_descriptors[5]);
586+
if (rc < 0) {
587+
write_error_and_exit;
651588
}
589+
// Close all other file descriptors
590+
rc = -1;
591+
errno = ENOSYS;
592+
#if __has_include(<linux/close_range.h>)
593+
// We must NOT close pipefd[1] for writing errors
594+
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0);
595+
rc |= close_range(pipefd[1] + 1, ~0U, 0);
596+
#endif
652597
if (rc != 0) {
653-
int error = errno;
654-
write(pipefd[1], &error, sizeof(error));
655-
close(pipefd[1]);
656-
_exit(EXIT_FAILURE);
598+
// close_range failed (or doesn't exist), fall back to close()
599+
for (int fd = STDERR_FILENO + 1; fd < _highest_possibly_open_fd(); fd++) {
600+
// We must NOT close pipefd[1] for writing errors
601+
if (fd != pipefd[1]) {
602+
close(fd);
603+
}
604+
}
657605
}
658606
// Run custom configuratior
659607
if (configurator != NULL) {

Tests/SubprocessTests/SubprocessTests+Unix.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,35 @@ extension SubprocessUnixTests {
968968
}
969969
try FileManager.default.removeItem(at: testFilePath)
970970
}
971+
972+
@Test func testDoesNotInheritRandomFileDescriptorsByDefault() async throws {
973+
// This tests makes sure POSIX_SPAWN_CLOEXEC_DEFAULT works on all platforms
974+
let pipe = try FileDescriptor.ssp_pipe()
975+
defer {
976+
close(pipe.readEnd.rawValue)
977+
close(pipe.writeEnd.rawValue)
978+
}
979+
let writeFd = pipe.writeEnd.rawValue
980+
let result = try await Subprocess.run(
981+
.path("/bin/bash"),
982+
arguments: ["-c", "echo hello from child >&\(writeFd); echo wrote into \(writeFd), echo exit code $?"],
983+
output: .string,
984+
error: .string
985+
)
986+
close(pipe.writeEnd.rawValue)
987+
988+
#expect(result.terminationStatus.isSuccess)
989+
#expect(
990+
result.standardOutput?.trimmingCharacters(in: .whitespacesAndNewlines) ==
991+
"wrote into \(writeFd), echo exit code 1"
992+
)
993+
// Depending on the platform, standard output should be something like
994+
// `/bin/bash: 7: Bad file descriptor
995+
#expect(!result.standardOutput!.isEmpty)
996+
let nonInherited = try await pipe.readEnd.readUntilEOF(upToLength: .max)
997+
// We should have read nothing because the pipe is not inherited
998+
#expect(nonInherited.isEmpty)
999+
}
9711000
}
9721001

9731002
// MARK: - Utils

0 commit comments

Comments
 (0)