Add timeout_sec parameter to os_command and os_script interface

This commit is contained in:
Andrea Mazzoleni 2026-09-22 08:15:21 +02:00
parent c6951393e8
commit 819b843163
3 changed files with 117 additions and 94 deletions

View file

@ -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)

17
os/os.h
View file

@ -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.
*/

144
os/unix.c
View file

@ -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)