Skip to content

Commit 0feb84b

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 8fdb554 commit 0feb84b

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
@@ -336,6 +293,100 @@ static int _clone3(int *pidfd) {
336293
return syscall(SYS_clone3, &args, sizeof(args));
337294
}
338295

296+
static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER;
297+
298+
static int _subprocess_make_critical_mask(sigset_t *old_mask) {
299+
sigset_t mask;
300+
int r = 0;
301+
r |= sigfillset(&mask);
302+
r |= sigdelset(&mask, SIGABRT);
303+
r |= sigdelset(&mask, SIGBUS);
304+
r |= sigdelset(&mask, SIGFPE);
305+
r |= sigdelset(&mask, SIGILL);
306+
r |= sigdelset(&mask, SIGKILL);
307+
r |= sigdelset(&mask, SIGSEGV);
308+
r |= sigdelset(&mask, SIGSTOP);
309+
r |= sigdelset(&mask, SIGSYS);
310+
r |= sigdelset(&mask, SIGTRAP);
311+
312+
r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask);
313+
return r;
314+
}
315+
316+
#define _subprocess_precondition(__cond) do { \
317+
int eval = (__cond); \
318+
if (!eval) { \
319+
__builtin_trap(); \
320+
} \
321+
} while(0)
322+
323+
#if __DARWIN_NSIG
324+
# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG
325+
#else
326+
# define _SUBPROCESS_SIG_MAX 32
327+
#endif
328+
329+
int _shims_snprintf(
330+
char * _Nonnull str,
331+
int len,
332+
const char * _Nonnull format,
333+
char * _Nonnull str1,
334+
char * _Nonnull str2
335+
) {
336+
return snprintf(str, len, format, str1, str2);
337+
}
338+
339+
static int _positive_int_parse(const char *str) {
340+
char *end;
341+
long value = strtol(str, &end, 10);
342+
if (end == str) {
343+
// No digits found
344+
return -1;
345+
}
346+
if (errno == ERANGE || val <= 0 || val > INT_MAX) {
347+
// Out of range
348+
return -1;
349+
}
350+
return (int)value;
351+
}
352+
353+
static int _highest_possibly_open_fd_dir(const char *fd_dir) {
354+
int highest_fd_so_far = 0;
355+
DIR *dir_ptr = opendir(fd_dir);
356+
if (dir_ptr == NULL) {
357+
return -1;
358+
}
359+
360+
struct dirent *dir_entry = NULL;
361+
while ((dir_entry = readdir(dir_ptr)) != NULL) {
362+
char *entry_name = dir_entry->d_name;
363+
int number = _positive_int_parse(entry_name);
364+
if (number > (long)highest_fd_so_far) {
365+
highest_fd_so_far = number;
366+
}
367+
}
368+
369+
closedir(dir_ptr);
370+
return highest_fd_so_far;
371+
}
372+
373+
static int _highest_possibly_open_fd(void) {
374+
#if defined(__APPLE__)
375+
int hi = _highest_possibly_open_fd_dir("/dev/fd");
376+
if (hi < 0) {
377+
hi = getdtablesize();
378+
}
379+
#elif defined(__linux__)
380+
int hi = _highest_possibly_open_fd_dir("/proc/self/fd");
381+
if (hi < 0) {
382+
hi = getdtablesize();
383+
}
384+
#else
385+
int hi = getdtablesize();
386+
#endif
387+
return hi;
388+
}
389+
339390
int _subprocess_fork_exec(
340391
pid_t * _Nonnull pid,
341392
int * _Nonnull pidfd,
@@ -395,7 +446,7 @@ int _subprocess_fork_exec(
395446
_subprocess_precondition(rc == 0);
396447
// Block all signals on this thread
397448
sigset_t old_sigmask;
398-
rc = _subprocess_block_everything_but_something_went_seriously_wrong_signals(&old_sigmask);
449+
rc = _subprocess_make_critical_mask(&old_sigmask);
399450
if (rc != 0) {
400451
close(pipefd[0]);
401452
close(pipefd[1]);
@@ -515,20 +566,22 @@ int _subprocess_fork_exec(
515566
if (rc < 0) {
516567
write_error_and_exit;
517568
}
518-
519-
// Close parent side
520-
if (file_descriptors[1] >= 0) {
521-
rc = close(file_descriptors[1]);
522-
}
523-
if (file_descriptors[3] >= 0) {
524-
rc = close(file_descriptors[3]);
525-
}
526-
if (file_descriptors[5] >= 0) {
527-
rc = close(file_descriptors[5]);
528-
}
529-
530-
if (rc < 0) {
531-
write_error_and_exit;
569+
// Close all other file descriptors
570+
rc = -1;
571+
errno = ENOSYS;
572+
#if __has_include(<linux/close_range.h>) || defined(__FreeBSD__)
573+
// We must NOT close pipefd[1] for writing errors
574+
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0);
575+
rc |= close_range(pipefd[1] + 1, ~0U, 0);
576+
#endif
577+
if (rc != 0) {
578+
// close_range failed (or doesn't exist), fall back to close()
579+
for (int fd = STDERR_FILENO + 1; fd < _highest_possibly_open_fd(); fd++) {
580+
// We must NOT close pipefd[1] for writing errors
581+
if (fd != pipefd[1]) {
582+
close(fd);
583+
}
584+
}
532585
}
533586

534587
// Run custom configuratior
@@ -606,8 +659,6 @@ int _subprocess_fork_exec(
606659

607660
#endif // TARGET_OS_LINUX
608661

609-
#endif // !TARGET_OS_WINDOWS
610-
611662
#pragma mark - Environment Locking
612663

613664
#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)