From 44ef4f480da904a99141486891ebeeaf3464501f Mon Sep 17 00:00:00 2001 From: Neil Henning Date: Fri, 14 Aug 2026 13:50:31 +0100 Subject: [PATCH 1/5] Restrict Windows subprocess handle inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧙 Conjured by AI via [pi.dev](https://pi.dev/) using gpt-5.6-sol --- subprocess.h | 66 ++++++++++++++++++++++++++++++++++- test/CMakeLists.txt | 1 + test/process_is_handle_open.c | 33 ++++++++++++++++++ test/test.c | 29 +++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 test/process_is_handle_open.c diff --git a/subprocess.h b/subprocess.h index c6494fb..df88679 100644 --- a/subprocess.h +++ b/subprocess.h @@ -402,6 +402,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; @@ -451,6 +456,13 @@ __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(void *, unsigned long, unsigned long, + subprocess_size_t *); +__declspec(dllimport) int __stdcall +UpdateProcThreadAttribute(void *, subprocess_size_t, subprocess_size_t, void *, + subprocess_size_t, void *, subprocess_size_t *); +__declspec(dllimport) void __stdcall DeleteProcThreadAttributeList(void *); __declspec(dllimport) int __stdcall MultiByteToWideChar( unsigned int, unsigned long, const char *, int, subprocess_wchar_t *, int); __declspec(dllimport) int __stdcall CloseHandle(void *); @@ -735,6 +747,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, 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; @@ -742,6 +755,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}; @@ -749,6 +764,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_size_t attribute_list_size = 0; + subprocess_size_t inherited_handle_count = 0; + void *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, @@ -1123,6 +1143,43 @@ 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 = _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 @@ -1133,7 +1190,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) { @@ -1142,6 +1199,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; @@ -1171,6 +1231,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); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 758690c..fc606f0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -63,6 +63,7 @@ set(SUBPROCESS_HELPER_SOURCES process_call_return_argc.c process_cwd.c process_is_fd_open.c + process_is_handle_open.c ) foreach(SUBPROCESS_HELPER_SOURCE ${SUBPROCESS_HELPER_SOURCES}) diff --git a/test/process_is_handle_open.c b/test/process_is_handle_open.c new file mode 100644 index 0000000..7e87782 --- /dev/null +++ b/test/process_is_handle_open.c @@ -0,0 +1,33 @@ +#include + +#ifdef _MSC_VER +#pragma warning(disable : 4996) +#endif + +#if defined(_WIN32) +__declspec(dllimport) unsigned long __stdcall GetLastError(void); +__declspec(dllimport) int __stdcall GetHandleInformation(void *, + unsigned long *); +#endif + +int main(int argc, char *argv[]) { +#if defined(_WIN32) + const unsigned long error_invalid_handle = 6; + unsigned long flags = 0; + void *handle = 0; + + if ((2 != argc) || (1 != sscanf(argv[1], "%p", &handle))) { + return 2; + } + + if (GetHandleInformation(handle, &flags)) { + return 1; + } + + return error_invalid_handle == GetLastError() ? 0 : 2; +#else + (void)argc; + (void)argv; + return 0; +#endif +} diff --git a/test/test.c b/test/test.c index c86a163..e67cbf4 100644 --- a/test/test.c +++ b/test/test.c @@ -38,3 +38,32 @@ UTEST(c, create_does_not_inherit_another_subprocess_pipe) { "the second subprocess inherited the first subprocess pipe"); #endif } + +UTEST(c, create_does_not_inherit_unlisted_windows_handle) { +#if defined(_WIN32) + const char *command_line[] = {"./process_is_handle_open", 0, 0}; + struct subprocess_security_attributes_s security_attributes = { + sizeof(security_attributes), SUBPROCESS_NULL, 1}; + struct subprocess_s process; + char handle_argument[32]; + void *inheritable_handle; + int return_code = -1; + + inheritable_handle = + CreateEventA(&security_attributes, 1, 0, SUBPROCESS_NULL); + ASSERT_TRUE(inheritable_handle); + ASSERT_TRUE(0 < snprintf(handle_argument, sizeof(handle_argument), "%p", + inheritable_handle)); + command_line[1] = handle_argument; + + ASSERT_EQ(0, subprocess_create(command_line, 0, &process)); + ASSERT_EQ(0, subprocess_join(&process, &return_code)); + ASSERT_EQ(0, subprocess_destroy(&process)); + ASSERT_TRUE(CloseHandle(inheritable_handle)); + + EXPECT_EQ_MSG(0, return_code, + "subprocess inherited a handle outside its standard streams"); +#else + UTEST_SKIP("Windows handle-inheritance test"); +#endif +} From 3102520b42ebf40a7ed3d88356ab3fa82ed58269 Mon Sep 17 00:00:00 2001 From: Neil Henning Date: Fri, 14 Aug 2026 14:01:19 +0100 Subject: [PATCH 2/5] Fix Windows test security attribute cast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧙 Conjured by AI via [pi.dev](https://pi.dev/) using gpt-5.6-sol --- test/test.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test.c b/test/test.c index e67cbf4..6d04bda 100644 --- a/test/test.c +++ b/test/test.c @@ -49,8 +49,9 @@ UTEST(c, create_does_not_inherit_unlisted_windows_handle) { void *inheritable_handle; int return_code = -1; - inheritable_handle = - CreateEventA(&security_attributes, 1, 0, SUBPROCESS_NULL); + inheritable_handle = CreateEventA( + SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &security_attributes), 1, 0, + SUBPROCESS_NULL); ASSERT_TRUE(inheritable_handle); ASSERT_TRUE(0 < snprintf(handle_argument, sizeof(handle_argument), "%p", inheritable_handle)); From 112aab9c2f44fe5e67bb9bbcd3f481b1ddb30ac2 Mon Sep 17 00:00:00 2001 From: Neil Henning Date: Fri, 14 Aug 2026 14:04:30 +0100 Subject: [PATCH 3/5] Match Windows attribute list API types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧙 Conjured by AI via [pi.dev](https://pi.dev/) using gpt-5.6-sol --- subprocess.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/subprocess.h b/subprocess.h index df88679..b6f9534 100644 --- a/subprocess.h +++ b/subprocess.h @@ -351,6 +351,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 @@ -457,12 +458,13 @@ __declspec(dllimport) int __stdcall CreateProcessW( LPSECURITY_ATTRIBUTES, int, unsigned long, void *, const subprocess_wchar_t *, LPSTARTUPINFOW, LPPROCESS_INFORMATION); __declspec(dllimport) int __stdcall -InitializeProcThreadAttributeList(void *, unsigned long, unsigned long, - subprocess_size_t *); -__declspec(dllimport) int __stdcall -UpdateProcThreadAttribute(void *, subprocess_size_t, subprocess_size_t, void *, - subprocess_size_t, void *, subprocess_size_t *); -__declspec(dllimport) void __stdcall DeleteProcThreadAttributeList(void *); +InitializeProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, + unsigned long, subprocess_size_t *); +__declspec(dllimport) int __stdcall UpdateProcThreadAttribute( + LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, subprocess_size_t, void *, + subprocess_size_t, void *, subprocess_size_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 *); @@ -766,7 +768,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, subprocess_wchar_t *used_environment = SUBPROCESS_NULL; subprocess_size_t attribute_list_size = 0; subprocess_size_t inherited_handle_count = 0; - void *attribute_list = SUBPROCESS_NULL; + 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, @@ -1159,7 +1161,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, goto cleanup; } - attribute_list = _alloca(attribute_list_size); + 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; From 5e8b8f6a2ecc650e32ad58315f9aa71312fec8f3 Mon Sep 17 00:00:00 2001 From: Neil Henning Date: Fri, 14 Aug 2026 14:29:27 +0100 Subject: [PATCH 4/5] Verify inherited event identity in Windows test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧙 Conjured by AI via [pi.dev](https://pi.dev/) using gpt-5.6-sol --- test/CMakeLists.txt | 2 +- test/process_is_handle_open.c | 33 --------------------------------- test/process_signal_handle.c | 26 ++++++++++++++++++++++++++ test/test.c | 8 ++++++-- 4 files changed, 33 insertions(+), 36 deletions(-) delete mode 100644 test/process_is_handle_open.c create mode 100644 test/process_signal_handle.c diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fc606f0..863273e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -63,7 +63,7 @@ set(SUBPROCESS_HELPER_SOURCES process_call_return_argc.c process_cwd.c process_is_fd_open.c - process_is_handle_open.c + process_signal_handle.c ) foreach(SUBPROCESS_HELPER_SOURCE ${SUBPROCESS_HELPER_SOURCES}) diff --git a/test/process_is_handle_open.c b/test/process_is_handle_open.c deleted file mode 100644 index 7e87782..0000000 --- a/test/process_is_handle_open.c +++ /dev/null @@ -1,33 +0,0 @@ -#include - -#ifdef _MSC_VER -#pragma warning(disable : 4996) -#endif - -#if defined(_WIN32) -__declspec(dllimport) unsigned long __stdcall GetLastError(void); -__declspec(dllimport) int __stdcall GetHandleInformation(void *, - unsigned long *); -#endif - -int main(int argc, char *argv[]) { -#if defined(_WIN32) - const unsigned long error_invalid_handle = 6; - unsigned long flags = 0; - void *handle = 0; - - if ((2 != argc) || (1 != sscanf(argv[1], "%p", &handle))) { - return 2; - } - - if (GetHandleInformation(handle, &flags)) { - return 1; - } - - return error_invalid_handle == GetLastError() ? 0 : 2; -#else - (void)argc; - (void)argv; - return 0; -#endif -} diff --git a/test/process_signal_handle.c b/test/process_signal_handle.c new file mode 100644 index 0000000..3923f83 --- /dev/null +++ b/test/process_signal_handle.c @@ -0,0 +1,26 @@ +#include + +#ifdef _MSC_VER +#pragma warning(disable : 4996) +#endif + +#if defined(_WIN32) +__declspec(dllimport) int __stdcall SetEvent(void *); +#endif + +int main(int argc, char *argv[]) { +#if defined(_WIN32) + void *handle = 0; + + if ((2 != argc) || (1 != sscanf(argv[1], "%p", &handle))) { + return 2; + } + + SetEvent(handle); + return 0; +#else + (void)argc; + (void)argv; + return 0; +#endif +} diff --git a/test/test.c b/test/test.c index 6d04bda..c9159be 100644 --- a/test/test.c +++ b/test/test.c @@ -41,12 +41,14 @@ UTEST(c, create_does_not_inherit_another_subprocess_pipe) { UTEST(c, create_does_not_inherit_unlisted_windows_handle) { #if defined(_WIN32) - const char *command_line[] = {"./process_is_handle_open", 0, 0}; + const unsigned long wait_timeout = 0x00000102; + const char *command_line[] = {"./process_signal_handle", 0, 0}; struct subprocess_security_attributes_s security_attributes = { sizeof(security_attributes), SUBPROCESS_NULL, 1}; struct subprocess_s process; char handle_argument[32]; void *inheritable_handle; + unsigned long wait_result; int return_code = -1; inheritable_handle = CreateEventA( @@ -60,9 +62,11 @@ UTEST(c, create_does_not_inherit_unlisted_windows_handle) { ASSERT_EQ(0, subprocess_create(command_line, 0, &process)); ASSERT_EQ(0, subprocess_join(&process, &return_code)); ASSERT_EQ(0, subprocess_destroy(&process)); + wait_result = WaitForSingleObject(inheritable_handle, 0); ASSERT_TRUE(CloseHandle(inheritable_handle)); - EXPECT_EQ_MSG(0, return_code, + EXPECT_EQ(0, return_code); + EXPECT_EQ_MSG(wait_timeout, wait_result, "subprocess inherited a handle outside its standard streams"); #else UTEST_SKIP("Windows handle-inheritance test"); From 3f8d2d8954717cba2d0ed43890d96a07d249be27 Mon Sep 17 00:00:00 2001 From: Neil Henning Date: Fri, 14 Aug 2026 15:00:34 +0100 Subject: [PATCH 5/5] Only define Windows handle test on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧙 Conjured by AI via [pi.dev](https://pi.dev/) using gpt-5.6-sol --- test/test.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test.c b/test/test.c index c9159be..82910d3 100644 --- a/test/test.c +++ b/test/test.c @@ -39,8 +39,8 @@ UTEST(c, create_does_not_inherit_another_subprocess_pipe) { #endif } -UTEST(c, create_does_not_inherit_unlisted_windows_handle) { #if defined(_WIN32) +UTEST(c, create_does_not_inherit_unlisted_windows_handle) { const unsigned long wait_timeout = 0x00000102; const char *command_line[] = {"./process_signal_handle", 0, 0}; struct subprocess_security_attributes_s security_attributes = { @@ -68,7 +68,5 @@ UTEST(c, create_does_not_inherit_unlisted_windows_handle) { EXPECT_EQ(0, return_code); EXPECT_EQ_MSG(wait_timeout, wait_result, "subprocess inherited a handle outside its standard streams"); -#else - UTEST_SKIP("Windows handle-inheritance test"); -#endif } +#endif