Skip to content

Commit c345e8d

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 afc1f73 commit c345e8d

File tree

2 files changed

+149
-69
lines changed

2 files changed

+149
-69
lines changed

Sources/_SubprocessCShims/process_shims.c

Lines changed: 120 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@
3434
#include <string.h>
3535
#include <fcntl.h>
3636
#include <pthread.h>
37-
37+
#include <dirent.h>
3838
#include <stdio.h>
3939

40+
#if __has_include(<linux/close_range.h>)
41+
#include <linux/close_range.h>
42+
#endif
43+
44+
#endif // TARGET_OS_WINDOWS
45+
4046
#if __has_include(<crt_externs.h>)
4147
#include <crt_externs.h>
4248
#elif defined(_WIN32)
@@ -50,6 +56,7 @@ extern char **environ;
5056
#include <mach/vm_page_size.h>
5157
#endif
5258

59+
#if !TARGET_OS_WINDOWS
5360
int _was_process_exited(int status) {
5461
return WIFEXITED(status);
5562
}
@@ -70,24 +77,8 @@ int _was_process_suspended(int status) {
7077
return WIFSTOPPED(status);
7178
}
7279

73-
#if TARGET_OS_LINUX
74-
#include <stdio.h>
75-
76-
int _shims_snprintf(
77-
char * _Nonnull str,
78-
int len,
79-
const char * _Nonnull format,
80-
char * _Nonnull str1,
81-
char * _Nonnull str2
82-
) {
83-
return snprintf(str, len, format, str1, str2);
84-
}
85-
86-
int _pidfd_send_signal(int pidfd, int signal) {
87-
return syscall(SYS_pidfd_send_signal, pidfd, signal, NULL, 0);
88-
}
80+
#endif // !TARGET_OS_WINDOWS
8981

90-
#endif
9182

9283
#if __has_include(<mach/vm_page_size.h>)
9384
vm_size_t _subprocess_vm_size(void) {
@@ -96,40 +87,6 @@ vm_size_t _subprocess_vm_size(void) {
9687
}
9788
#endif
9889

99-
// MARK: - Private Helpers
100-
static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER;
101-
102-
static int _subprocess_block_everything_but_something_went_seriously_wrong_signals(sigset_t *old_mask) {
103-
sigset_t mask;
104-
int r = 0;
105-
r |= sigfillset(&mask);
106-
r |= sigdelset(&mask, SIGABRT);
107-
r |= sigdelset(&mask, SIGBUS);
108-
r |= sigdelset(&mask, SIGFPE);
109-
r |= sigdelset(&mask, SIGILL);
110-
r |= sigdelset(&mask, SIGKILL);
111-
r |= sigdelset(&mask, SIGSEGV);
112-
r |= sigdelset(&mask, SIGSTOP);
113-
r |= sigdelset(&mask, SIGSYS);
114-
r |= sigdelset(&mask, SIGTRAP);
115-
116-
r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask);
117-
return r;
118-
}
119-
120-
#define _subprocess_precondition(__cond) do { \
121-
int eval = (__cond); \
122-
if (!eval) { \
123-
__builtin_trap(); \
124-
} \
125-
} while(0)
126-
127-
#if __DARWIN_NSIG
128-
# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG
129-
#else
130-
# define _SUBPROCESS_SIG_MAX 32
131-
#endif
132-
13390

13491
// MARK: - Darwin (posix_spawn)
13592
#if TARGET_OS_MAC
@@ -351,6 +308,100 @@ static int _clone3(int *pidfd) {
351308
return syscall(SYS_clone3, &args, sizeof(args));
352309
}
353310

