Skip to content

Commit 6e9afb5

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 67e2f7d commit 6e9afb5

File tree

2 files changed

+112
-132
lines changed

2 files changed

+112
-132
lines changed

Sources/_SubprocessCShims/process_shims.c

Lines changed: 83 additions & 132 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)
@@ -361,91 +367,57 @@ static int _subprocess_addchdir_np(
361367
#endif
362368
}
363369

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

402-
// Close parent side
403-
if (file_descriptors[1] >= 0) {
404-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]);
405-
if (rc != 0) { return rc; }
406-
}
407-
if (file_descriptors[3] >= 0) {
408-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]);
409-
if (rc != 0) { return rc; }
374+
while ((c = *str++) != 0) {
375+
out *= 10;
376+
if (c >= '0' && c <= '9') {
377+
out += c - '0';
378+
} else {
379+
return -1;
380+
}
410381
}
411-
if (file_descriptors[5] >= 0) {
412-
rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]);
413-
if (rc != 0) { return rc; }
382+
return out;
383+
}
384+
385+
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
386+
int highest_fd_so_far = 0;
387+
DIR *dir_ptr = opendir(fd_dir);
388+
if (dir_ptr == NULL) {
389+
return -1;
414390
}
415391

416-
// Setup spawnattr
417-
posix_spawnattr_t spawn_attr;
418-
rc = posix_spawnattr_init(&spawn_attr);
419-
if (rc != 0) { return rc; }
420-
// Masks
421-
sigset_t no_signals;
422-
sigset_t all_signals;
423-
sigemptyset(&no_signals);
424-
sigfillset(&all_signals);
425-
rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals);
426-
if (rc != 0) { return rc; }
427-
rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals);
428-
if (rc != 0) { return rc; }
429-
// Flags
430-
short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF;
431-
if (process_group_id != NULL) {
432-
flags |= POSIX_SPAWN_SETPGROUP;
433-
rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id);
434-
if (rc != 0) { return rc; }
392+
struct dirent *dir_entry = NULL;
393+
while ((dir_entry = readdir(dir_ptr)) != NULL) {
394+
char *entry_name = dir_entry->d_name;
395+
int number = _positive_int_parse(entry_name);
396+
if (number > (long)highest_fd_so_far) {
397+
highest_fd_so_far = number;
398+
}
435399
}
436-
rc = posix_spawnattr_setflags(&spawn_attr, flags);
437400

438-
// Spawn!
439-
rc = posix_spawn(
440-
pid, exec_path,
441-
&file_actions, &spawn_attr,
442-
args, env
443-
);
444-
posix_spawn_file_actions_destroy(&file_actions);
445-
posix_spawnattr_destroy(&spawn_attr);
446-
return rc;
401+
closedir(dir_ptr);
402+
return highest_fd_so_far;
403+
}
404+
405+
static int _highest_possibly_open_fd(void) {
406+
#if defined(__APPLE__)
407+
int hi = _highest_possibly_open_fd_dir("/dev/fd");
408+
if (hi < 0) {
409+
hi = getdtablesize();
410+
}
411+
#elif defined(__linux__)
412+
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
413+
if (hi < 0) {
414+
hi = getdtablesize();
415+
}
416+
#else
417+
int hi = 1024;
418+
#endif
419+
return hi;
447420
}
448-
#endif // _POSIX_SPAWN
449421

450422
int _subprocess_fork_exec(
451423
pid_t * _Nonnull pid,
@@ -466,32 +438,6 @@ int _subprocess_fork_exec(
466438
close(pipefd[1]); \
467439
_exit(EXIT_FAILURE)
468440

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

553499
if (childPid == 0) {
554500
// Child process
555-
close(pipefd[0]); // Close unused read end
556-
557501
// Reset signal handlers
558502
for (int signo = 1; signo < _SUBPROCESS_SIG_MAX; signo++) {
559503
if (signo == SIGKILL || signo == SIGSTOP) {
@@ -615,41 +559,48 @@ int _subprocess_fork_exec(
615559
// Bind stdin, stdout, and stderr
616560
if (file_descriptors[0] >= 0) {
617561
rc = dup2(file_descriptors[0], STDIN_FILENO);
618-
if (rc < 0) {
619-
write_error_and_exit;
620-
}
562+
} else {
563+
rc = close(STDIN_FILENO);
564+
}
565+
if (rc < 0) {
566+
write_error_and_exit;
621567
}
568+
622569
if (file_descriptors[2] >= 0) {
623570
rc = dup2(file_descriptors[2], STDOUT_FILENO);
624-
if (rc < 0) {
625-
write_error_and_exit;
626-
}
571+
} else {
572+
rc = close(STDOUT_FILENO);
573+
}
574+
if (rc < 0) {
575+
write_error_and_exit;
627576
}
577+
628578
if (file_descriptors[4] >= 0) {
629579
rc = dup2(file_descriptors[4], STDERR_FILENO);
630-
if (rc < 0) {
631-
int error = errno;
632-
write(pipefd[1], &error, sizeof(error));
633-
close(pipefd[1]);
634-
_exit(EXIT_FAILURE);
635-
}
636-
}
637-
// Close parent side
638-
if (file_descriptors[1] >= 0) {
639-
rc = close(file_descriptors[1]);
580+
} else {
581+
rc = close(STDERR_FILENO);
640582
}
641-
if (file_descriptors[3] >= 0) {
642-
rc = close(file_descriptors[3]);
643-
}
644-
if (file_descriptors[4] >= 0) {
645-
rc = close(file_descriptors[4]);
583+
if (rc < 0) {
584+
write_error_and_exit;
646585
}
586+
// Close all other file descriptors
587+
rc = -1;
588+
errno = ENOSYS;
589+
#if __has_include(<linux/close_range.h>)
590+
// We must NOT close pipefd[1] for writing errors
591+
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0);
592+
rc |= close_range(pipefd[1] + 1, ~0U, 0);
593+
#endif
647594
if (rc != 0) {
648-
int error = errno;
649-
write(pipefd[1], &error, sizeof(error));
650-
close(pipefd[1]);
651-
_exit(EXIT_FAILURE);
595+
// close_range failed (or doesn't exist), fall back to close()
596+
for (int fd = STDERR_FILENO + 1; fd < _highest_possibly_open_fd(); fd++) {
597+
// We must NOT close pipefd[1] for writing errors
598+
if (fd != pipefd[1]) {
599+
close(fd);
600+
}
601+
}
652602
}
603+
653604
// Run custom configuratior
654605
if (configurator != NULL) {
655606
configurator();

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)