Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions src/tools/shell.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
#include "sandbox/sandbox.h"
#include "sandbox/allowlist.h"
#include "cJSON.h"
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#define DEFAULT_TIMEOUT_SEC 60
Expand Down Expand Up @@ -56,6 +58,49 @@ static int fallback_is_blocked(const char *cmd)

static const config_t *g_shell_cfg;

static void kill_command_tree(pid_t pid)
{
if (kill(-pid, SIGKILL) != 0)
(void)kill(pid, SIGKILL);
}

/* Kill leftover children after drain (timeout or output cap), matching sandbox_exec. */
static int reap_running_child(pid_t pid)
{
int status = 0;
int wr = waitpid(pid, &status, WNOHANG);
int i;
struct timespec ts;

if (wr != 0)
return 0;
kill_command_tree(pid);
for (i = 0; i < 40; i++) {
wr = waitpid(pid, &status, WNOHANG);
if (wr != 0)
return 1;
ts.tv_sec = 0;
ts.tv_nsec = 50 * 1000 * 1000;
(void)nanosleep(&ts, NULL);
}
(void)waitpid(pid, &status, 0);
return 1;
}

static void append_unsandboxed_output(char *result_buf, size_t max_len, size_t *total,
const char *chunk, size_t n)
{
size_t add = n;

if (*total + add >= max_len - 1)
add = max_len - 1 - *total;
if (add == 0)
return;
memcpy(result_buf + *total, chunk, add);
*total += add;
result_buf[*total] = '\0';
}

/* ------------------------------------------------------------------ */
/* Unsandboxed execution (fork + poll + waitpid) */
/* ------------------------------------------------------------------ */
Expand All @@ -68,7 +113,6 @@ static int run_unsandboxed(const char *command, int timeout_sec,
size_t total = 0;
int timed_out = 0;
int elapsed_ms = 0;
int status;
char buf[256];
if (pipe(pipefd) != 0) {
snprintf(result_buf, max_len, "{\"error\":\"pipe failed\"}");
Expand All @@ -86,9 +130,11 @@ static int run_unsandboxed(const char *command, int timeout_sec,
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
(void)setpgid(0, 0);
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
_exit(127);
}
(void)setpgid(pid, pid);
close(pipefd[1]);
result_buf[0] = '\0';
while (total < max_len - 1 && elapsed_ms < timeout_sec * 1000) {
Expand All @@ -102,28 +148,27 @@ static int run_unsandboxed(const char *command, int timeout_sec,
rem = timeout_sec * 1000 - elapsed_ms;
if (rem > 5000) rem = 5000;
r = poll(&pfd, 1, rem);
if (r < 0) break;
if (r < 0) {
if (errno == EINTR) continue;
break;
}
if (r == 0) {
elapsed_ms += rem;
if (elapsed_ms >= timeout_sec * 1000) {
timed_out = 1;
kill(pid, SIGKILL);
kill_command_tree(pid);
break;
}
continue;
}
n = read(pipefd[0], buf, sizeof(buf) - 1);
n = read(pipefd[0], buf, sizeof(buf));
if (n <= 0) break;
buf[n] = '\0';
{
size_t add = (size_t)n;
if (total + add >= max_len - 1) add = max_len - 1 - total;
memcpy(result_buf + total, buf, add + 1);
total += add;
}
append_unsandboxed_output(result_buf, max_len, &total, buf, (size_t)n);
}
result_buf[total] = '\0';
close(pipefd[0]);
waitpid(pid, &status, 0);
if (reap_running_child(pid))
timed_out = 1;
if (timed_out && total < max_len - 32)
snprintf(result_buf + total, max_len - total, "\n[Command timed out]");
return 0;
Expand Down
44 changes: 44 additions & 0 deletions tests/test_shell.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
#include "tools/tool.h"
#include "tools/shell.h"
#include "core/config.h"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int tests_run = 0;
static int tests_failed = 0;
Expand Down Expand Up @@ -68,13 +71,54 @@ static void test_shell_missing_command(void)
MU_ASSERT(r == -1, "missing command returns -1");
}

static void output_cap_hang_watchdog(int sig)
{
(void)sig;
fprintf(stderr, "FAIL: unsandboxed shell hung after filling the output cap\n");
_exit(2);
}

static int buf_has_nul(const char *buf, size_t n)
{
size_t i;

for (i = 0; i < n; i++) {
if (buf[i] == '\0')
return 1;
}
return 0;
}

static void test_shell_caps_output_without_hanging(void)
{
const tool_t *t = tool_shell_get();
char buf[64];
int r;

tool_shell_set_config(NULL);
memset(buf, 'B', sizeof(buf));
signal(SIGALRM, output_cap_hang_watchdog);
alarm(5);
/* Fill the 64-byte cap, then sleep so the child stays alive without
* writing (SIGPIPE will not reap it). Unsandboxed waitpid used to block
* forever on this path.
*/
r = t->execute("{\"command\":\"printf '%080d' 0; sleep 9999\"}", buf,
sizeof(buf));
alarm(0);
signal(SIGALRM, SIG_DFL);
MU_ASSERT(r == 0, "capped shell command returns");
MU_ASSERT(buf_has_nul(buf, sizeof(buf)), "capped output is NUL-terminated");
}

int main(void)
{
MU_RUN(test_shell_blocked_rm_rf);
MU_RUN(test_shell_blocked_mkfs);
MU_RUN(test_shell_ls_succeeds);
MU_RUN(test_shell_invalid_json);
MU_RUN(test_shell_missing_command);
MU_RUN(test_shell_caps_output_without_hanging);
printf("%d tests run, %d failed\n", tests_run, tests_failed);
return tests_failed ? 1 : 0;
}
Loading