From 4a1ee9528e1a37b118ea434a5a66ee1761cf0aca Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Thu, 20 Aug 2026 11:31:15 +0200 Subject: [PATCH 1/4] Enable the GNU feature test macro on Linux The vendored subprocess.h calls pipe2 and posix_spawn_file_actions_addchdir_np, which glibc only declares under _GNU_SOURCE. The test suite compiles most of its translation units at strict -std=c99, -std=c11 and -std=c++XX, where that macro is not set for us, so those calls reach the compiler undeclared and -Werror=implicit-function-declaration stops the build. This is what subprocess.h's own test/CMakeLists.txt does, for the same reason. --- test/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6a215fc..712caa7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,6 +30,13 @@ include(CTest) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../) +# Strict -std=c99/-std=c11/-std=c++XX modes on glibc hide POSIX/GNU +# declarations the vendored subprocess.h uses unless feature test macros are +# enabled. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_compile_definitions(_GNU_SOURCE) +endif() + # Verify that including utest.h restores the caller's MSVC warning state. # The undefined-macro warning is enabled as an error before the include and # deliberately triggered afterwards. The probe must therefore fail to compile; From 1cf231dab186baa2cc9f72498a5fd7e1eef4985c Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Thu, 20 Aug 2026 11:32:07 +0200 Subject: [PATCH 2/4] Refresh the vendored subprocess.h to sheredom/subprocess.h@8a4715c The old copy was a pristine snapshot of subprocess.h@8671cee and is twelve commits behind. Since then upstream gained the SUBPROCESS_HAVE_CWD and SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS probes, Windows argument quoting and handle-inheritance fixes, a fork/exec path for platforms whose posix_spawn cannot change directory, and pipe ends kept off the standard descriptors. Pristine snapshot again, no local changes. --- test/subprocess.h | 487 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 452 insertions(+), 35 deletions(-) diff --git a/test/subprocess.h b/test/subprocess.h index 5e80902..b312912 100644 --- a/test/subprocess.h +++ b/test/subprocess.h @@ -107,7 +107,8 @@ enum subprocess_error_e { subprocess_error_permission_denied = -5, subprocess_error_no_memory = -6, subprocess_error_pipe = -7, - subprocess_error_spawn = -8 + subprocess_error_spawn = -8, + subprocess_error_not_supported = -9 }; #if defined(__cplusplus) @@ -274,6 +275,80 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #include #endif +#if defined(__NetBSD__) +/* pulls in , which hides the PRI macros from C++ + before C++11 behind an include guard, so this has to be set before it. */ +#if defined(__cplusplus) && !defined(__STDC_FORMAT_MACROS) +#define __STDC_FORMAT_MACROS 1 +#endif +#include +#endif + +/* Which spelling of the chdir file action the platform provides, if any. + POSIX 2024 standardised posix_spawn_file_actions_addchdir; implementations + that shipped it earlier called it ..._np. macOS 26 and NetBSD 10 use the + standard name, glibc 2.29+, macOS 10.15+ and FreeBSD 13.1+ use the _np name, + and AIX, NetBSD 9 and older, and OpenBSD provide neither. */ +#if !defined(SUBPROCESS_ADDCHDIR_IS_POSIX) +#if (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000) || \ + (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000) +#define SUBPROCESS_ADDCHDIR_IS_POSIX 1 +#else +#define SUBPROCESS_ADDCHDIR_IS_POSIX 0 +#endif +#endif + +/* Whether to launch the child with fork()+exec() instead of posix_spawn(), + for platforms with no posix_spawn_file_actions_addchdir under either + spelling: the child chdir()s before exec, and a close-on-exec pipe carries + exec's errno back. Define this yourself to force either implementation. */ +#if !defined(SUBPROCESS_SPAWN_VIA_FORK) +#if defined(_AIX) || defined(__OpenBSD__) || \ + (defined(__NetBSD__) && (__NetBSD_Version__ < 1000000000)) +#define SUBPROCESS_SPAWN_VIA_FORK 1 +#else +#define SUBPROCESS_SPAWN_VIA_FORK 0 +#endif +#endif + +/* Whether subprocess_create_ex can honour process_cwd. glibc only gained + posix_spawn_file_actions_addchdir_np in 2.29, and macOS in 10.15; the SDKs + mark it unavailable on iOS, tvOS and watchOS, where the undefined version + macro folds to 0 and so answers correctly. Define this yourself to override + the detection, for instance on musl older than 1.1.24. */ +#if !defined(SUBPROCESS_HAVE_CWD) +#if SUBPROCESS_SPAWN_VIA_FORK +#define SUBPROCESS_HAVE_CWD 1 +#elif defined(__GLIBC__) +#if __GLIBC_PREREQ(2, 29) +#define SUBPROCESS_HAVE_CWD 1 +#else +#define SUBPROCESS_HAVE_CWD 0 +#endif +#elif defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED < 101500 +#define SUBPROCESS_HAVE_CWD 0 +#else +#define SUBPROCESS_HAVE_CWD 1 +#endif +#endif + +/* Whether a failed exec is reported back to the caller. The fork() path always + reports it through its error pipe. glibc's posix_spawn only started doing so + in 2.24; before that the child silently exits with 127. */ +#if !defined(SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS) +#if SUBPROCESS_SPAWN_VIA_FORK +#define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 1 +#elif defined(__GLIBC__) +#if __GLIBC_PREREQ(2, 24) +#define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 1 +#else +#define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 0 +#endif +#else +#define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 1 +#endif +#endif + #if defined(_WIN32) #include @@ -308,6 +383,14 @@ typedef intptr_t subprocess_intptr_t; typedef size_t subprocess_size_t; #endif +/* SIZE_T is ULONG_PTR, which is not size_t: on Win32 both are 32 bits wide but + unsigned long and unsigned int are still distinct types. */ +#ifdef _WIN64 +typedef subprocess_size_t subprocess_ulongptr_t; +#else +typedef unsigned long subprocess_ulongptr_t; +#endif + #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wreserved-identifier" @@ -317,6 +400,7 @@ typedef struct _PROCESS_INFORMATION *LPPROCESS_INFORMATION; typedef struct _SECURITY_ATTRIBUTES *LPSECURITY_ATTRIBUTES; typedef struct _STARTUPINFOW *LPSTARTUPINFOW; typedef struct _OVERLAPPED *LPOVERLAPPED; +typedef struct _PROC_THREAD_ATTRIBUTE_LIST *LPPROC_THREAD_ATTRIBUTE_LIST; #ifdef __clang__ #pragma clang diagnostic pop @@ -368,6 +452,11 @@ struct subprocess_startup_info_s { void *hStdError; }; +struct subprocess_startup_info_ex_s { + struct subprocess_startup_info_s startupInfo; + void *attributeList; +}; + struct subprocess_overlapped_s { uintptr_t Internal; uintptr_t InternalHigh; @@ -417,6 +506,14 @@ __declspec(dllimport) int __stdcall CreateProcessW( const subprocess_wchar_t *, subprocess_wchar_t *, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, int, unsigned long, void *, const subprocess_wchar_t *, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall +InitializeProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, + unsigned long, subprocess_ulongptr_t *); +__declspec(dllimport) int __stdcall UpdateProcThreadAttribute( + LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, subprocess_ulongptr_t, void *, + subprocess_ulongptr_t, void *, subprocess_ulongptr_t *); +__declspec(dllimport) void __stdcall +DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST); __declspec(dllimport) int __stdcall MultiByteToWideChar( unsigned int, unsigned long, const char *, int, subprocess_wchar_t *, int); __declspec(dllimport) int __stdcall CloseHandle(void *); @@ -539,6 +636,8 @@ int subprocess_error_from_errno(int error) { case ENFILE: case ENOMEM: return subprocess_error_no_memory; + case ENOSYS: + return subprocess_error_not_supported; default: return subprocess_error_unknown; } @@ -631,12 +730,104 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { } #endif +#if !defined(_WIN32) +/* Move a pipe end off 0, 1 or 2. Duplicating a descriptor onto itself is a + no-op, so a pipe end already sitting on a standard descriptor would keep its + FD_CLOEXEC and be closed by exec, leaving the child without that stream. */ +static int subprocess_fds_above_std(int fds[2]) { + int fd_flags; + int index; + int moved; + int saved_errno; + + for (index = 0; index < 2; index++) { + if (fds[index] > STDERR_FILENO) { + continue; + } + + moved = fcntl(fds[index], F_DUPFD, STDERR_FILENO + 1); + if (-1 != moved) { + fd_flags = fcntl(moved, F_GETFD, 0); + if ((-1 == fd_flags) || + (-1 == fcntl(moved, F_SETFD, fd_flags | FD_CLOEXEC))) { + saved_errno = errno; + close(moved); + errno = saved_errno; + moved = -1; + } + } + + if (-1 == moved) { + saved_errno = errno; + close(fds[0]); + close(fds[1]); + fds[0] = -1; + fds[1] = -1; + errno = saved_errno; + return -1; + } + + close(fds[index]); + fds[index] = moved; + } + + return 0; +} + +/* Create pipes with close-on-exec set so later subprocesses do not inherit + descriptors belonging to subprocesses which are already running. */ +static int subprocess_pipe_cloexec(int fds[2]) { + int fd_flags; + int index; + int saved_errno; + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || \ + defined(__OpenBSD__) || defined(__DragonFly__) || \ + (defined(__sun) && defined(__SVR4)) + if (0 == pipe2(fds, O_CLOEXEC)) { + return subprocess_fds_above_std(fds); + } + + /* Older kernels can lack pipe2 even when the C library declares it. */ + if (ENOSYS != errno) { + return -1; + } +#endif + + if (0 != pipe(fds)) { + return -1; + } + + for (index = 0; index < 2; index++) { + fd_flags = fcntl(fds[index], F_GETFD, 0); + if ((-1 == fd_flags) || + (-1 == fcntl(fds[index], F_SETFD, fd_flags | FD_CLOEXEC))) { + saved_errno = errno; + close(fds[0]); + close(fds[1]); + fds[0] = -1; + fds[1] = -1; + errno = saved_errno; + return -1; + } + } + + return subprocess_fds_above_std(fds); +} +#endif + int subprocess_create(const char *const commandLine[], int options, struct subprocess_s *const out_process) { return subprocess_create_ex(commandLine, options, SUBPROCESS_NULL, SUBPROCESS_NULL, out_process); } +#if SUBPROCESS_SPAWN_VIA_FORK +/* Not every platform declares execvpe: AIX exports it from libc without ever + naming it in a header, and glibc hides it behind _GNU_SOURCE. */ +extern int execvpe(const char *, char *const *, char *const *); +#endif + int subprocess_create_ex(const char *const commandLine[], int options, const char *const environment[], const char *const process_cwd, @@ -653,8 +844,10 @@ int subprocess_create_ex(const char *const commandLine[], int options, int wide_len; int i, j; int need_quoting; + subprocess_size_t bs_run; unsigned long flags = 0; unsigned long last_error = 0; + int attribute_list_initialized = 0; int result = subprocess_error_unknown; const unsigned int codePageUtf8 = 65001; const unsigned long mbErrInvalidChars = 0x00000008; @@ -662,6 +855,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, const unsigned long handleFlagInherit = 0x00000001; const unsigned long createNoWindow = 0x08000000; const unsigned long createUnicodeEnvironment = 0x00000400; + const unsigned long extendedStartupInfoPresent = 0x00080000; + const subprocess_size_t procThreadAttributeHandleList = 0x00020002; struct subprocess_subprocess_information_s processInfo = {SUBPROCESS_NULL, SUBPROCESS_NULL, 0, 0}; @@ -669,6 +864,11 @@ int subprocess_create_ex(const char *const commandLine[], int options, SUBPROCESS_NULL, 1}; subprocess_wchar_t empty_environment[2] = {0, 0}; subprocess_wchar_t *used_environment = SUBPROCESS_NULL; + subprocess_ulongptr_t attribute_list_size = 0; + subprocess_size_t inherited_handle_count = 0; + LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = SUBPROCESS_NULL; + void *inherited_handles[3]; + struct subprocess_startup_info_ex_s startInfoEx; struct subprocess_startup_info_s startInfo = {0, SUBPROCESS_NULL, SUBPROCESS_NULL, @@ -906,25 +1106,29 @@ int subprocess_create_ex(const char *const commandLine[], int options, len++; // Quote the argument if it has a space in it - if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || - commandLine[i][0] == SUBPROCESS_NULL) + need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || + commandLine[i][0] == SUBPROCESS_NULL; + if (need_quoting) len += 2; + bs_run = 0; for (j = 0; '\0' != commandLine[i][j]; j++) { - switch (commandLine[i][j]) { - default: - break; - case '\\': - if (commandLine[i][j + 1] == '"') { - len++; - } + len++; - break; - case '"': - len++; - break; + if ('\\' == commandLine[i][j]) { + bs_run++; + } else { + if ('"' == commandLine[i][j]) { + // Duplicate the preceding run and escape the quote. + len += bs_run + 1; + } + bs_run = 0; } - len++; + } + + if (need_quoting) { + // Duplicate trailing slashes before the generated closing quote. + len += bs_run; } } @@ -949,22 +1153,29 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined[len++] = '"'; } - for (j = 0; '\0' != commandLine[i][j]; j++) { - switch (commandLine[i][j]) { - default: - break; - case '\\': - if (commandLine[i][j + 1] == '"') { - commandLineCombined[len++] = '\\'; - } + for (j = 0; '\0' != commandLine[i][j];) { + bs_run = 0; + while ('\\' == commandLine[i][j]) { + bs_run++; + j++; + } - break; - case '"': + if ('"' == commandLine[i][j]) { + // 2n + 1 slashes preserve n slashes and escape the quote. + bs_run = (bs_run * 2) + 1; + } else if ('\0' == commandLine[i][j] && need_quoting) { + // 2n slashes preserve n slashes before the closing quote. + bs_run *= 2; + } + + while (bs_run > 0) { commandLineCombined[len++] = '\\'; - break; + bs_run--; } - commandLineCombined[len++] = commandLine[i][j]; + if ('\0' != commandLine[i][j]) { + commandLineCombined[len++] = commandLine[i][j++]; + } } if (need_quoting) { commandLineCombined[len++] = '"'; @@ -1032,6 +1243,44 @@ int subprocess_create_ex(const char *const commandLine[], int options, } } + /* Restrict inheritance to this subprocess's standard streams. Without a + handle list, concurrent subprocess_create calls can inherit each other's + temporarily-inheritable child pipe handles. */ + inherited_handles[inherited_handle_count++] = startInfo.hStdInput; + inherited_handles[inherited_handle_count++] = startInfo.hStdOutput; + if (startInfo.hStdError != startInfo.hStdOutput) { + inherited_handles[inherited_handle_count++] = startInfo.hStdError; + } + + InitializeProcThreadAttributeList(SUBPROCESS_NULL, 1, 0, + &attribute_list_size); + if (0 == attribute_list_size) { + result = subprocess_error_spawn; + goto cleanup; + } + + attribute_list = SUBPROCESS_PTR_CAST(LPPROC_THREAD_ATTRIBUTE_LIST, + _alloca(attribute_list_size)); + if (!attribute_list || !InitializeProcThreadAttributeList( + attribute_list, 1, 0, &attribute_list_size)) { + result = subprocess_error_spawn; + goto cleanup; + } + attribute_list_initialized = 1; + + if (!UpdateProcThreadAttribute( + attribute_list, 0, procThreadAttributeHandleList, inherited_handles, + inherited_handle_count * sizeof(inherited_handles[0]), + SUBPROCESS_NULL, SUBPROCESS_NULL)) { + result = subprocess_error_spawn; + goto cleanup; + } + + startInfoEx.startupInfo = startInfo; + startInfoEx.startupInfo.cb = sizeof(startInfoEx); + startInfoEx.attributeList = attribute_list; + flags |= extendedStartupInfoPresent; + if (!CreateProcessW( SUBPROCESS_NULL, commandLineCombinedWide, // command line @@ -1042,7 +1291,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, used_environment, // used environment process_cwd_wide, // use specified current directory SUBPROCESS_PTR_CAST(LPSTARTUPINFOW, - &startInfo), // STARTUPINFO pointer + &startInfoEx), // STARTUPINFOEX pointer SUBPROCESS_PTR_CAST(LPPROCESS_INFORMATION, &processInfo))) { result = subprocess_error_from_windows_error(GetLastError()); if (subprocess_error_unknown == result) { @@ -1051,6 +1300,9 @@ int subprocess_create_ex(const char *const commandLine[], int options, goto cleanup; } + DeleteProcThreadAttributeList(attribute_list); + attribute_list_initialized = 0; + out_process->hProcess = processInfo.hProcess; processInfo.hProcess = SUBPROCESS_NULL; @@ -1080,6 +1332,10 @@ int subprocess_create_ex(const char *const commandLine[], int options, cleanup: last_error = GetLastError(); + if (attribute_list_initialized) { + DeleteProcThreadAttributeList(attribute_list); + } + if (subprocess_error_unknown == result) { result = subprocess_error_from_windows_error(last_error); } @@ -1125,15 +1381,20 @@ int subprocess_create_ex(const char *const commandLine[], int options, int stderrfd[2] = {-1, -1}; int fd, fd_flags; int async_no_wait; - int actions_created = 0; int result = subprocess_error_unknown; int saved_errno = 0; - int posix_error; pid_t child = 0; extern char **environ; char *const empty_environment[1] = {SUBPROCESS_NULL}; - posix_spawn_file_actions_t actions; char *const *used_environment; +#if SUBPROCESS_SPAWN_VIA_FORK + /* Pipe used to relay the child's exec() errno back to the parent. */ + int exec_errfd[2] = {-1, -1}; +#else + int actions_created = 0; + int posix_error; + posix_spawn_file_actions_t actions; +#endif async_no_wait = subprocess_option_enable_async_no_wait == (options & subprocess_option_enable_async_no_wait); @@ -1154,13 +1415,13 @@ int subprocess_create_ex(const char *const commandLine[], int options, memset(out_process, 0, sizeof(*out_process)); - if (0 != pipe(stdinfd)) { + if (0 != subprocess_pipe_cloexec(stdinfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; } - if (0 != pipe(stdoutfd)) { + if (0 != subprocess_pipe_cloexec(stdoutfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; @@ -1168,7 +1429,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, if (subprocess_option_combined_stdout_stderr != (options & subprocess_option_combined_stdout_stderr)) { - if (0 != pipe(stderrfd)) { + if (0 != subprocess_pipe_cloexec(stderrfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; @@ -1192,6 +1453,136 @@ int subprocess_create_ex(const char *const commandLine[], int options, used_environment = empty_environment; } +#if SUBPROCESS_SPAWN_VIA_FORK + /* fork()+exec() instead of posix_spawn, so the child can chdir() first. + exec_errfd[1] is close-on-exec: a successful exec closes it and the parent + reads EOF; a failed exec writes errno through it before _exit. */ + if (0 != pipe(exec_errfd)) { + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; + } + + if (-1 == fcntl(exec_errfd[1], F_SETFD, FD_CLOEXEC)) { + saved_errno = errno; + result = subprocess_error_spawn; + goto cleanup; + } + + child = fork(); + + if (child < 0) { + saved_errno = errno; + result = subprocess_error_spawn; + goto cleanup; + } + + if (0 == child) { + /* Child. Everything below must stay async-signal-safe: after fork() in a + threaded process only such functions may be called before exec. */ + int child_errno; + + close(exec_errfd[0]); + + if ((-1 == dup2(stdinfd[0], STDIN_FILENO)) || + (-1 == dup2(stdoutfd[1], STDOUT_FILENO))) { + goto child_failed; + } + + if (subprocess_option_combined_stdout_stderr == + (options & subprocess_option_combined_stdout_stderr)) { + if (-1 == dup2(STDOUT_FILENO, STDERR_FILENO)) { + goto child_failed; + } + } else { + if (-1 == dup2(stderrfd[1], STDERR_FILENO)) { + goto child_failed; + } + } + + /* The originals are only closed once they have been duplicated, so that a + pipe end that already sits on 0, 1 or 2 is not closed out from under us. */ + if (stdinfd[0] > STDERR_FILENO) { + close(stdinfd[0]); + } + if (stdinfd[1] > STDERR_FILENO) { + close(stdinfd[1]); + } + if (stdoutfd[0] > STDERR_FILENO) { + close(stdoutfd[0]); + } + if (stdoutfd[1] > STDERR_FILENO) { + close(stdoutfd[1]); + } + if (stderrfd[0] > STDERR_FILENO) { + close(stderrfd[0]); + } + if (stderrfd[1] > STDERR_FILENO) { + close(stderrfd[1]); + } + + if (process_cwd && (0 != chdir(process_cwd))) { + goto child_failed; + } + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" +#pragma clang diagnostic ignored "-Wold-style-cast" +#endif + if (subprocess_option_search_user_path == + (options & subprocess_option_search_user_path)) { + execvpe(commandLine[0], + SUBPROCESS_CONST_CAST(char *const *, commandLine), + SUBPROCESS_CONST_CAST(char *const *, used_environment)); + } else { + execve(commandLine[0], + SUBPROCESS_CONST_CAST(char *const *, commandLine), + SUBPROCESS_CONST_CAST(char *const *, used_environment)); + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + + child_failed: + child_errno = errno; + /* Nothing useful can be done if this write fails; the parent then sees EOF + and reports success, exactly as posix_spawn would without exec reporting. */ + (void)!write(exec_errfd[1], &child_errno, sizeof(child_errno)); + /* 127 is what POSIX requires posix_spawn's child to exit with when exec + fails, so both implementations look the same to a caller. */ + _exit(127); + } + + /* Parent. */ + close(exec_errfd[1]); + exec_errfd[1] = -1; + + { + int child_errno = 0; + ssize_t bytes_read; + + do { + bytes_read = read(exec_errfd[0], &child_errno, sizeof(child_errno)); + } while ((-1 == bytes_read) && (EINTR == errno)); + + close(exec_errfd[0]); + exec_errfd[0] = -1; + + if (bytes_read == (ssize_t)sizeof(child_errno)) { + /* exec failed in the child. Reap it and surface the reason. */ + while ((-1 == waitpid(child, SUBPROCESS_NULL, 0)) && (EINTR == errno)) { + } + child = 0; + saved_errno = child_errno; + result = subprocess_error_from_errno(child_errno); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + } +#else posix_error = posix_spawn_file_actions_init(&actions); if (0 != posix_error) { saved_errno = posix_error; @@ -1205,8 +1596,10 @@ int subprocess_create_ex(const char *const commandLine[], int options, // Set working directory if (process_cwd) { -#if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000 +#if SUBPROCESS_ADDCHDIR_IS_POSIX posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd); +#elif !SUBPROCESS_HAVE_CWD + posix_error = ENOSYS; #else #if defined(__APPLE__) && defined(__clang__) #pragma clang diagnostic push @@ -1329,6 +1722,17 @@ int subprocess_create_ex(const char *const commandLine[], int options, goto cleanup; } } else { +#if !SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS + /* posix_spawn cannot tell us the exec failed, so check up front */ + if (0 != access(commandLine[0], X_OK)) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } +#endif posix_error = posix_spawn(&child, commandLine[0], &actions, SUBPROCESS_NULL, SUBPROCESS_CONST_CAST(char *const *, commandLine), @@ -1345,6 +1749,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, #ifdef __clang__ #pragma clang diagnostic pop #endif +#endif /* SUBPROCESS_SPAWN_VIA_FORK */ // Close the stdin read end close(stdinfd[0]); @@ -1419,9 +1824,21 @@ int subprocess_create_ex(const char *const commandLine[], int options, result = subprocess_error_from_errno(saved_errno); } +#if SUBPROCESS_SPAWN_VIA_FORK + if (-1 != exec_errfd[0]) { + close(exec_errfd[0]); + exec_errfd[0] = -1; + } + + if (-1 != exec_errfd[1]) { + close(exec_errfd[1]); + exec_errfd[1] = -1; + } +#else if (actions_created) { posix_spawn_file_actions_destroy(&actions); } +#endif if (0 != result) { if (child) { From 61f1dabcf0bf3efbb7fe173f6270eb8b933821a5 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Thu, 20 Aug 2026 11:32:33 +0200 Subject: [PATCH 3/4] Add a portability workflow beside the existing matrix cmake.yml covers x86-64 Linux, Windows and macOS. What no matrix over runs-on reaches is 32-bit, non-x86 architectures, a second libc, the BSDs, and older macOS deployment targets - and utest.h's format-macro and timing code differs along exactly those axes. Measured on a fork before opening this: 14 of 14 green, including NetBSD 9.4 with GCC 7.5.0 and the four emulated architectures running the full suite. No capability-paths job, unlike subprocess.h's version of this workflow: UTEST_USE_CLOCKGETTIME is derived from the glibc version by the header itself rather than being settable from outside, so a job forcing it would only be able to confirm the path already taken. --- .github/workflows/portability.yml | 110 ++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/portability.yml diff --git a/.github/workflows/portability.yml b/.github/workflows/portability.yml new file mode 100644 index 0000000..e2f1daf --- /dev/null +++ b/.github/workflows/portability.yml @@ -0,0 +1,110 @@ +name: Portability + +# Dimensions cmake.yml does not reach: 32-bit, non-x86, BSD, older macOS. + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + linux-32-bit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install multilib + run: sudo apt-get update && sudo apt-get install -y gcc-multilib g++-multilib + - name: Build and test + run: | + cmake -S test -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_FLAGS=-m32 \ + -DCMAKE_CXX_FLAGS=-m32 \ + -DCMAKE_EXE_LINKER_FLAGS=-m32 + cmake --build build -j + ctest --test-dir build --output-on-failure + + windows-x86: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - run: cmake -S test -B build -A Win32 + - run: cmake --build build --config RelWithDebInfo + - run: ctest --test-dir build -C RelWithDebInfo --output-on-failure + + musl: + runs-on: ubuntu-latest + container: alpine:3 + steps: + - name: Install toolchain + run: apk add --no-cache gcc g++ musl-dev cmake make linux-headers git + - uses: actions/checkout@v4 + - name: Build and test + run: | + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build -j + ctest --test-dir build --output-on-failure + + foreign-arch: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: [ppc64le, riscv64, s390x, armv7] + name: linux ${{ matrix.arch }} + steps: + - uses: actions/checkout@v4 + - uses: uraimo/run-on-arch-action@v3 + with: + arch: ${{ matrix.arch }} + distro: ubuntu_latest + install: | + apt-get update -q + apt-get install -q -y build-essential cmake + run: | + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build -j + ctest --test-dir build --output-on-failure + + # NetBSD twice on purpose: 9.x and 10.x differ in what exposes. + bsd: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { os: freebsd, version: "14.3", pkg: "sudo pkg install -y cmake" } + - { os: netbsd, version: "9.4", pkg: "sudo pkgin -y install cmake" } + - { os: netbsd, version: "10.1", pkg: "sudo pkgin -y install cmake" } + - { os: openbsd, version: "7.9", pkg: "sudo pkg_add cmake" } + name: ${{ matrix.os }} ${{ matrix.version }} + steps: + - uses: actions/checkout@v4 + - uses: cross-platform-actions/action@v1.4.0 + with: + operating_system: ${{ matrix.os }} + version: ${{ matrix.version }} + run: | + ${{ matrix.pkg }} + cmake -S test -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build + ctest --test-dir build --output-on-failure + + macos-deployment-target: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + target: ["10.14", "10.15", "11.0"] + name: macOS deployment target ${{ matrix.target }} + steps: + - uses: actions/checkout@v4 + - name: Build and test + run: | + cmake -S test -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ matrix.target }} + cmake --build build -j + ctest --test-dir build --output-on-failure From c8dac87962c505b22a5755e3abac8955f6bb6410 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Fri, 21 Aug 2026 16:11:23 +0200 Subject: [PATCH 4/4] Scope _GNU_SOURCE to main.c, the only file including subprocess.h --- test/CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 712caa7..989a4a0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,13 +30,6 @@ include(CTest) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../) -# Strict -std=c99/-std=c11/-std=c++XX modes on glibc hide POSIX/GNU -# declarations the vendored subprocess.h uses unless feature test macros are -# enabled. -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - add_compile_definitions(_GNU_SOURCE) -endif() - # Verify that including utest.h restores the caller's MSVC warning state. # The undefined-macro warning is enabled as an error before the include and # deliberately triggered afterwards. The probe must therefore fail to compile; @@ -154,6 +147,13 @@ endfunction() # C files — each compiled under a specific C standard. utest_set_c_flags(main.c "-std=gnu89") +# main.c is the only translation unit that includes the vendored subprocess.h, +# which calls pipe2 and posix_spawn_file_actions_addchdir_np. glibc declares +# those only under _GNU_SOURCE, and -std=gnu89 does not set it. Scoped to this +# file so every other unit still compiles without it. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set_source_files_properties(main.c PROPERTIES COMPILE_DEFINITIONS _GNU_SOURCE) +endif() utest_set_c_flags(test.c "-std=gnu89") utest_set_c_flags(order.c "-std=gnu89") utest_set_c_flags(test99.c "-std=c99")