process: better validation for process->pid usage (#3539)

Previously, the user might unknowingly close a uv_process_t before
doing waitpid on the zombie, leaving it forever undead. Track the state
of the child, so that the application wrapper can avoid this by calling
uv_process_kill and checking for UV_ESRCH error.
This commit is contained in:
Jameson Nash 2026-03-19 15:28:02 -04:00 committed by GitHub
parent 1899789be8
commit 58418d5310
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 42 additions and 10 deletions

View File

@ -172,7 +172,10 @@ Public members
.. c:member:: int uv_process_t.pid .. c:member:: int uv_process_t.pid
The PID of the spawned process. It's set after calling :c:func:`uv_spawn`. The PID of the spawned process. It is set after calling :c:func:`uv_spawn`
and retains the value even after the process exits. The value is only
unique while the process is alive; after exit, another process may be
reassigned the same PID.
.. note:: .. note::
The :c:type:`uv_handle_t` members also apply. The :c:type:`uv_handle_t` members also apply.
@ -259,14 +262,20 @@ API
Initializes the process handle and starts the process. If the process is Initializes the process handle and starts the process. If the process is
successfully spawned, this function will return 0. Otherwise, the successfully spawned, this function will return 0. Otherwise, the
negative error code corresponding to the reason it couldn't spawn is negative error code corresponding to the reason it couldn't spawn is
returned. Note that either way you must eventually call :c:func:`uv_close` returned. Note that either way-success or failure--you must eventually call
to close the handle again. :c:func:`uv_close` to close the handle again before freeing the memory of
the handle, unlike other the other init functions in libuv.
Possible reasons for failing to spawn would include (but not be limited to) Possible reasons for failing to spawn would include (but not be limited to)
the file to execute not existing, not having permissions to use the setuid or the file to execute not existing, not having permissions to use the setuid or
setgid specified, or not having enough memory to allocate for the new setgid specified, or not having enough memory to allocate for the new
process. process.
.. warning::
On unix, if the process has not yet exited when you call `uv_close`,
you will create a zombie that libuv cannot reap. You are responsible
for calling `waitpid` later. This is not relevant on Windows.
.. versionchanged:: 1.24.0 Added `UV_PROCESS_WINDOWS_HIDE_CONSOLE` and .. versionchanged:: 1.24.0 Added `UV_PROCESS_WINDOWS_HIDE_CONSOLE` and
`UV_PROCESS_WINDOWS_HIDE_GUI` flags. `UV_PROCESS_WINDOWS_HIDE_GUI` flags.
@ -278,6 +287,11 @@ API
Sends the specified signal to the given process handle. Check the documentation Sends the specified signal to the given process handle. Check the documentation
on :c:ref:`signal` for signal support, specially on Windows. on :c:ref:`signal` for signal support, specially on Windows.
If the specified process is already dead, this will not kill a different
process which happened to reuse the same pid. By contrast, `uv_kill` may
kill an arbitrary other process if you use a cached value of
:c:func:`uv_process_get_pid`.
.. c:function:: int uv_kill(int pid, int signum) .. c:function:: int uv_kill(int pid, int signum)
Sends the specified signal to the given PID. Check the documentation Sends the specified signal to the given PID. Check the documentation

View File

@ -145,6 +145,7 @@ void uv__wait_children(uv_loop_t* loop) {
} }
assert(pid == process->pid); assert(pid == process->pid);
process->flags |= UV_HANDLE_ESRCH; /* pid is no longer valid (or unique) */
process->status = status; process->status = status;
uv__queue_remove(&process->queue); uv__queue_remove(&process->queue);
uv__queue_insert_tail(&pending, &process->queue); uv__queue_insert_tail(&pending, &process->queue);
@ -968,6 +969,10 @@ int uv_spawn(uv_loop_t* loop,
const uv_process_options_t* options) { const uv_process_options_t* options) {
#if defined(__APPLE__) && (TARGET_OS_TV || TARGET_OS_WATCH) #if defined(__APPLE__) && (TARGET_OS_TV || TARGET_OS_WATCH)
/* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */ /* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS);
QUEUE_INIT(&process->queue);
process->status = 0;
process->pid = 0;
return UV_ENOSYS; return UV_ENOSYS;
#else #else
int pipes_storage[8][2]; int pipes_storage[8][2];
@ -991,6 +996,7 @@ int uv_spawn(uv_loop_t* loop,
uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS); uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS);
uv__queue_init(&process->queue); uv__queue_init(&process->queue);
process->status = 0; process->status = 0;
process->pid = 0;
stdio_count = options->stdio_count; stdio_count = options->stdio_count;
if (stdio_count < 3) if (stdio_count < 3)
@ -1095,6 +1101,8 @@ error:
int uv_process_kill(uv_process_t* process, int signum) { int uv_process_kill(uv_process_t* process, int signum) {
if (process->flags & UV_HANDLE_ESRCH)
return UV_ESRCH;
return uv_kill(process->pid, signum); return uv_kill(process->pid, signum);
} }
@ -1115,6 +1123,9 @@ int uv_kill(int pid, int signum) {
void uv__process_close(uv_process_t* handle) { void uv__process_close(uv_process_t* handle) {
/* Warning: if UV_HANDLE_ESRCH is not set, the caller is creating a zombie
* that we cannot reap. We assume here that it is intentional, and that the
* user will be wise and cleanup later. */
uv__queue_remove(&handle->queue); uv__queue_remove(&handle->queue);
uv__handle_stop(handle); uv__handle_stop(handle);
#ifdef UV_USE_SIGCHLD #ifdef UV_USE_SIGCHLD

View File

@ -137,6 +137,7 @@ enum {
UV_HANDLE_POLL_SLOW = 0x01000000, UV_HANDLE_POLL_SLOW = 0x01000000,
/* Only used by uv_process_t handles. */ /* Only used by uv_process_t handles. */
UV_HANDLE_ESRCH = 0x01000000,
UV_HANDLE_REAP = 0x10000000 UV_HANDLE_REAP = 0x10000000
}; };

