From 9f48c87dd4c3091d2e9b3eb8c4655be5a173898d Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Mon, 14 Sep 2026 14:55:24 +0200 Subject: [PATCH] Encapsulate race-safe process slot synchronization and termination in OS layer for Unix and Windows --- cmdline/main.c | 29 ++- cmdline/mingwapp.c | 4 +- os/mingw.c | 442 +++++++++++++++++++++++++++++++++++---------- os/os.h | 83 +++++++-- os/unix.c | 370 ++++++++++++++++++++++++++++++++----- 5 files changed, 756 insertions(+), 172 deletions(-) diff --git a/cmdline/main.c b/cmdline/main.c index 2227bdd2..c9c53950 100644 --- a/cmdline/main.c +++ b/cmdline/main.c @@ -461,20 +461,41 @@ int main(int argc, char* argv[]) /* unblock signals in parent */ sigprocmask(SIG_SETMASK, &oldset, NULL); + /* + * First wait for child termination without reaping (WNOWAIT). + * The child enters zombie state, holding its PID reserved in the kernel + * so no newly created process can receive the same PID. We clear + * child_pid before the final waitpid() reap so the signal forwarder + * never sends signals to a recycled PID. + */ + siginfo_t info; + int wret; do { - ret = waitpid(pid, &status, 0); - } while (ret == -1 && errno == EINTR); /* retry if interrupted by signal */ + memset(&info, 0, sizeof(info)); + wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT); + } while (wret == -1 && errno == EINTR); - if (ret == -1) { + if (wret == -1) { /* LCOV_EXCL_START */ perror("Failed to wait for SnapRAID"); exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } - /* clear child PID */ + /* clear child PID while PID is still reserved in zombie state */ child_pid = 0; + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); /* retry if interrupted by signal */ + + if (ret == -1) { + /* LCOV_EXCL_START */ + perror("Failed to reap SnapRAID"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + /* restore default signal handlers */ signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); diff --git a/cmdline/mingwapp.c b/cmdline/mingwapp.c index 4bea6410..0d172387 100644 --- a/cmdline/mingwapp.c +++ b/cmdline/mingwapp.c @@ -309,7 +309,7 @@ static int windows_ps(const char* ps_command, char* out, size_t out_size) 0 }; - pid = os_spawn(argv, &stdout_fd, 0, 0); + pid = os_spawn(argv, &stdout_fd, 0, 0, 0); if (pid < 0) { log_error(errno, "Failed to run PowerShell command '%s' (from os_spawn).\n", ps_command); return -1; @@ -337,7 +337,7 @@ static int windows_ps(const char* ps_command, char* out, size_t out_size) close(stdout_fd); status = 0; - pid_t pid_ret = os_wait(pid, &status); + pid_t pid_ret = os_wait(pid, &status, 0); os_dispose(pid); if (pid_ret < 0) { log_error(errno, "Failed to wait for PowerShell command '%s' (from os_wait).\n", ps_command); diff --git a/os/mingw.c b/os/mingw.c index 18e53789..dbfe720d 100644 --- a/os/mingw.c +++ b/os/mingw.c @@ -65,6 +65,207 @@ static windows_key_t last_error; static windows_mutex_t tick_lock; static uint64_t tick_last; +/** + * Mutex for process reference publication, termination, and unpublication. + */ +static windows_mutex_t exec_mutex; + +#define PID_NONE ((pid_t)0) /**< No process is active. */ +#define PID_SPAWN ((pid_t)-1) /**< Process creation is in progress. */ +#define PID_TERM ((pid_t)-2) /**< Process termination was requested while creation is in progress. */ +#define PID_KILL ((pid_t)-3) /**< Process kill was requested; rejects future spawns or terminates in-progress/active processes. */ + +/** + * Acquire the process execution mutex. + */ +static void exec_lock(void) +{ + windows_mutex_lock(&exec_mutex); +} + +/** + * Release the process execution mutex. + */ +static void exec_unlock(void) +{ + windows_mutex_unlock(&exec_mutex); +} + +/** + * Mark the start of process creation in a process-reference slot. + */ +static int pid_spawn_begin(pid_t* pid_slot) +{ + if (pid_slot == 0) + return 0; + + exec_lock(); + if (*pid_slot == PID_KILL) { + exec_unlock(); + errno = ECANCELED; + return -1; + } + *pid_slot = PID_SPAWN; + exec_unlock(); + return 0; +} + +/** + * Forcibly terminate the specified raw process reference. + */ +static int pid_kill(pid_t pid) +{ + if (pid <= 0) { + errno = EINVAL; + return -1; + } + + HANDLE h = (void*)pid; + + if (!TerminateProcess(h, 1)) { + windows_errno(GetLastError()); + return -1; + } + + return 0; +} + +/** + * Gracefully terminate a process created by os_spawn() on Windows. + * + * os_spawn() creates console applications with both CREATE_NO_WINDOW and + * CREATE_NEW_PROCESS_GROUP. + * + * CREATE_NO_WINDOW does not mean that the child is created as a detached + * process with no console support. For a console application it provides a + * windowless console, so the process can still receive console control events. + * + * Note that the Microsoft documentation is somewhat ambiguous here: it says + * that with CREATE_NO_WINDOW "the console handle for the application is not + * set", which can be misread as meaning that the process has no console at all. + * In practice, CREATE_NO_WINDOW creates a console session without a visible + * console window, and is distinct from DETACHED_PROCESS, which creates a + * process with no attached console. + * + * This behavior is documented and experimentally verified here: + * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags + * https://github.com/rprichard/win32-console-docs + * + * CREATE_NEW_PROCESS_GROUP creates a new process group whose identifier is + * the process ID of the child created by CreateProcess(). This allows + * CTRL_BREAK_EVENT to be directed specifically to the child's process group: + * + * CREATE_NO_WINDOW + * -> child has a windowless console + * + * CREATE_NEW_PROCESS_GROUP + * -> child PID is also the process-group ID + * + * GenerateConsoleCtrlEvent() can signal only processes sharing the console + * of the caller. Therefore, this function first detaches the calling process + * from its current console, if any, temporarily attaches it to the child's + * console using AttachConsole(child_pid), and then sends: + * + * GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid) + * + * The child process group then receives CTRL_BREAK_EVENT and can perform an + * orderly shutdown instead of being forcibly terminated. + * + * This works particularly well when called by a Windows Service. Services + * normally have no console of their own, so the sequence is simply: + * + * Service without a console + * -> AttachConsole(child_pid) + * -> GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid) + * -> child receives CTRL_BREAK_EVENT + * -> FreeConsole() + * -> Service is console-less again + * + * Note that console attachment is process-wide. If the calling process was + * already attached to a console, the initial FreeConsole() detaches it and + * that previous console is not restored on return. FreeConsole() and + * AttachConsole() also reset the process console control-handler table. + * For this reason, this function is best suited to callers that normally + * have no console, such as Windows Services. + * + * If attaching to the child's console fails, fall back to TerminateProcess(). + */ +static int pid_term(pid_t pid) +{ + HANDLE h = (void*)pid; + DWORD id = GetProcessId(h); + + if (id == 0) { + errno = EINVAL; + return -1; + } + + /* detach from current console (if any; background service has no console) */ + FreeConsole(); + + /* attach to the child's invisible console host */ + if (!AttachConsole(id)) { + /* fallback: terminate process forcibly if console attachment fails */ + if (!TerminateProcess(h, 1)) { + windows_errno(GetLastError()); + return -1; + } + return 0; + } + + /* disable Ctrl-C for the PARENT so we don't kill ourselves */ + SetConsoleCtrlHandler(0, TRUE); + + /* this will now reach the child's SetConsoleCtrlHandler */ + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id); + + /* detach from child console, returning calling service process to its console-less state */ + FreeConsole(); + + return 0; +} + +/** + * Publish a newly created process reference, or terminate it immediately if canceled during spawn. + */ +static void pid_publish(pid_t* pid_slot, pid_t pid) +{ + if (pid_slot == 0) + return; + + exec_lock(); + if (pid <= 0) { + if (*pid_slot != PID_KILL) + *pid_slot = PID_NONE; + } else { + pid_t prev = *pid_slot; + if (prev == PID_KILL) { + *pid_slot = PID_KILL; + pid_kill(pid); + } else { + *pid_slot = pid; + + if (prev == PID_TERM) + pid_term(pid); + } + } + exec_unlock(); +} + +/** + * Unpublish a process reference from a slot. + */ +static void pid_unpublish(pid_t* pid_slot, pid_t pid) +{ + if (pid_slot == 0) + return; + + exec_lock(); + if (*pid_slot == pid || (*pid_slot < 0 && *pid_slot != PID_KILL)) + *pid_slot = PID_NONE; + exec_unlock(); +} + /** * If we are running in Wine. */ @@ -2846,7 +3047,7 @@ static int process_startup_init(struct process_startup* startup, HANDLE stdin_ha return 0; } -pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const char* run_as_user) +pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const char* run_as_user, pid_t* pid_slot) { wchar_t conv[CONV_MAX]; HANDLE stdout_write_handle = INVALID_HANDLE_VALUE; @@ -2996,6 +3197,20 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch */ const wchar_t* cwd = L"C:\\"; + if (pid_spawn_begin(pid_slot) != 0) { + CloseHandle(nul_handle); + if (has_out) { + CloseHandle(stdout_write_handle); + close(out_f); + } + if (has_err) { + CloseHandle(stderr_write_handle); + close(err_f); + } + process_startup_done(&startup); + return -1; + } + /* create the child process */ if (run_as_user == 0 || run_as_user[0] == 0) { ret = CreateProcessW( @@ -3016,6 +3231,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch /* Validate that the requested user is actually a supported Service Account before attempting logon */ if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + pid_unpublish(pid_slot, 0); process_startup_done(&startup); CloseHandle(nul_handle); if (has_out) { @@ -3032,6 +3248,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); process_startup_done(&startup); CloseHandle(nul_handle); if (has_out) { @@ -3050,6 +3267,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); CloseHandle(h_token); process_startup_done(&startup); CloseHandle(nul_handle); @@ -3085,6 +3303,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch if (!ret) { windows_errno(create_error); os_syslog(OS_LVL_INFO, "failed to create process '%s' for spawn, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno); + pid_unpublish(pid_slot, 0); CloseHandle(nul_handle); if (has_out) { CloseHandle(stdout_write_handle); @@ -3108,6 +3327,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to assign process to job object, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3127,6 +3347,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch if (ResumeThread(pi.hThread) == (DWORD)-1) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to resume thread for spawn, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3159,10 +3380,17 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch *stderr_read_int = err_f; } - return (intptr_t)pi.hProcess; + pid_t pid = (intptr_t)pi.hProcess; + pid_publish(pid_slot, pid); + + return pid; } -uint64_t os_display_pid(pid_t pid) +/** + * Get the operating system process ID suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. + */ +uint64_t os_pid(pid_t pid) { if (pid <= 0) return 0; @@ -3171,7 +3399,26 @@ uint64_t os_display_pid(pid_t pid) return (uint64_t)GetProcessId(h); } -pid_t os_wait(pid_t pid, int* status) +/** + * Get the operating system process ID of a published process slot suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. + */ +uint64_t os_slot_pid(const pid_t* pid_slot) +{ + if (pid_slot == 0) + return 0; + + exec_lock(); + uint64_t pid = os_pid(*pid_slot); + exec_unlock(); + + return pid; +} + +/** + * Wait for the child process to terminate. + */ +pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot) { HANDLE h = (void*)pid; DWORD exit_code; @@ -3180,13 +3427,20 @@ pid_t os_wait(pid_t pid, int* status) if (GetExitCodeProcess(h, &exit_code)) { *status = exit_code; + if (pid_slot != 0) + pid_unpublish(pid_slot, pid); return pid; } else { windows_errno(GetLastError()); + if (pid_slot != 0) + pid_unpublish(pid_slot, pid); return -1; } } +/** + * Release the process reference returned by os_spawn(). + */ void os_dispose(pid_t pid) { if (pid > 0) @@ -3194,118 +3448,62 @@ void os_dispose(pid_t pid) } /** - * Gracefully terminate a process created by os_spawn() on Windows. - * - * os_spawn() creates console applications with both CREATE_NO_WINDOW and - * CREATE_NEW_PROCESS_GROUP. - * - * CREATE_NO_WINDOW does not mean that the child is created as a detached - * process with no console support. For a console application it provides a - * windowless console, so the process can still receive console control events. - * - * Note that the Microsoft documentation is somewhat ambiguous here: it says - * that with CREATE_NO_WINDOW "the console handle for the application is not - * set", which can be misread as meaning that the process has no console at all. - * In practice, CREATE_NO_WINDOW creates a console session without a visible - * console window, and is distinct from DETACHED_PROCESS, which creates a - * process with no attached console. - * - * This behavior is documented and experimentally verified here: - * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags - * https://github.com/rprichard/win32-console-docs - * - * CREATE_NEW_PROCESS_GROUP creates a new process group whose identifier is - * the process ID of the child created by CreateProcess(). This allows - * CTRL_BREAK_EVENT to be directed specifically to the child's process group: - * - * CREATE_NO_WINDOW - * -> child has a windowless console - * - * CREATE_NEW_PROCESS_GROUP - * -> child PID is also the process-group ID - * - * GenerateConsoleCtrlEvent() can signal only processes sharing the console - * of the caller. Therefore, this function first detaches the calling process - * from its current console, if any, temporarily attaches it to the child's - * console using AttachConsole(child_pid), and then sends: - * - * GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid) - * - * The child process group then receives CTRL_BREAK_EVENT and can perform an - * orderly shutdown instead of being forcibly terminated. - * - * This works particularly well when called by a Windows Service. Services - * normally have no console of their own, so the sequence is simply: - * - * Service without a console - * -> AttachConsole(child_pid) - * -> GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid) - * -> child receives CTRL_BREAK_EVENT - * -> FreeConsole() - * -> Service is console-less again - * - * Note that console attachment is process-wide. If the calling process was - * already attached to a console, the initial FreeConsole() detaches it and - * that previous console is not restored on return. FreeConsole() and - * AttachConsole() also reset the process console control-handler table. - * For this reason, this function is best suited to callers that normally - * have no console, such as Windows Services. - * - * If attaching to the child's console fails, fall back to TerminateProcess(). + * Gracefully terminate a published process. */ -int os_term(pid_t pid) +int os_term(pid_t* pid_slot) { - HANDLE h = (void*)pid; - DWORD id = GetProcessId(h); - - if (id == 0) { + if (pid_slot == 0) { errno = EINVAL; return -1; } - /* detach from current console (if any; background service has no console) */ - FreeConsole(); - - /* attach to the child's invisible console host */ - if (!AttachConsole(id)) { - /* fallback: terminate process forcibly if console attachment fails */ - if (!TerminateProcess(h, 1)) { - windows_errno(GetLastError()); - return -1; - } + exec_lock(); + pid_t pid = *pid_slot; + if (pid == PID_SPAWN) { + *pid_slot = PID_TERM; + exec_unlock(); return 0; } - - /* disable Ctrl-C for the PARENT so we don't kill ourselves */ - SetConsoleCtrlHandler(0, TRUE); - - /* this will now reach the child's SetConsoleCtrlHandler */ - GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id); - - /* detach from child console, returning calling service process to its console-less state */ - FreeConsole(); - - return 0; + if (pid == PID_NONE || pid == PID_TERM || pid == PID_KILL) { + exec_unlock(); + return 0; + } + int ret = pid_term(pid); + exec_unlock(); + return ret; } -int os_kill(pid_t pid) +/** + * Forcibly terminate a published process. + */ +int os_kill(pid_t* pid_slot) { - if (pid <= 0) { + if (pid_slot == 0) { errno = EINVAL; return -1; } - HANDLE h = (void*)pid; - - if (!TerminateProcess(h, 1)) { - windows_errno(GetLastError()); - return -1; + exec_lock(); + pid_t pid = *pid_slot; + if (pid == PID_NONE || pid == PID_SPAWN || pid == PID_TERM) { + *pid_slot = PID_KILL; + exec_unlock(); + return 0; } - - return 0; + if (pid == PID_KILL) { + exec_unlock(); + return 0; + } + *pid_slot = PID_KILL; + int ret = pid_kill(pid); + exec_unlock(); + return ret; } -int os_command(const char* command, const char* run_as_user, const char* stdin_text) +/** + * Execute a shell command synchronously and wait for termination. + */ +int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot) { wchar_t conv[CONV_MAX]; HANDLE stdin_read_handle; @@ -3371,6 +3569,14 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t */ const wchar_t* cwd = L"C:\\"; + if (pid_spawn_begin(pid_slot) != 0) { + CloseHandle(stdin_read_handle); + CloseHandle(stdin_write_handle); + CloseHandle(nul); + process_startup_done(&startup); + return -1; + } + /* * No drop of privilege requested * Run exactly as the parent daemon @@ -3400,6 +3606,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t */ if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + pid_unpublish(pid_slot, 0); process_startup_done(&startup); CloseHandle(stdin_read_handle); CloseHandle(stdin_write_handle); @@ -3410,6 +3617,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); process_startup_done(&startup); CloseHandle(stdin_read_handle); CloseHandle(stdin_write_handle); @@ -3424,6 +3632,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); process_startup_done(&startup); CloseHandle(stdin_read_handle); CloseHandle(stdin_write_handle); @@ -3453,6 +3662,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (!ret) { windows_errno(create_error); os_syslog(OS_LVL_INFO, "failed to create process '%s' for command, errno=%s(%d)", command, strerror(errno), errno); + pid_unpublish(pid_slot, 0); CloseHandle(stdin_read_handle); CloseHandle(stdin_write_handle); CloseHandle(nul); @@ -3470,6 +3680,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to assign command process to job object, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3482,6 +3693,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (ResumeThread(pi.hThread) == (DWORD)-1) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to resume thread for command, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3489,6 +3701,9 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t return -1; } + pid_t pid = (intptr_t)pi.hProcess; + pid_publish(pid_slot, pid); + /* write the string to the child's STDIN */ if (stdin_text != 0) { size_t len = strlen(stdin_text); @@ -3516,6 +3731,8 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t DWORD status; GetExitCodeProcess(pi.hProcess, &status); + pid_unpublish(pid_slot, pid); + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3673,7 +3890,10 @@ bail: return new_env; } -int os_script(char** argv, char** envp, const char* run_as_user) +/** + * Execute an external script synchronously and wait for termination. + */ +int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot) { wchar_t conv[CONV_MAX]; PROCESS_INFORMATION pi; @@ -3738,6 +3958,10 @@ int os_script(char** argv, char** envp, const char* run_as_user) */ const wchar_t* cwd = L"C:\\"; + if (pid_spawn_begin(pid_slot) != 0) { + return -1; + } + /* * No drop of privilege requested * Run exactly as the parent daemon @@ -3752,6 +3976,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!base_env) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to get environment strings, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); return -1; } combined_env = env_combine(base_env, envp); @@ -3759,6 +3984,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!combined_env) { errno = ENOMEM; os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)"); + pid_unpublish(pid_slot, 0); return -1; } creation_flags |= CREATE_UNICODE_ENVIRONMENT; @@ -3789,12 +4015,14 @@ int os_script(char** argv, char** envp, const char* run_as_user) */ if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) { os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService"); + pid_unpublish(pid_slot, 0); return -1; } if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); return -1; } @@ -3805,6 +4033,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!CreateEnvironmentBlock(&env, h_token, FALSE)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno); + pid_unpublish(pid_slot, 0); CloseHandle(h_token); return -1; } @@ -3815,6 +4044,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!combined_env) { errno = ENOMEM; os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)"); + pid_unpublish(pid_slot, 0); DestroyEnvironmentBlock(env); CloseHandle(h_token); return -1; @@ -3841,6 +4071,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!ret) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to create process '%s' for script, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno); + pid_unpublish(pid_slot, 0); return -1; } @@ -3849,6 +4080,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to assign script process to job object, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -3860,17 +4092,23 @@ int os_script(char** argv, char** envp, const char* run_as_user) if (ResumeThread(pi.hThread) == (DWORD)-1) { windows_errno(GetLastError()); os_syslog(OS_LVL_INFO, "failed to resume thread for script, errno=%s(%d)", strerror(errno), errno); + pid_unpublish(pid_slot, 0); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return -1; } + pid_t pid = (intptr_t)pi.hProcess; + pid_publish(pid_slot, pid); + WaitForSingleObject(pi.hProcess, INFINITE); DWORD status; GetExitCodeProcess(pi.hProcess, &status); + pid_unpublish(pid_slot, pid); + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -4093,6 +4331,11 @@ void os_init(unsigned opt) os_exit(); } + if (windows_mutex_init(&exec_mutex, 0) != 0) { + os_syslog(OS_LVL_CRITICAL, "error calling windows_mutex_init()"); + os_exit(); + } + ntdll = GetModuleHandle("NTDLL.DLL"); if (!ntdll) { os_syslog(OS_LVL_CRITICAL, "error loading the NTDLL module"); @@ -4161,6 +4404,7 @@ void os_done(void) /* delete the thread local storage for strerror() */ windows_key_delete(last_error); + windows_mutex_destroy(&exec_mutex); windows_mutex_destroy(&tick_lock); /* restore the normal execution level */ diff --git a/os/os.h b/os/os.h index 08ecf290..dfce4ad6 100644 --- a/os/os.h +++ b/os/os.h @@ -116,33 +116,56 @@ int os_pclose(OS_FILE* stream); /** * Spawn a new process with the specified argument vector, optionally capturing stdout and/or stderr. + * * The caller is expected to call os_privileges_acquire() before this operation if permission is needed. + * + * Process Reference Slots (pid_slot): + * If pid_slot is not NULL, process creation and the resulting process reference are published through it + * to enable safe, race-free process tracking and termination (os_term, os_kill) across concurrent threads. + * If os_kill() was called on pid_slot, process creation is rejected immediately and this function + * returns -1 with errno set to ECANCELED without spawning a child process. In contrast, os_term() on an + * idle slot is a no-op and does not prevent subsequent process creation. + * The same pid_slot pointer should be passed to os_wait() to unpublish it when the process terminates. + * * \param argv Array of command line arguments. * \param stdout_read_fd Pointer to store file descriptor for stdout, or NULL to redirect to /dev/null. * \param stderr_read_fd Pointer to store file descriptor for stderr, or NULL to redirect to /dev/null. * \param run_as_user User to run script as (NULL for current user). + * \param pid_slot Optional pointer used to publish the process reference, or NULL if concurrent termination is not required. * \return Process reference of spawned process, or -1 on failure. */ -pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user); +pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user, pid_t* pid_slot); /** - * Get the operating system process ID suitable for display. + * Get the operating system process ID suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. * On Unix the process reference is already the PID. * On Windows the process reference is a HANDLE and the actual PID is obtained with GetProcessId(). * \param pid Process reference returned by os_spawn(). * \return Operating system process ID, or 0 if unavailable. */ -uint64_t os_display_pid(pid_t pid); +uint64_t os_pid(pid_t pid); + +/** + * Get the operating system process ID of a published process slot suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. + * Internal synchronization states (including creation in progress or persistent kill) always return 0. + * \param pid_slot Pointer to a published process reference slot. + * \return Operating system process ID, or 0 if no process is currently published. + */ +uint64_t os_slot_pid(const pid_t* pid_slot); /** * Wait for the child process to terminate. + * If pid_slot is not NULL, the process reference is unpublished before this function returns. * This function does not release the process reference returned by os_spawn(). * The caller must eventually call os_dispose(). * \param pid Process reference returned by os_spawn(). * \param status Pointer to store the exit status. + * \param pid_slot Optional pointer previously passed to os_spawn(), or NULL if the process was not published. * \return Child process reference on success, -1 on failure. */ -pid_t os_wait(pid_t pid, int* status); +pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot); /** * Release the process reference returned by os_spawn(). @@ -152,20 +175,36 @@ pid_t os_wait(pid_t pid, int* status); void os_dispose(pid_t pid); /** - * Terminate gracefully a process. Intended for daemon/service processes controlling child processes. - * The caller retains ownership and must eventually call both os_wait() and os_dispose() as appropriate. - * \param pid Process reference of the process to terminate. - * \return 0 on success, -1 on failure. + * Gracefully terminate a published process reference. + * + * If a process is currently running in the slot, it is gracefully requested to terminate. + * If process creation is in progress, the termination request is recorded and applied when the process is published. + * If no process is active (slot is 0/idle), there is nothing to terminate: this function is a harmless no-op, + * returning 0 without modifying the slot state or preventing subsequent process creation. + * + * The caller retains ownership for spawned processes and must eventually call both os_wait() and os_dispose() + * as appropriate. + * \param pid_slot Pointer to the published process reference slot. + * \return 0 on success or if no process is active, -1 on failure. */ -int os_term(pid_t pid); +int os_term(pid_t* pid_slot); /** - * Forcibly terminate a process. - * The caller retains ownership and must eventually call both os_wait() and os_dispose() as appropriate. - * \param pid Process reference of the process to kill. - * \return 0 on success, -1 on failure. + * Forcibly terminate a published process reference. + * + * If a process is currently running in the slot, it is terminated immediately. + * If process creation is in progress, the kill request is recorded and the child is killed upon publication. + * If no process is active (slot is idle), the kill request is recorded persistently in the slot so that + * any future process creation attempted with this slot is rejected immediately without spawning. + * + * The slot retains the persistent kill state until explicitly reset by the caller (e.g. *pid_slot = 0). + * The caller retains ownership for spawned processes and must eventually call both os_wait() and os_dispose() + * as appropriate. + * + * \param pid_slot Pointer to the published process reference slot. + * \return 0 on success or if recorded in the slot, -1 on failure. */ -int os_kill(pid_t pid); +int os_kill(pid_t* pid_slot); /** * Fork and execute a verified executable, discarding all I/O. @@ -188,22 +227,30 @@ int os_spawn_and_wait(const char** argv); /** * Execute a system command with optional user context and input. * The caller is expected to call os_privileges_acquire() before this operation if permission is needed. + * If pid_slot is not NULL, the child process is published for concurrent termination while blocked. + * If os_kill() was called on pid_slot, process creation is rejected immediately and this function + * returns -1 with errno set to ECANCELED without executing the command. * \param command Command to execute. * \param run_as_user User to run command as (NULL for current user). * \param stdin_text Text to provide as stdin (NULL for no input). - * \return Exit status of command. + * \param pid_slot Optional pointer to publish the process reference while active, or NULL. + * \return Exit status of command, or -1 on failure. */ -int os_command(const char* command, const char* run_as_user, const char* stdin_text); +int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot); /** * Execute a script file with specified user context. * The caller is expected to call os_privileges_acquire() before this operation if permission is needed. + * If pid_slot is not NULL, the child process is published for concurrent termination while blocked. + * If os_kill() was called on pid_slot, process creation is rejected immediately and this function + * returns -1 with errno set to ECANCELED without executing the script. * \param argv Array of command line arguments. * \param envp Environment variables (NULL-terminated list of strings). * \param run_as_user User to run script as (NULL for current user). - * \return Exit status of script. + * \param pid_slot Optional pointer to publish the process reference while active, or NULL. + * \return Exit status of script, or -1 on failure. */ -int os_script(char** argv, char** envp, const char* run_as_user); +int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot); /** * Validates a string for exec. diff --git a/os/unix.c b/os/unix.c index 734a6aef..c0c74a03 100644 --- a/os/unix.c +++ b/os/unix.c @@ -876,11 +876,148 @@ bail: #endif } +#define PID_NONE ((pid_t)0) /**< No process is active. */ +#define PID_SPAWN ((pid_t)-1) /**< Process creation is in progress. */ +#define PID_TERM ((pid_t)-2) /**< Process termination was requested while creation is in progress. */ +#define PID_KILL ((pid_t)-3) /**< Process kill was requested; rejects future spawns or terminates in-progress/active processes. */ + +static pthread_mutex_t exec_mutex = PTHREAD_MUTEX_INITIALIZER; + +/** + * Acquire the process execution mutex. + */ +static void exec_lock(void) +{ + pthread_mutex_lock(&exec_mutex); +} + +/** + * Release the process execution mutex. + */ +static void exec_unlock(void) +{ + pthread_mutex_unlock(&exec_mutex); +} + +/** + * Forcibly terminate the specified raw process reference. + */ +static int pid_kill(pid_t pid) +{ + if (pid <= 0) { + errno = EINVAL; + return -1; + } + + /* Send SIGKILL signal to the negative PID to target the entire Process Group */ + int ret = kill(-pid, SIGKILL); + + /* + * If setpgid(0, 0) has not yet executed in the child, process group -pid + * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. + */ + if (ret < 0 && errno == ESRCH) { + ret = kill(pid, SIGKILL); + } + + return ret; +} + +/** + * Gracefully terminate the specified raw process reference. + */ +static int pid_term(pid_t pid) +{ + if (pid <= 0) { + errno = EINVAL; + return -1; + } + + /* + * Send signal to the negative PID to target the entire Process Group. + * This ensures that SnapRAID and any programs it may have spawned are + * terminated together, preventing orphaned worker processes. + */ + int ret = kill(-pid, SIGTERM); + + /* + * If setpgid(0, 0) has not yet executed in the child, process group -pid + * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. + */ + if (ret < 0 && errno == ESRCH) { + ret = kill(pid, SIGTERM); + } + + return ret; +} + +/** + * Mark the start of process creation in a process-reference slot. + */ +static int pid_spawn_begin(pid_t* pid_slot) +{ + if (pid_slot == 0) + return 0; + + exec_lock(); + if (*pid_slot == PID_KILL) { + exec_unlock(); + errno = ECANCELED; + return -1; + } + *pid_slot = PID_SPAWN; + exec_unlock(); + return 0; +} + +/** + * Publish a newly created process reference, or terminate it immediately if canceled during spawn. + */ +static void pid_publish(pid_t* pid_slot, pid_t pid) +{ + if (pid_slot == 0) + return; + + exec_lock(); + if (pid <= 0) { + if (*pid_slot != PID_KILL) + *pid_slot = PID_NONE; + } else { + pid_t prev = *pid_slot; + if (prev == PID_KILL) { + *pid_slot = PID_KILL; + pid_kill(pid); + } else { + *pid_slot = pid; + + if (prev == PID_TERM) + pid_term(pid); + } + } + exec_unlock(); +} + +/** + * Unpublish a process reference from a slot. + */ +static void pid_unpublish(pid_t* pid_slot, pid_t pid) +{ + if (pid_slot == 0) + return; + + exec_lock(); + if (*pid_slot == pid || (*pid_slot < 0 && *pid_slot != PID_KILL)) + *pid_slot = PID_NONE; + exec_unlock(); +} + /** * Waits for a process with a timeout, killing the entire process group if exceeded. + * Observes process termination without reaping (via waitid WNOWAIT), leaving the process + * as a zombie until the caller unpublishes the process slot and performs the final reap. * Optionally delivers input to input_fd using non-blocking writes. */ -static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int input_fd, const char* input, int* timed_out, int* input_error) +static pid_t waitpid_timeout_group(pid_t pid, int64_t start, int input_fd, const char* input, int* timed_out, int* input_error) { size_t input_len = input != 0 ? strlen(input) : 0; size_t input_pos = 0; @@ -892,15 +1029,23 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in *input_error = 0; while (1) { - ret = waitpid(pid, status, WNOHANG); + /* + * Poll child termination with WNOWAIT so the terminated child remains + * in zombie state with its PID reserved in the kernel until unpublished. + * POSIX specifies that with WNOHANG, waitid() returns 0 and sets + * si_pid to 0 if the child state has not changed. + */ + siginfo_t info; + memset(&info, 0, sizeof(info)); + int wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOHANG | WNOWAIT); - if (ret == pid) { + if (wret == 0 && info.si_pid == pid) { if (input_fd != -1) close(input_fd); - return ret; + return pid; } - if (ret < 0) { + if (wret < 0) { if (errno == EINTR) continue; @@ -959,15 +1104,22 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in input_fd = -1; } - if (os_kill(pid) != 0 && errno != ESRCH) + if (pid_kill(pid) != 0 && errno != ESRCH) return -1; - /* reap the process-group leader */ + /* + * Wait for the process-group leader to terminate after SIGKILL. + * Use WNOWAIT so the child remains a zombie until slot unpublication. + */ do { - ret = waitpid(pid, status, 0); + memset(&info, 0, sizeof(info)); + ret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT); } while (ret < 0 && errno == EINTR); - return ret; + if (ret < 0) + return -1; + + return pid; } os_usleep(100000); @@ -977,7 +1129,7 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in /** * Executes a script directly via its file descriptor. */ -int os_script(char** argv, char** envp, const char* run_as_user) +int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot) { char resolved_path[PATH_MAX]; pid_t pid; @@ -1008,9 +1160,15 @@ int os_script(char** argv, char** envp, const char* run_as_user) pid_t ppid = getpid(); #endif + if (pid_spawn_begin(pid_slot) != 0) { + close(fd); + return -1; + } + pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork script=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); + pid_unpublish(pid_slot, 0); close(fd); return -1; } @@ -1114,7 +1272,24 @@ int os_script(char** argv, char** envp, const char* run_as_user) /* parent process */ close(fd); - ret = waitpid_timeout_group(pid, &status, start, -1, 0, &timed_out, 0); + pid_publish(pid_slot, pid); + + ret = waitpid_timeout_group(pid, start, -1, 0, &timed_out, 0); + + /* + * Unpublish the PID slot before reaping the zombie process. + * While the child is a zombie, the OS kernel keeps the PID reserved, + * preventing any new process from reusing it. Unpublishing the slot first + * guarantees that concurrent os_term()/os_kill() calls will not target + * a recycled PID once waitpid() finally reaps the process. + */ + pid_unpublish(pid_slot, pid); + + if (ret != -1) { + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); + } if (ret == -1) { os_syslog(OS_LVL_INFO, "failed to wait for script, path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); @@ -1153,7 +1328,7 @@ int os_script(char** argv, char** envp, const char* run_as_user) } } -int os_command(const char* command, const char* run_as_user, const char* stdin_text) +int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot) { pid_t pid; int ret; @@ -1195,9 +1370,18 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t pid_t ppid = getpid(); #endif + if (pid_spawn_begin(pid_slot) != 0) { + if (pipe_fds[0] != -1) { + close(pipe_fds[0]); + close(pipe_fds[1]); + } + return -1; + } + pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork command=%s, errno=%s(%d)", command, strerror(errno), errno); + pid_unpublish(pid_slot, 0); if (pipe_fds[0] != -1) { close(pipe_fds[0]); close(pipe_fds[1]); @@ -1279,12 +1463,29 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t if (pipe_fds[0] != -1) close(pipe_fds[0]); /* close unused read end */ - ret = waitpid_timeout_group(pid, &status, start, pipe_fds[1], stdin_text, &timed_out, &input_error); + pid_publish(pid_slot, pid); + + ret = waitpid_timeout_group(pid, start, pipe_fds[1], stdin_text, &timed_out, &input_error); pipe_fds[1] = -1; + /* + * Unpublish the PID slot before reaping the zombie process. + * While the child is a zombie, the OS kernel keeps the PID reserved, + * preventing any new process from reusing it. Unpublishing the slot first + * guarantees that concurrent os_term()/os_kill() calls will not target + * a recycled PID once waitpid() finally reaps the process. + */ + pid_unpublish(pid_slot, pid); + if (input_error != 0) os_syslog(OS_LVL_INFO, "failed to write stdin to command %s, errno=%s(%d)", command, strerror(input_error), input_error); + if (ret != -1) { + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); + } + if (ret == -1) { os_syslog(OS_LVL_INFO, "failed to wait for command, command=%s, errno=%s(%d)", command, strerror(errno), errno); return -1; @@ -1333,7 +1534,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t * * Returns the child PID on success, or -1 on failure. */ -pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user) +pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user, pid_t* pid_slot) { char resolved_path[PATH_MAX]; int out_pipe[2]; @@ -1385,9 +1586,23 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char pid_t ppid = getpid(); #endif + if (pid_spawn_begin(pid_slot) != 0) { + if (has_out) { + close(out_pipe[0]); + close(out_pipe[1]); + } + if (has_err) { + close(err_pipe[0]); + close(err_pipe[1]); + } + close(fd); + return -1; + } + pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); + pid_unpublish(pid_slot, 0); if (has_out) { close(out_pipe[0]); close(out_pipe[1]); @@ -1484,6 +1699,8 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char /* parent */ close(fd); + pid_publish(pid_slot, pid); + if (has_out) { #ifdef F_SETPIPE_SZ if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) { @@ -1507,7 +1724,11 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char return pid; } -uint64_t os_display_pid(pid_t pid) +/** + * Get the operating system process ID suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. + */ +uint64_t os_pid(pid_t pid) { if (pid <= 0) return 0; @@ -1515,10 +1736,51 @@ uint64_t os_display_pid(pid_t pid) return (uint64_t)pid; } -pid_t os_wait(pid_t pid, int* status) +/** + * Get the operating system process ID of a published process slot suitable for display or logging. + * This function is intended for display purposes only and not as a process reference. + */ +uint64_t os_slot_pid(const pid_t* pid_slot) +{ + if (pid_slot == 0) + return 0; + + exec_lock(); + uint64_t pid = os_pid(*pid_slot); + exec_unlock(); + + return pid; +} + +/** + * Wait for the child process to terminate. + */ +pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot) { pid_t ret; + /* + * If published in a slot, first wait for termination without reaping (WNOWAIT). + * The child enters zombie state, holding its PID reserved in the kernel so + * no newly created process can receive the same PID. We then unpublish + * the slot before the final waitpid() reap, ensuring concurrent termination + * requests (os_term/os_kill) never signal a recycled PID. + */ + if (pid_slot != 0) { + siginfo_t info; + int wret; + + do { + memset(&info, 0, sizeof(info)); + wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT); + } while (wret == -1 && errno == EINTR); + + if (wret == -1) + os_syslog(OS_LVL_INFO, "failed to waitid, errno=%s(%d)", strerror(errno), errno); + + pid_unpublish(pid_slot, pid); + } + do { ret = waitpid(pid, status, 0); } while (ret == -1 && errno == EINTR); @@ -1530,60 +1792,70 @@ pid_t os_wait(pid_t pid, int* status) return ret; } +/** + * Release the process reference returned by os_spawn(). + */ void os_dispose(pid_t pid) { (void)pid; } -int os_term(pid_t pid) +/** + * Gracefully terminate a published process. + */ +int os_term(pid_t* pid_slot) { - if (pid <= 0) { + if (pid_slot == 0) { errno = EINVAL; return -1; } - /* - * Send signal to the negative PID to target the entire Process Group. - * This ensures that SnapRAID and any programs it may have spawned are - * terminated together, preventing orphaned worker processes. - */ - int ret = kill(-pid, SIGTERM); - - /* - * If setpgid(0, 0) has not yet executed in the child, process group -pid - * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. - */ - if (ret < 0 && errno == ESRCH) { - ret = kill(pid, SIGTERM); + exec_lock(); + pid_t pid = *pid_slot; + if (pid == PID_SPAWN) { + *pid_slot = PID_TERM; + exec_unlock(); + return 0; } - + if (pid == PID_NONE || pid == PID_TERM || pid == PID_KILL) { + exec_unlock(); + return 0; + } + int ret = pid_term(pid); + exec_unlock(); return ret; } -int os_kill(pid_t pid) +/** + * Forcibly terminate a published process. + */ +int os_kill(pid_t* pid_slot) { - if (pid <= 0) { + if (pid_slot == 0) { errno = EINVAL; return -1; } - /* Send SIGKILL signal to the negative PID to target the entire Process Group */ - int ret = kill(-pid, SIGKILL); - - /* - * If setpgid(0, 0) has not yet executed in the child, process group -pid - * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. - */ - if (ret < 0 && errno == ESRCH) { - ret = kill(pid, SIGKILL); + exec_lock(); + pid_t pid = *pid_slot; + if (pid == PID_NONE || pid == PID_SPAWN || pid == PID_TERM) { + *pid_slot = PID_KILL; + exec_unlock(); + return 0; } - + if (pid == PID_KILL) { + exec_unlock(); + return 0; + } + *pid_slot = PID_KILL; + int ret = pid_kill(pid); + exec_unlock(); return ret; } int os_spawn_and_wait(const char** argv) { - pid_t pid = os_spawn((char**)argv, 0, 0, 0); + pid_t pid = os_spawn((char**)argv, 0, 0, 0, 0); if (pid < 0) { /* LCOV_EXCL_START */ return -1; @@ -1591,7 +1863,7 @@ int os_spawn_and_wait(const char** argv) } int status; - int ret = os_wait(pid, &status); + int ret = os_wait(pid, &status, 0); os_dispose(pid); if (ret == -1) { /* LCOV_EXCL_START */ @@ -1615,7 +1887,7 @@ OS_FILE* os_popen(const char** argv) if (!os_file) return 0; - pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0); + pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0, 0); if (pid < 0) { free(os_file); return 0; @@ -1627,7 +1899,7 @@ OS_FILE* os_popen(const char** argv) int saved_errno = errno; close(stdout_fd); int status; - os_wait(pid, &status); + os_wait(pid, &status, 0); os_dispose(pid); errno = saved_errno; free(os_file); @@ -1657,7 +1929,7 @@ int os_pclose(OS_FILE* stream) fclose(fp); int status = 0; - int ret = os_wait(pid, &status); + int ret = os_wait(pid, &status, 0); os_dispose(pid); if (ret < 0) { return -1;