From 0d4acd733b89af6577918a8b0424e3878c8a5530 Mon Sep 17 00:00:00 2001 From: Sandbox User Date: Tue, 24 Mar 2026 14:55:01 +0000 Subject: [PATCH 1/2] win: use PROC_THREAD_ATTRIBUTE_HANDLE_LIST in uv_spawn Use STARTUPINFOEXW with PROC_THREAD_ATTRIBUTE_HANDLE_LIST so that only the stdio handles prepared for each child process are inherited by it. This closes the race condition where two concurrent uv_spawn calls could cause handles intended for one child to leak into another child process. Fixes: https://github.com/libuv/libuv/issues/1490 Co-Authored-By: Claude Sonnet 4.6 --- src/win/process.c | 91 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/src/win/process.c b/src/win/process.c index c4471cd45..b1f44af23 100644 --- a/src/win/process.c +++ b/src/win/process.c @@ -902,14 +902,20 @@ int uv_spawn(uv_loop_t* loop, BOOL result; WCHAR* application_path = NULL, *application = NULL, *arguments = NULL, *env = NULL, *cwd = NULL; - STARTUPINFOW startup; + STARTUPINFOEXW startup; PROCESS_INFORMATION info; DWORD process_flags, cwd_len; BYTE* child_stdio_buffer; + HANDLE* handle_list; + LPPROC_THREAD_ATTRIBUTE_LIST attr_list; + int attr_list_initialized; uv__process_init(loop, process); process->exit_cb = options->exit_cb; child_stdio_buffer = NULL; + handle_list = NULL; + attr_list = NULL; + attr_list_initialized = 0; if (options->flags & (UV_PROCESS_SETGID | UV_PROCESS_SETUID)) { return UV_ENOTSUP; @@ -1022,20 +1028,67 @@ int uv_spawn(uv_loop_t* loop, goto done; } - startup.cb = sizeof(startup); - startup.lpReserved = NULL; - startup.lpDesktop = NULL; - startup.lpTitle = NULL; - startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + memset(&startup, 0, sizeof startup); + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.lpReserved = NULL; + startup.StartupInfo.lpDesktop = NULL; + startup.StartupInfo.lpTitle = NULL; + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - startup.cbReserved2 = uv__stdio_size(child_stdio_buffer); - startup.lpReserved2 = (BYTE*) child_stdio_buffer; + startup.StartupInfo.cbReserved2 = uv__stdio_size(child_stdio_buffer); + startup.StartupInfo.lpReserved2 = (BYTE*) child_stdio_buffer; - startup.hStdInput = uv__stdio_handle(child_stdio_buffer, 0); - startup.hStdOutput = uv__stdio_handle(child_stdio_buffer, 1); - startup.hStdError = uv__stdio_handle(child_stdio_buffer, 2); + startup.StartupInfo.hStdInput = uv__stdio_handle(child_stdio_buffer, 0); + startup.StartupInfo.hStdOutput = uv__stdio_handle(child_stdio_buffer, 1); + startup.StartupInfo.hStdError = uv__stdio_handle(child_stdio_buffer, 2); - process_flags = CREATE_UNICODE_ENVIRONMENT; + /* Build the list of handles to inherit. Using + * PROC_THREAD_ATTRIBUTE_HANDLE_LIST ensures only these specific handles are + * inherited, closing the race condition where concurrent uv_spawn calls + * could cause handles intended for one child to leak into another. */ + { + int count = uv__stdio_size(child_stdio_buffer); + int n = 0; + SIZE_T attr_size = 0; + + handle_list = (HANDLE*) uv__malloc(count * sizeof(HANDLE)); + if (handle_list == NULL) { + err = ERROR_OUTOFMEMORY; + goto done; + } + + for (i = 0; i < count; i++) { + HANDLE h = uv__stdio_handle(child_stdio_buffer, i); + if (h != INVALID_HANDLE_VALUE) + handle_list[n++] = h; + } + + InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); + attr_list = (LPPROC_THREAD_ATTRIBUTE_LIST) uv__malloc(attr_size); + if (attr_list == NULL) { + err = ERROR_OUTOFMEMORY; + goto done; + } + if (!InitializeProcThreadAttributeList(attr_list, 1, 0, &attr_size)) { + err = GetLastError(); + goto done; + } + attr_list_initialized = 1; + if (!UpdateProcThreadAttribute(attr_list, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handle_list, + n * sizeof(HANDLE), + NULL, + NULL)) { + err = GetLastError(); + goto done; + } + } + + startup.lpAttributeList = attr_list; + + process_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT; if ((options->flags & UV_PROCESS_WINDOWS_HIDE_CONSOLE) || (options->flags & UV_PROCESS_WINDOWS_HIDE)) { @@ -1050,9 +1103,9 @@ int uv_spawn(uv_loop_t* loop, if ((options->flags & UV_PROCESS_WINDOWS_HIDE_GUI) || (options->flags & UV_PROCESS_WINDOWS_HIDE)) { /* Use SW_HIDE to avoid any potential process window. */ - startup.wShowWindow = SW_HIDE; + startup.StartupInfo.wShowWindow = SW_HIDE; } else { - startup.wShowWindow = SW_SHOWDEFAULT; + startup.StartupInfo.wShowWindow = SW_SHOWDEFAULT; } if (options->flags & UV_PROCESS_DETACHED) { @@ -1078,7 +1131,7 @@ int uv_spawn(uv_loop_t* loop, process_flags, env, cwd, - &startup, + &startup.StartupInfo, &info)) { /* CreateProcessW failed. */ err = GetLastError(); @@ -1161,6 +1214,14 @@ int uv_spawn(uv_loop_t* loop, uv__free(env); uv__free(alloc_path); + if (attr_list != NULL) { + if (attr_list_initialized) + DeleteProcThreadAttributeList(attr_list); + uv__free(attr_list); + attr_list = NULL; + } + uv__free(handle_list); + if (child_stdio_buffer != NULL) { /* Clean up child stdio handles. */ uv__stdio_destroy(child_stdio_buffer); From afb8fd2bddfcf8b4d6e9902b9d3a2c30fa730b7a Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Thu, 26 Mar 2026 16:45:54 -0400 Subject: [PATCH 2/2] fixup! win: use PROC_THREAD_ATTRIBUTE_HANDLE_LIST in uv_spawn --- src/win/process.c | 5 ++++- test/run-tests.c | 19 ++++++------------- test/test-spawn.c | 9 --------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/win/process.c b/src/win/process.c index b1f44af23..7dca09a86 100644 --- a/src/win/process.c +++ b/src/win/process.c @@ -1047,7 +1047,10 @@ int uv_spawn(uv_loop_t* loop, * inherited, closing the race condition where concurrent uv_spawn calls * could cause handles intended for one child to leak into another. */ { - int count = uv__stdio_size(child_stdio_buffer); +#define CHILD_STDIO_COUNT(buffer) \ + *((unsigned int*) (buffer)) + int count = CHILD_STDIO_COUNT(child_stdio_buffer); +#undef CHILD_STDIO_COUNT int n = 0; SIZE_T attr_size = 0; diff --git a/test/run-tests.c b/test/run-tests.c index 9c2574ebf..4b0c961ea 100644 --- a/test/run-tests.c +++ b/test/run-tests.c @@ -211,25 +211,18 @@ static int maybe_run_test(int argc, char **argv) { if (strcmp(argv[1], "spawn_helper8") == 0) { uv_os_fd_t closed_fd; uv_os_fd_t open_fd; -#ifdef _WIN32 - DWORD flags; - HMODULE kernelbase_module; - union { - FARPROC proc; - sCompareObjectHandles pCompareObjectHandles; /* Windows >= 10 */ - } u; -#endif notify_parent_process(); ASSERT_EQ(sizeof(closed_fd), read(0, &closed_fd, sizeof(closed_fd))); ASSERT_EQ(sizeof(open_fd), read(0, &open_fd, sizeof(open_fd))); #ifdef _WIN32 + DWORD flags; ASSERT_GT((intptr_t) closed_fd, 0); ASSERT_GT((intptr_t) open_fd, 0); - ASSERT_NE(0, GetHandleInformation(open_fd, &flags)); - kernelbase_module = GetModuleHandleW(L"kernelbase.dll"); - u.proc = GetProcAddress(kernelbase_module, "CompareObjectHandles"); - if (u.pCompareObjectHandles != NULL) - ASSERT_EQ(FALSE, u.pCompareObjectHandles(open_fd, closed_fd)); + ASSERT_EQ(0, GetHandleInformation(open_fd, &flags)); + ASSERT_EQ(ERROR_INVALID_HANDLE, GetLastError()); + SetLastError(ERROR_SUCCESS); + ASSERT_EQ(0, GetHandleInformation(closed_fd, &flags)); + ASSERT_EQ(ERROR_INVALID_HANDLE, GetLastError()); #else ASSERT_GT(open_fd, 2); ASSERT_GT(closed_fd, 2); diff --git a/test/test-spawn.c b/test/test-spawn.c index eaaa8bbea..9a69b7eca 100644 --- a/test/test-spawn.c +++ b/test/test-spawn.c @@ -1677,11 +1677,6 @@ TEST_IMPL(spawn_fs_open) { uv_stdio_container_t stdio[1]; #ifdef _WIN32 const char dev_null[] = "NUL"; - HMODULE kernelbase_module; - union { - FARPROC proc; - sCompareObjectHandles pCompareObjectHandles; /* Windows >= 10 */ - } u; #else const char dev_null[] = "/dev/null"; #endif @@ -1704,10 +1699,6 @@ TEST_IMPL(spawn_fs_open) { #ifdef _WIN32 ASSERT_NE(0, DuplicateHandle(GetCurrentProcess(), fd, GetCurrentProcess(), &dup_fd, 0, /* inherit */ TRUE, DUPLICATE_SAME_ACCESS)); - kernelbase_module = GetModuleHandleW(L"kernelbase.dll"); - u.proc = GetProcAddress(kernelbase_module, "CompareObjectHandles"); - if (u.pCompareObjectHandles != NULL) - ASSERT_EQ(TRUE, u.pCompareObjectHandles(fd, dup_fd)); #else dup_fd = dup(fd); #endif