View File

@ -836,10 +836,6 @@ void uv__process_proc_exit(uv_loop_t* loop, uv_process_t* handle) {
handle->wait_handle = INVALID_HANDLE_VALUE; handle->wait_handle = INVALID_HANDLE_VALUE;
} }
/* Set the handle to inactive: no callbacks will be made after the exit
* callback. */
uv__handle_stop(handle);
if (GetExitCodeProcess(handle->process_handle, &status)) { if (GetExitCodeProcess(handle->process_handle, &status)) {
exit_code = status; exit_code = status;
} else { } else {
@ -847,6 +843,15 @@ void uv__process_proc_exit(uv_loop_t* loop, uv_process_t* handle) {
exit_code = uv_translate_sys_error(GetLastError()); exit_code = uv_translate_sys_error(GetLastError());
} }
/* Clean-up the process handle eagerly. */
CloseHandle(handle->process_handle);
handle->process_handle = INVALID_HANDLE_VALUE;
handle->flags |= UV_HANDLE_ESRCH;
/* Set the handle to inactive: no callbacks will be made after the exit
* callback. */
uv__handle_stop(handle);
/* Fire the exit callback. */ /* Fire the exit callback. */
if (handle->exit_cb) { if (handle->exit_cb) {
handle->exit_cb(handle, exit_code, handle->exit_signal); handle->exit_cb(handle, exit_code, handle->exit_signal);
@ -881,6 +886,7 @@ void uv__process_endgame(uv_loop_t* loop, uv_process_t* handle) {
assert(!(handle->flags & UV_HANDLE_CLOSED)); assert(!(handle->flags & UV_HANDLE_CLOSED));
/* Clean-up the process handle. */ /* Clean-up the process handle. */
if (handle->process_handle != INVALID_HANDLE_VALUE)
CloseHandle(handle->process_handle); CloseHandle(handle->process_handle);
uv__handle_close(handle); uv__handle_close(handle);
@ -1364,8 +1370,8 @@ static int uv__kill(HANDLE process_handle, int signum) {
int uv_process_kill(uv_process_t* process, int signum) { int uv_process_kill(uv_process_t* process, int signum) {
int err; int err;
if (process->process_handle == INVALID_HANDLE_VALUE) { if (process->flags & UV_HANDLE_ESRCH) {
return UV_EINVAL; return UV_ESRCH;
} }
err = uv__kill(process->process_handle, signum); err = uv__kill(process->process_handle, signum);