diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c index cf7ca66..43e8c35 100644 --- a/Sources/_SubprocessCShims/process_shims.c +++ b/Sources/_SubprocessCShims/process_shims.c @@ -31,9 +31,15 @@ #include #include #include - +#include #include +#if __has_include() +#include +#endif + +#endif // TARGET_OS_WINDOWS + #if __has_include() #include #elif defined(_WIN32) @@ -47,6 +53,7 @@ extern char **environ; #include #endif +#if !TARGET_OS_WINDOWS int _was_process_exited(int status) { return WIFEXITED(status); } @@ -67,19 +74,8 @@ int _was_process_suspended(int status) { return WIFSTOPPED(status); } -#if TARGET_OS_LINUX -#include +#endif // !TARGET_OS_WINDOWS -int _shims_snprintf( - char * _Nonnull str, - int len, - const char * _Nonnull format, - char * _Nonnull str1, - char * _Nonnull str2 -) { - return snprintf(str, len, format, str1, str2); -} -#endif #if __has_include() vm_size_t _subprocess_vm_size(void) { @@ -88,40 +84,6 @@ vm_size_t _subprocess_vm_size(void) { } #endif -// MARK: - Private Helpers -static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER; - -static int _subprocess_block_everything_but_something_went_seriously_wrong_signals(sigset_t *old_mask) { - sigset_t mask; - int r = 0; - r |= sigfillset(&mask); - r |= sigdelset(&mask, SIGABRT); - r |= sigdelset(&mask, SIGBUS); - r |= sigdelset(&mask, SIGFPE); - r |= sigdelset(&mask, SIGILL); - r |= sigdelset(&mask, SIGKILL); - r |= sigdelset(&mask, SIGSEGV); - r |= sigdelset(&mask, SIGSTOP); - r |= sigdelset(&mask, SIGSYS); - r |= sigdelset(&mask, SIGTRAP); - - r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask); - return r; -} - -#define _subprocess_precondition(__cond) do { \ - int eval = (__cond); \ - if (!eval) { \ - __builtin_trap(); \ - } \ -} while(0) - -#if __DARWIN_NSIG -# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG -#else -# define _SUBPROCESS_SIG_MAX 32 -#endif - // MARK: - Darwin (posix_spawn) #if TARGET_OS_MAC @@ -303,154 +265,99 @@ int _subprocess_spawn( #define __GLIBC_PREREQ(maj, min) 0 #endif -#if _POSIX_SPAWN -static int _subprocess_is_addchdir_np_available() { -#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) - // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: - // - Amazon Linux 2 (EoL mid-2025) - return 0; -#elif defined(__OpenBSD__) || defined(__QNX__) - // Currently missing as of: - // - OpenBSD 7.5 (April 2024) - // - QNX 8 (December 2023) - return 0; -#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) - // Pre-standard posix_spawn_file_actions_addchdir_np version available in: - // - Solaris 11.3 (October 2015) - // - Glibc 2.29 (February 2019) - // - macOS 10.15 (October 2019) - // - musl 1.1.24 (October 2019) - // - FreeBSD 13.1 (May 2022) - // - Android 14 (October 2023) - return 1; -#else - // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: - // - Solaris 11.4 (August 2018) - // - NetBSD 10.0 (March 2024) - return 1; -#endif +static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER; + +static int _subprocess_make_critical_mask(sigset_t *old_mask) { + sigset_t mask; + int r = 0; + r |= sigfillset(&mask); + r |= sigdelset(&mask, SIGABRT); + r |= sigdelset(&mask, SIGBUS); + r |= sigdelset(&mask, SIGFPE); + r |= sigdelset(&mask, SIGILL); + r |= sigdelset(&mask, SIGKILL); + r |= sigdelset(&mask, SIGSEGV); + r |= sigdelset(&mask, SIGSTOP); + r |= sigdelset(&mask, SIGSYS); + r |= sigdelset(&mask, SIGTRAP); + + r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask); + return r; } -static int _subprocess_addchdir_np( - posix_spawn_file_actions_t *file_actions, - const char * __restrict path -) { -#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) - // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: - // - Amazon Linux 2 (EoL mid-2025) - return ENOTSUP; -#elif defined(__ANDROID__) && __ANDROID_API__ < 34 - // Android versions prior to 14 (API level 34) don't support posix_spawn_file_actions_addchdir_np - return ENOTSUP; -#elif defined(__OpenBSD__) || defined(__QNX__) - // Currently missing as of: - // - OpenBSD 7.5 (April 2024) - // - QNX 8 (December 2023) - return ENOTSUP; -#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || defined(__ANDROID__) || defined(__musl__) - // Pre-standard posix_spawn_file_actions_addchdir_np version available in: - // - Solaris 11.3 (October 2015) - // - Glibc 2.29 (February 2019) - // - macOS 10.15 (October 2019) - // - musl 1.1.24 (October 2019) - // - FreeBSD 13.1 (May 2022) - // - Android 14 (API level 34) (October 2023) - return posix_spawn_file_actions_addchdir_np(file_actions, path); +#define _subprocess_precondition(__cond) do { \ + int eval = (__cond); \ + if (!eval) { \ + __builtin_trap(); \ + } \ +} while(0) + +#if __DARWIN_NSIG +# define _SUBPROCESS_SIG_MAX __DARWIN_NSIG #else - // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: - // - Solaris 11.4 (August 2018) - // - NetBSD 10.0 (March 2024) - return posix_spawn_file_actions_addchdir(file_actions, path); +# define _SUBPROCESS_SIG_MAX 32 #endif -} -static int _subprocess_posix_spawn_fallback( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - gid_t * _Nullable process_group_id +int _shims_snprintf( + char * _Nonnull str, + int len, + const char * _Nonnull format, + char * _Nonnull str1, + char * _Nonnull str2 ) { - // Setup stdin, stdout, and stderr - posix_spawn_file_actions_t file_actions; - - int rc = posix_spawn_file_actions_init(&file_actions); - if (rc != 0) { return rc; } - if (file_descriptors[0] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[0], STDIN_FILENO - ); - if (rc != 0) { return rc; } + return snprintf(str, len, format, str1, str2); +} + +static int _positive_int_parse(const char *str) { + char *end; + long value = strtol(str, &end, 10); + if (end == str) { + // No digits found + return -1; } - if (file_descriptors[2] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[2], STDOUT_FILENO - ); - if (rc != 0) { return rc; } + if (errno == ERANGE || val <= 0 || val > INT_MAX) { + // Out of range + return -1; } - if (file_descriptors[4] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[4], STDERR_FILENO - ); - if (rc != 0) { return rc; } + return (int)value; +} + +static int _highest_possibly_open_fd_dir(const char *fd_dir) { + int highest_fd_so_far = 0; + DIR *dir_ptr = opendir(fd_dir); + if (dir_ptr == NULL) { + return -1; } - // Setup working directory - if (working_directory != NULL) { - rc = _subprocess_addchdir_np(&file_actions, working_directory); - if (rc != 0) { - return rc; + + struct dirent *dir_entry = NULL; + while ((dir_entry = readdir(dir_ptr)) != NULL) { + char *entry_name = dir_entry->d_name; + int number = _positive_int_parse(entry_name); + if (number > (long)highest_fd_so_far) { + highest_fd_so_far = number; } } - // Close parent side - if (file_descriptors[1] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]); - if (rc != 0) { return rc; } - } - if (file_descriptors[3] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]); - if (rc != 0) { return rc; } - } - if (file_descriptors[5] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]); - if (rc != 0) { return rc; } - } + closedir(dir_ptr); + return highest_fd_so_far; +} - // Setup spawnattr - posix_spawnattr_t spawn_attr; - rc = posix_spawnattr_init(&spawn_attr); - if (rc != 0) { return rc; } - // Masks - sigset_t no_signals; - sigset_t all_signals; - sigemptyset(&no_signals); - sigfillset(&all_signals); - rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals); - if (rc != 0) { return rc; } - rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals); - if (rc != 0) { return rc; } - // Flags - short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; - if (process_group_id != NULL) { - flags |= POSIX_SPAWN_SETPGROUP; - rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id); - if (rc != 0) { return rc; } +static int _highest_possibly_open_fd(void) { +#if defined(__APPLE__) + int hi = _highest_possibly_open_fd_dir("/dev/fd"); + if (hi < 0) { + hi = getdtablesize(); } - rc = posix_spawnattr_setflags(&spawn_attr, flags); - - // Spawn! - rc = posix_spawn( - pid, exec_path, - &file_actions, &spawn_attr, - args, env - ); - posix_spawn_file_actions_destroy(&file_actions); - posix_spawnattr_destroy(&spawn_attr); - return rc; +#elif defined(__linux__) + int hi = _highest_possibly_open_fd_dir("/proc/self/fd"); + if (hi < 0) { + hi = getdtablesize(); + } +#else + int hi = getdtablesize(); +#endif + return hi; } -#endif // _POSIX_SPAWN int _subprocess_fork_exec( pid_t * _Nonnull pid, @@ -471,32 +378,6 @@ int _subprocess_fork_exec( close(pipefd[1]); \ _exit(EXIT_FAILURE) - int require_pre_fork = _subprocess_is_addchdir_np_available() == 0 || - uid != NULL || - gid != NULL || - process_group_id != NULL || - (number_of_sgroups > 0 && sgroups != NULL) || - create_session || - configurator != NULL; - -#if _POSIX_SPAWN - // If posix_spawn is available on this platform and - // we do not require prefork, use posix_spawn if possible. - // - // (Glibc's posix_spawn does not support - // `POSIX_SPAWN_SETEXEC` therefore we have to keep - // using fork/exec if `require_pre_fork` is true. - if (require_pre_fork == 0) { - return _subprocess_posix_spawn_fallback( - pid, exec_path, - working_directory, - file_descriptors, - args, env, - process_group_id - ); - } -#endif - // Setup pipe to catch exec failures from child int pipefd[2]; if (pipe(pipefd) != 0) { @@ -536,7 +417,7 @@ int _subprocess_fork_exec( _subprocess_precondition(rc == 0); // Block all signals on this thread sigset_t old_sigmask; - rc = _subprocess_block_everything_but_something_went_seriously_wrong_signals(&old_sigmask); + rc = _subprocess_make_critical_mask(&old_sigmask); if (rc != 0) { close(pipefd[0]); close(pipefd[1]); @@ -557,8 +438,6 @@ int _subprocess_fork_exec( if (childPid == 0) { // Child process - close(pipefd[0]); // Close unused read end - // Reset signal handlers for (int signo = 1; signo < _SUBPROCESS_SIG_MAX; signo++) { if (signo == SIGKILL || signo == SIGSTOP) { @@ -620,40 +499,46 @@ int _subprocess_fork_exec( // Bind stdin, stdout, and stderr if (file_descriptors[0] >= 0) { rc = dup2(file_descriptors[0], STDIN_FILENO); - if (rc < 0) { - write_error_and_exit; - } + } else { + rc = close(STDIN_FILENO); } + if (rc < 0) { + write_error_and_exit; + } + if (file_descriptors[2] >= 0) { rc = dup2(file_descriptors[2], STDOUT_FILENO); - if (rc < 0) { - write_error_and_exit; - } + } else { + rc = close(STDOUT_FILENO); } + if (rc < 0) { + write_error_and_exit; + } + if (file_descriptors[4] >= 0) { rc = dup2(file_descriptors[4], STDERR_FILENO); - if (rc < 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } + } else { + rc = close(STDERR_FILENO); } - // Close parent side - if (file_descriptors[1] >= 0) { - rc = close(file_descriptors[1]); - } - if (file_descriptors[3] >= 0) { - rc = close(file_descriptors[3]); - } - if (file_descriptors[5] >= 0) { - rc = close(file_descriptors[5]); + if (rc < 0) { + write_error_and_exit; } + // Close all other file descriptors + rc = -1; + errno = ENOSYS; +#if __has_include() || defined(__FreeBSD__) + // We must NOT close pipefd[1] for writing errors + rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0); + rc |= close_range(pipefd[1] + 1, ~0U, 0); +#endif if (rc != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); + // close_range failed (or doesn't exist), fall back to close() + for (int fd = STDERR_FILENO + 1; fd < _highest_possibly_open_fd(); fd++) { + // We must NOT close pipefd[1] for writing errors + if (fd != pipefd[1]) { + close(fd); + } + } } // Run custom configuratior if (configurator != NULL) { @@ -708,8 +593,6 @@ int _subprocess_fork_exec( #endif // TARGET_OS_LINUX -#endif // !TARGET_OS_WINDOWS - #pragma mark - Environment Locking #if __has_include() diff --git a/Tests/SubprocessTests/SubprocessTests+Unix.swift b/Tests/SubprocessTests/SubprocessTests+Unix.swift index a50c4e8..5a31670 100644 --- a/Tests/SubprocessTests/SubprocessTests+Unix.swift +++ b/Tests/SubprocessTests/SubprocessTests+Unix.swift @@ -969,6 +969,35 @@ extension SubprocessUnixTests { } try FileManager.default.removeItem(at: testFilePath) } + + @Test func testDoesNotInheritRandomFileDescriptorsByDefault() async throws { + // This tests makes sure POSIX_SPAWN_CLOEXEC_DEFAULT works on all platforms + let pipe = try FileDescriptor.ssp_pipe() + defer { + close(pipe.readEnd.rawValue) + close(pipe.writeEnd.rawValue) + } + let writeFd = pipe.writeEnd.rawValue + let result = try await Subprocess.run( + .path("/bin/sh"), + arguments: ["-c", "echo hello from child >&\(writeFd); echo wrote into \(writeFd), echo exit code $?"], + output: .string, + error: .string + ) + close(pipe.writeEnd.rawValue) + + #expect(result.terminationStatus.isSuccess) + #expect( + result.standardOutput?.trimmingCharacters(in: .whitespacesAndNewlines) == + "wrote into \(writeFd), echo exit code 1" + ) + // Depending on the platform, standard error should be something like + // `/bin/bash: 7: Bad file descriptor + #expect(!result.standardError!.isEmpty) + let nonInherited = try await pipe.readEnd.readUntilEOF(upToLength: .max) + // We should have read nothing because the pipe is not inherited + #expect(nonInherited.isEmpty) + } } // MARK: - Utils