From 819b843163c96aee5d83fb65e0dd5eb706c45370 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Tue, 22 Sep 2026 08:15:21 +0200 Subject: [PATCH] Add timeout_sec parameter to os_command and os_script interface --- os/mingw.c | 50 +++++++++++++++++-- os/os.h | 17 +++++-- os/unix.c | 144 ++++++++++++++++++++++------------------------------- 3 files changed, 117 insertions(+), 94 deletions(-) diff --git a/os/mingw.c b/os/mingw.c index 376f5588..0d161f1b 100644 --- a/os/mingw.c +++ b/os/mingw.c @@ -3568,10 +3568,34 @@ int os_kill(pid_t* pid_slot) return ret; } +/** + * Waits for a process with an optional timeout, terminating it if exceeded. + */ +static void wait_process_timeout(HANDLE process, uint64_t timeout_sec, int* timed_out) +{ + DWORD wait_ms = INFINITE; + + if (timeout_sec != 0) { + if (timeout_sec > 0xFFFFFFFE / 1000) + wait_ms = 0xFFFFFFFE; + else + wait_ms = (DWORD)(timeout_sec * 1000); + } + + *timed_out = 0; + + DWORD wait_res = WaitForSingleObject(process, wait_ms); + if (wait_res == WAIT_TIMEOUT) { + *timed_out = 1; + TerminateProcess(process, 1); + WaitForSingleObject(process, INFINITE); + } +} + /** * 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) +int os_command(const char* command, const char* run_as_user, const char* stdin_text, uint64_t timeout_sec, pid_t* pid_slot) { wchar_t conv[CONV_MAX]; HANDLE stdin_read_handle; @@ -3794,7 +3818,8 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t CloseHandle(stdin_write_handle); /* wait for completion and get exit code */ - WaitForSingleObject(pi.hProcess, INFINITE); + int timed_out; + wait_process_timeout(pi.hProcess, timeout_sec, &timed_out); DWORD status; GetExitCodeProcess(pi.hProcess, &status); @@ -3806,6 +3831,12 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t stop = os_tick_sec(); int64_t execution_time = stop - start; + + if (timed_out) { + os_syslog(OS_LVL_INFO, "command %s timeout after %" PRId64 " seconds", command, execution_time); + return -1; + } + if (execution_time > 30) os_syslog(OS_LVL_WARNING, "command %s ran for %" PRId64 " seconds that is unexpectedly long", command, execution_time); @@ -3961,7 +3992,7 @@ bail: /** * 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) +int os_script(char** argv, char** envp, const char* run_as_user, uint64_t timeout_sec, pid_t* pid_slot) { wchar_t conv[CONV_MAX]; PROCESS_INFORMATION pi; @@ -4170,7 +4201,9 @@ int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot pid_t pid = (intptr_t)pi.hProcess; pid_publish(pid_slot, pid); - WaitForSingleObject(pi.hProcess, INFINITE); + /* wait for completion and get exit code */ + int timed_out; + wait_process_timeout(pi.hProcess, timeout_sec, &timed_out); DWORD status; GetExitCodeProcess(pi.hProcess, &status); @@ -4182,6 +4215,12 @@ int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot stop = os_tick_sec(); int64_t execution_time = stop - start; + + if (timed_out) { + os_syslog(OS_LVL_INFO, "script %s timeout after %" PRId64 " seconds", resolved_path, execution_time); + return -1; + } + if (execution_time > 30) os_syslog(OS_LVL_WARNING, "script %s took %" PRId64 " seconds", resolved_path, execution_time); @@ -4396,8 +4435,9 @@ void os_privileges_release(void) { } -void os_privileges_drop(void) +int os_privileges_drop(void) { + return 0; } void os_abort(void) diff --git a/os/os.h b/os/os.h index 80aecbbd..a0544181 100644 --- a/os/os.h +++ b/os/os.h @@ -244,10 +244,11 @@ int os_spawn_and_wait(const char** argv); * \param command Command to execute. * \param run_as_user User to run command as (0 for current user). * \param stdin_text Text to provide as stdin (0 for no input). + * \param timeout_sec Execution timeout in seconds (0 for no timeout). * \param pid_slot Optional pointer to publish the process reference while active, or 0. * \return Exit status of command, or -1 on failure. */ -int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot); +int os_command(const char* command, const char* run_as_user, const char* stdin_text, uint64_t timeout_sec, pid_t* pid_slot); /** * Execute a script file with specified user context. @@ -258,10 +259,11 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t * \param argv Array of command line arguments. * \param envp Environment variables (0-terminated list of strings). * \param run_as_user User to run script as (0 for current user). + * \param timeout_sec Execution timeout in seconds (0 for no timeout). * \param pid_slot Optional pointer to publish the process reference while active, or 0. * \return Exit status of script, or -1 on failure. */ -int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot); +int os_script(char** argv, char** envp, const char* run_as_user, uint64_t timeout_sec, pid_t* pid_slot); /** * Validates a string for exec. @@ -351,11 +353,17 @@ void os_privileges_acquire(void); void os_privileges_release(void); /** - * Drop effective privileges permanently to an unprivileged user (e.g., "nobody"). + * Drop effective privileges permanently to an unprivileged user + * (e.g., "nobody"). * Called after startup/initialization is complete to transition the daemon into * the Bracketed Privileges execution mode. + * + * On failure the process credentials may have been partially changed and the + * caller must terminate the process. + * + * \return 0 on success, -1 on failure. */ -void os_privileges_drop(void); +int os_privileges_drop(void); /****************************************************************************/ /* os */ @@ -404,7 +412,6 @@ uint64_t os_tick_sec(void); */ int os_usleep(uint64_t usec); - /** * Abort the process with a stacktrace. */ diff --git a/os/unix.c b/os/unix.c index c7d5d694..bc8e9ae0 100644 --- a/os/unix.c +++ b/os/unix.c @@ -353,11 +353,6 @@ int filephy(const char* path, uint64_t size, uint64_t* physical) /****************************************************************************/ /* exec */ -/** - * Maximum execution time for helper scripts and commands, in seconds. - */ -#define OS_EXEC_TIMEOUT_SEC 300 - /* * Scrubbed environment * Only provide the bare essentials. @@ -1053,7 +1048,7 @@ static void pid_unpublish(pid_t* pid_slot, pid_t pid) * 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, 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, uint64_t timeout_sec, int* timed_out, int* input_error) { size_t input_len = input != 0 ? strlen(input) : 0; size_t input_pos = 0; @@ -1132,7 +1127,7 @@ static pid_t waitpid_timeout_group(pid_t pid, int64_t start, int input_fd, const } } - if (os_tick_sec() - start >= OS_EXEC_TIMEOUT_SEC) { + if (timeout_sec != 0 && os_tick_sec() - start >= timeout_sec) { *timed_out = 1; if (input_fd != -1) { @@ -1165,7 +1160,7 @@ static pid_t waitpid_timeout_group(pid_t pid, int64_t start, int input_fd, const /** * Executes a script directly via its file descriptor. */ -int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot) +int os_script(char** argv, char** envp, const char* run_as_user, uint64_t timeout_sec, pid_t* pid_slot) { char resolved_path[PATH_MAX]; pid_t pid; @@ -1310,7 +1305,7 @@ int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot pid_publish(pid_slot, pid); - ret = waitpid_timeout_group(pid, start, -1, 0, &timed_out, 0); + ret = waitpid_timeout_group(pid, start, -1, 0, timeout_sec, &timed_out, 0); /* * Unpublish the PID slot before reaping the zombie process. @@ -1364,7 +1359,7 @@ int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot } } -int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot) +int os_command(const char* command, const char* run_as_user, const char* stdin_text, uint64_t timeout_sec, pid_t* pid_slot) { pid_t pid; int ret; @@ -1501,7 +1496,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t pid_publish(pid_slot, pid); - ret = waitpid_timeout_group(pid, start, pipe_fds[1], stdin_text, &timed_out, &input_error); + ret = waitpid_timeout_group(pid, start, pipe_fds[1], stdin_text, timeout_sec, &timed_out, &input_error); pipe_fds[1] = -1; /* @@ -1989,7 +1984,7 @@ static int groups_dropped = 0; static gid_t* privileged_groups = 0; static int privileged_group_count = 0; -void os_privileges_drop(void) +int os_privileges_drop(void) { /* * Detect if running as root @@ -2000,80 +1995,61 @@ void os_privileges_drop(void) if (getuid() == 0 || geteuid() == 0) { /* find the unprivileged user "nobody" */ struct passwd* pw = getpwnam("nobody"); - if (pw) { - unpriv_uid = pw->pw_uid; - unpriv_gid = pw->pw_gid; - - /* - * Save the supplementary groups before dropping them. - * They must be restored by os_privileges_acquire() because - * spawned user scripts may depend on them. - */ - privileged_group_count = getgroups(0, 0); - if (privileged_group_count < 0) { - os_syslog(OS_LVL_INFO, "failed to get supplementary group count, errno=%s(%d)", strerror(errno), errno); - os_abort(); - } - - if (privileged_group_count != 0) { - privileged_groups = malloc(privileged_group_count * sizeof(gid_t)); - if (!privileged_groups) { - os_syslog(OS_LVL_CRITICAL, "failed to allocate supplementary group list"); - os_abort(); - } - - if (getgroups(privileged_group_count, privileged_groups) != privileged_group_count) { - os_syslog(OS_LVL_INFO, "failed to get supplementary groups, errno=%s(%d)", strerror(errno), errno); - os_abort(); - } - - if (setgroups(0, 0) != 0) { - if (errno == EPERM) { - /* - * If EPERM, process lacks permission to switch - * supplementary groups. Continue with them active. - */ - os_syslog(OS_LVL_INFO, "permission denied to release supplementary group privileges, continuing with active privileges"); - } else { - os_syslog(OS_LVL_INFO, "failed to release supplementary group privileges, errno=%s(%d)", strerror(errno), errno); - os_abort(); - } - } else { - groups_dropped = 1; - } - } - - if (setegid(unpriv_gid) != 0) { - if (errno == EPERM) { - /* - * If EPERM, process lacks permission to switch privileges - * (e.g. dropped capabilities); continue with active privileges. - */ - os_syslog(OS_LVL_INFO, "permission denied to release group privileges, continuing with active privileges"); - } else { - os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); - os_abort(); - } - } else { - gid_dropped = 1; - } - - if (seteuid(unpriv_uid) != 0) { - if (errno == EPERM) { - /* - * If EPERM, process lacks permission to switch privileges - * (e.g. dropped capabilities); continue with active privileges. - */ - os_syslog(OS_LVL_INFO, "permission denied to release privileges, continuing with active privileges"); - } else { - os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); - os_abort(); - } - } else { - uid_dropped = 1; - } + if (!pw) { + os_syslog(OS_LVL_INFO, "unprivileged user 'nobody' not found"); + return -1; } + + unpriv_uid = pw->pw_uid; + unpriv_gid = pw->pw_gid; + + /* + * Save the supplementary groups before dropping them. + * They must be restored by os_privileges_acquire() because + * spawned user scripts may depend on them. + */ + privileged_group_count = getgroups(0, 0); + if (privileged_group_count < 0) { + os_syslog(OS_LVL_INFO, "failed to get supplementary group count, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + if (privileged_group_count != 0) { + privileged_groups = malloc(privileged_group_count * sizeof(gid_t)); + if (!privileged_groups) { + os_syslog(OS_LVL_CRITICAL, "failed to allocate supplementary group list"); + return -1; + } + + if (getgroups(privileged_group_count, privileged_groups) != privileged_group_count) { + os_syslog(OS_LVL_INFO, "failed to get supplementary groups, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + if (setgroups(0, 0) != 0) { + os_syslog(OS_LVL_INFO, "failed to release supplementary group privileges, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + groups_dropped = 1; + } + + if (setegid(unpriv_gid) != 0) { + os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + gid_dropped = 1; + + if (seteuid(unpriv_uid) != 0) { + os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); + return -1; + } + + uid_dropped = 1; } + + return 0; } void os_privileges_acquire(void)