311+
static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER;
312+
313+
static int _subprocess_make_critical_mask(sigset_t *old_mask) {
314+
sigset_t mask;
315+
int r = 0;
316+
r |= sigfillset(&mask);
317+
r |= sigdelset(&mask, SIGABRT);
318+
r |= sigdelset(&mask, SIGBUS);
319+
r |= sigdelset(&mask, SIGFPE);
320+
r |= sigdelset(&mask, SIGILL);
321+
r |= sigdelset(&mask, SIGKILL);
322+
r |= sigdelset(&mask, SIGSEGV);
323+
r |= sigdelset(&mask, SIGSTOP);
324+
r |= sigdelset(&mask, SIGSYS);
325+
r |= sigdelset(&mask, SIGTRAP);
326+
327+
r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask);
328+
return r;
329+
}
330+
331+
#define _subprocess_precondition(__cond) do { \
332+
int eval = (__cond); \
333+
if (!eval) { \
334+
__builtin_trap(); \
335+
} \
336+
} while(0)
337+
338+
#if __DARWIN_NSIG
339+
# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG
340+
#else
341+
# define _SUBPROCESS_SIG_MAX 32
342+
#endif
343+
344+
int _shims_snprintf(
345+
char * _Nonnull str,
346+
int len,
347+
const char * _Nonnull format,
348+
char * _Nonnull str1,
349+
char * _Nonnull str2
350+
) {
351+
return snprintf(str, len, format, str1, str2);
352+
}
353+
354+
static int _positive_int_parse(const char *str) {
355+
char *end;
356+
long value = strtol(str, &end, 10);
357+
if (end == str) {
358+
// No digits found
359+
return -1;
360+
}
361+
if (errno == ERANGE || val <= 0 || val > INT_MAX) {
362+
// Out of range
363+
return -1;
364+
}
365+
return (int)value;
366+
}
367+
368+
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
369+
int highest_fd_so_far = 0;
370+
DIR *dir_ptr = opendir(fd_dir);
371+
if (dir_ptr == NULL) {
372+
return -1;
373+
}
374+
375+
struct dirent *dir_entry = NULL;
376+
while ((dir_entry = readdir(dir_ptr)) != NULL) {
377+
char *entry_name = dir_entry->d_name;
378+
int number = _positive_int_parse(entry_name);
379+
if (number > (long)highest_fd_so_far) {
380+
highest_fd_so_far = number;
381+
}
382+
}
383+
384+
closedir(dir_ptr);
385+
return highest_fd_so_far;
386+
}
387+
388+
static int _highest_possibly_open_fd(void) {
389+
#if defined(__APPLE__)
390+
int hi = _highest_possibly_open_fd_dir("/dev/fd");
391+
if (hi < 0) {
392+
hi = getdtablesize();
393+
}
394+
#elif defined(__linux__)
395+
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
396+
if (hi < 0) {
397+
hi = getdtablesize();
398+
}
399+
#else
400+
int hi = getdtablesize();
401+
#endif
402+
return hi;
403+
}
404+
354405
int _subprocess_fork_exec(
355406
pid_t * _Nonnull pid,
356407
int * _Nonnull pidfd,
@@ -410,7 +461,7 @@ int _subprocess_fork_exec(
410461
_subprocess_precondition(rc == 0);
411462
// Block all signals on this thread
412463
sigset_t old_sigmask;
413-
rc = _subprocess_block_everything_but_something_went_seriously_wrong_signals(&old_sigmask);
464+
rc = _subprocess_make_critical_mask(&old_sigmask);
414465
if (rc != 0) {
415466
close(pipefd[0]);
416467
close(pipefd[1]);
@@ -530,20 +581,22 @@ int _subprocess_fork_exec(
530581
if (rc < 0) {
531582
write_error_and_exit;
532583
}
533-
534-
// Close parent side
535-
if (file_descriptors[1] >= 0) {
536-
rc = close(file_descriptors[1]);
537-
}
538-
if (file_descriptors[3] >= 0) {
539-
rc = close(file_descriptors[3]);
540-
}
541-
if (file_descriptors[5] >= 0) {
542-
rc = close(file_descriptors[5]);
543-
}
544-
545-
if (rc < 0) {
546-
write_error_and_exit;
584+
// Close all other file descriptors
585+
rc = -1;
586+
errno = ENOSYS;
587+
#if __has_include(<linux/close_range.h>) || defined(__FreeBSD__)
588+
// We must NOT close pipefd[1] for writing errors
589+
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0);
590+
rc |= close_range(pipefd[1] + 1, ~0U, 0);
591+
#endif
592+
if (rc != 0) {
593+
// close_range failed (or doesn't exist), fall back to close()
594+
for (int fd = STDERR_FILENO + 1; fd < _highest_possibly_open_fd(); fd++) {
595+
// We must NOT close pipefd[1] for writing errors
596+
if (fd != pipefd[1]) {
597+
close(fd);
598+
}
599+
}
547600
}
548601

549602
// Run custom configuratior
@@ -621,8 +674,6 @@ int _subprocess_fork_exec(
621674

622675
#endif // TARGET_OS_LINUX
623676

624-
#endif // !TARGET_OS_WINDOWS
625-
626677
#pragma mark - Environment Locking
627678

628679
#if __has_include(<libc_private.h>)

Tests/SubprocessTests/SubprocessTests+Unix.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,35 @@ extension SubprocessUnixTests {
952952
}
953953
try FileManager.default.removeItem(at: testFilePath)
954954
}
955+
956+
@Test func testDoesNotInheritRandomFileDescriptorsByDefault() async throws {
957+
// This tests makes sure POSIX_SPAWN_CLOEXEC_DEFAULT works on all platforms
958+
let pipe = try FileDescriptor.ssp_pipe()
959+
defer {
960+
close(pipe.readEnd.rawValue)
961+
close(pipe.writeEnd.rawValue)
962+
}
963+
let writeFd = pipe.writeEnd.rawValue
964+
let result = try await Subprocess.run(
965+
.path("/bin/sh"),
966+
arguments: ["-c", "echo hello from child >&\(writeFd); echo wrote into \(writeFd), echo exit code $?"],
967+
output: .string,
968+
error: .string
969+
)
970+
close(pipe.writeEnd.rawValue)
971+
972+
#expect(result.terminationStatus.isSuccess)
973+
#expect(
974+
result.standardOutput?.trimmingCharacters(in: .whitespacesAndNewlines) ==
975+
"wrote into \(writeFd), echo exit code 1"
976+
)
977+
// Depending on the platform, standard error should be something like
978+
// `/bin/bash: 7: Bad file descriptor
979+
#expect(!result.standardError!.isEmpty)
980+
let nonInherited = try await pipe.readEnd.readUntilEOF(upToLength: .max)
981+
// We should have read nothing because the pipe is not inherited
982+
#expect(nonInherited.isEmpty)
983+
}
955984
}
956985

957986
// MARK: - Utils

0 commit comments

Comments
 (0)