#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifdef __APPLE__ #include #include #endif #include "os_compat.h" #include #include #include #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #else #include #include #include #include #include #include #include #include #endif void os_run_result_free(os_run_result_t *r) { if (!r) return; free(r->out); r->out = NULL; r->out_len = 0; } int os_path_is_abs(const char *p) { if (!p || !p[0]) return 0; #ifdef _WIN32 if (p[0] == '/' || p[0] == '\\') return 1; if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) && p[1] == ':' && (p[2] == '/' || p[2] == '\\' || p[2] == '\0')) return 1; return 0; #else return p[0] == '/'; #endif } void os_path_norm_slashes(char *p) { if (!p) return; for (; *p; p++) { if (*p == '\\') *p = '/'; } } int os_path_join(char *out, size_t out_sz, const char *a, const char *b) { if (!out || out_sz < 2 || !a) return -1; if (!b || !b[0]) { snprintf(out, out_sz, "%s", a); return 0; } if (os_path_is_abs(b)) { snprintf(out, out_sz, "%s", b); return 0; } size_t al = strlen(a); while (al > 0 && (a[al - 1] == '/' || a[al - 1] == '\\')) al--; #ifdef _WIN32 int n = snprintf(out, out_sz, "%.*s\\%s", (int)al, a, b); #else int n = snprintf(out, out_sz, "%.*s/%s", (int)al, a, b); #endif if (n < 0 || (size_t)n >= out_sz) return -1; return 0; } int os_realpath(const char *path, char *out, size_t out_sz) { if (!path || !out || out_sz < 2) return -1; #ifdef _WIN32 char buf[OS_PATH_MAX]; DWORD n = GetFullPathNameA(path, (DWORD)sizeof(buf), buf, NULL); if (n == 0 || n >= sizeof(buf)) return -1; /* Prefer forward slashes internally for sandbox compares. */ os_path_norm_slashes(buf); if (strlen(buf) + 1 > out_sz) return -1; memcpy(out, buf, strlen(buf) + 1); /* Lowercase drive letter for stable compares */ if (out[0] && out[1] == ':') { if (out[0] >= 'A' && out[0] <= 'Z') out[0] = (char)(out[0] - 'A' + 'a'); } return 0; #else char *r = realpath(path, NULL); if (!r) return -1; if (strlen(r) + 1 > out_sz) { free(r); return -1; } memcpy(out, r, strlen(r) + 1); free(r); return 0; #endif } int os_path_under(const char *root, const char *path) { if (!root || !path) return 0; size_t rl = strlen(root); if (strncmp(path, root, rl) != 0) return 0; if (path[rl] == '\0') return 1; #ifdef _WIN32 return path[rl] == '/' || path[rl] == '\\'; #else return path[rl] == '/'; #endif } int os_mkdir(const char *path, int mode) { if (!path) return -1; #ifdef _WIN32 (void)mode; if (_mkdir(path) != 0 && errno != EEXIST) return -1; return 0; #else if (mkdir(path, (mode_t)mode) != 0 && errno != EEXIST) return -1; return 0; #endif } int os_mkdir_p(const char *path, int mode) { if (!path || !path[0]) return -1; char tmp[OS_PATH_MAX]; snprintf(tmp, sizeof(tmp), "%s", path); for (char *p = tmp + 1; *p; p++) { if (*p == '/' || *p == '\\') { char c = *p; *p = 0; os_mkdir(tmp, mode); *p = c; } } return os_mkdir(tmp, mode); } int os_config_dir(char *out, size_t out_sz) { if (!out || out_sz < 8) return -1; #ifdef _WIN32 const char *base = getenv("APPDATA"); if (!base || !base[0]) base = getenv("LOCALAPPDATA"); if (!base || !base[0]) return -1; int n = snprintf(out, out_sz, "%s\\cst-agent", base); #else const char *xdg = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); int n; if (xdg && xdg[0] == '/') n = snprintf(out, out_sz, "%s/cst-agent", xdg); else if (home && home[0]) n = snprintf(out, out_sz, "%s/.config/cst-agent", home); else return -1; #endif if (n < 0 || (size_t)n >= out_sz) return -1; return 0; } int os_temp_dir(char *out, size_t out_sz) { if (!out || out_sz < 2) return -1; #ifdef _WIN32 DWORD n = GetTempPathA((DWORD)out_sz, out); if (n == 0 || n >= out_sz) return -1; os_path_norm_slashes(out); size_t L = strlen(out); while (L > 0 && out[L - 1] == '/') out[--L] = 0; return 0; #else const char *t = getenv("TMPDIR"); if (!t || !t[0]) t = "/tmp"; snprintf(out, out_sz, "%s", t); return 0; #endif } int os_mkstemp(char *out, size_t out_sz, const char *prefix) { if (!out || out_sz < 16) return -1; char dir[OS_PATH_MAX]; if (os_temp_dir(dir, sizeof(dir)) != 0) return -1; if (!prefix) prefix = "cst"; #ifdef _WIN32 char name[MAX_PATH]; if (GetTempFileNameA(dir, prefix, 0, name) == 0) return -1; os_path_norm_slashes(name); if (strlen(name) + 1 > out_sz) return -1; memcpy(out, name, strlen(name) + 1); int fd = _open(out, _O_RDWR | _O_BINARY); return fd; #else int n = snprintf(out, out_sz, "%s/%s-XXXXXX", dir, prefix); if (n < 0 || (size_t)n >= out_sz) return -1; int fd = mkstemp(out); return fd; #endif } /* Optional: main() may set this for Termux / argv0 fallback when * /proc/self/exe is unavailable. */ static char g_argv0_dir[OS_PATH_MAX]; void os_set_prog_path(const char *argv0) { g_argv0_dir[0] = 0; if (!argv0 || !argv0[0]) return; char buf[OS_PATH_MAX]; snprintf(buf, sizeof(buf), "%s", argv0); os_path_norm_slashes(buf); char *slash = strrchr(buf, '/'); if (!slash) return; *slash = 0; /* Relative "./foo" → keep as-is; caller may pass absolute. */ snprintf(g_argv0_dir, sizeof(g_argv0_dir), "%s", buf); } int os_exe_dir(char *out, size_t out_sz) { if (!out || out_sz < 2) return -1; const char *env = getenv("CST_EXE_DIR"); if (env && env[0]) { snprintf(out, out_sz, "%s", env); return 0; } #ifdef _WIN32 char buf[OS_PATH_MAX]; DWORD n = GetModuleFileNameA(NULL, buf, (DWORD)sizeof(buf)); if (n == 0 || n >= sizeof(buf)) return -1; os_path_norm_slashes(buf); char *slash = strrchr(buf, '/'); if (!slash) return -1; *slash = 0; if (strlen(buf) + 1 > out_sz) return -1; memcpy(out, buf, strlen(buf) + 1); return 0; #else char buf[OS_PATH_MAX]; #ifdef __APPLE__ uint32_t asz = (uint32_t)sizeof(buf); ssize_t n = _NSGetExecutablePath(buf, &asz) == 0 ? (ssize_t)strlen(buf) : -1; #else ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); #endif if (n >= 0) { buf[n] = 0; char *slash = strrchr(buf, '/'); if (slash) { *slash = 0; if (strlen(buf) + 1 <= out_sz) { memcpy(out, buf, strlen(buf) + 1); return 0; } } } /* Termux / restricted Android: fall back to argv0 dir. */ if (g_argv0_dir[0]) { snprintf(out, out_sz, "%s", g_argv0_dir); return 0; } return -1; #endif } static int file_exists(const char *p) { #ifdef _WIN32 DWORD a = GetFileAttributesA(p); return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY); #else return access(p, X_OK) == 0 || access(p, F_OK) == 0; #endif } int os_resolve_shell(char *out, size_t out_sz) { if (!out || out_sz < 2) return -1; const char *env = getenv("CST_SHELL"); if (env && env[0]) { snprintf(out, out_sz, "%s", env); return 0; } char dir[OS_PATH_MAX]; if (os_exe_dir(dir, sizeof(dir)) == 0) { #ifdef _WIN32 char cand[OS_PATH_MAX]; if (os_path_join(cand, sizeof(cand), dir, "busybox.exe") == 0 && file_exists(cand)) { snprintf(out, out_sz, "%s", cand); return 0; } #else char cand[OS_PATH_MAX]; if (os_path_join(cand, sizeof(cand), dir, "busybox") == 0 && file_exists(cand)) { snprintf(out, out_sz, "%s", cand); return 0; } #endif } #ifdef _WIN32 snprintf(out, out_sz, "%s", "busybox.exe"); return 0; #else /* Termux: $PREFIX/bin/sh */ { const char *prefix = getenv("PREFIX"); if (prefix && prefix[0]) { char cand[OS_PATH_MAX]; if (os_path_join(cand, sizeof(cand), prefix, "bin/sh") == 0 && file_exists(cand)) { snprintf(out, out_sz, "%s", cand); return 0; } } } if (file_exists("/bin/sh")) { snprintf(out, out_sz, "%s", "/bin/sh"); return 0; } if (file_exists("/system/bin/sh")) { snprintf(out, out_sz, "%s", "/system/bin/sh"); return 0; } snprintf(out, out_sz, "%s", "sh"); return 0; #endif } static volatile int *g_run_cancel; static int g_run_timeout_s; void os_set_cancel_flag(volatile int *flag) { g_run_cancel = flag; } void os_set_run_timeout(int seconds) { g_run_timeout_s = seconds; } static int run_timeout_s(void) { if (g_run_timeout_s > 0) return g_run_timeout_s; const char *e = getenv("CST_SHELL_TIMEOUT"); if (e && atoi(e) > 0) return atoi(e); return 120; } double os_now_ms(void) { #ifdef _WIN32 static LARGE_INTEGER freq; LARGE_INTEGER now; if (!freq.QuadPart) QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&now); return (double)now.QuadPart * 1000.0 / (double)freq.QuadPart; #else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; #endif } /* Append to capture buffer, keeping at most max_out bytes. */ static int cap_append(char **buf, size_t *len, size_t *cap, size_t max_out, const char *data, size_t n, int *truncated) { if (*len >= max_out) { *truncated = 1; return 0; } if (*len + n > max_out) { n = max_out - *len; *truncated = 1; } if (*len + n + 1 > *cap) { size_t nc = *cap ? *cap * 2 : 4096; while (nc < *len + n + 1) nc *= 2; char *nb = realloc(*buf, nc); if (!nb) return -1; *buf = nb; *cap = nc; } memcpy(*buf + *len, data, n); *len += n; (*buf)[*len] = 0; return 0; } static void cap_note(char **buf, size_t *len, size_t *cap, const char *note) { size_t n = strlen(note); if (*len + n + 1 > *cap) { char *nb = realloc(*buf, *len + n + 1); if (!nb) return; *buf = nb; *cap = *len + n + 1; } memcpy(*buf + *len, note, n + 1); *len += n; } #ifdef _WIN32 int os_run_capture(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r) { if (!argv || !argv[0] || !r) return -1; if (max_out == 0) max_out = 512 * 1024; memset(r, 0, sizeof(*r)); char cmdline[8192]; size_t pos = 0; cmdline[0] = 0; for (int i = 0; argv[i]; i++) { const char *a = argv[i]; int need_q = strchr(a, ' ') || strchr(a, '\t') || strchr(a, '"'); if (i && pos + 1 < sizeof(cmdline)) cmdline[pos++] = ' '; if (need_q) { if (pos + 1 < sizeof(cmdline)) cmdline[pos++] = '"'; for (const char *p = a; *p && pos + 2 < sizeof(cmdline); p++) { if (*p == '"') { cmdline[pos++] = '\\'; cmdline[pos++] = '"'; } else { cmdline[pos++] = *p; } } if (pos + 1 < sizeof(cmdline)) cmdline[pos++] = '"'; } else { size_t L = strlen(a); if (pos + L >= sizeof(cmdline)) break; memcpy(cmdline + pos, a, L); pos += L; } cmdline[pos] = 0; } SECURITY_ATTRIBUTES sa = {0}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; HANDLE rd = NULL, wr = NULL; if (!CreatePipe(&rd, &wr, &sa, 0)) return -1; SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); STARTUPINFOA si = {0}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdOutput = wr; si.hStdError = wr; HANDLE nul = CreateFileA("NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL); si.hStdInput = nul != INVALID_HANDLE_VALUE ? nul : GetStdHandle(STD_INPUT_HANDLE); PROCESS_INFORMATION pi = {0}; char *cmd_mut = _strdup(cmdline); if (!cmd_mut) { CloseHandle(rd); CloseHandle(wr); return -1; } BOOL ok = CreateProcessA(NULL, cmd_mut, NULL, NULL, TRUE, 0, NULL, cwd && cwd[0] ? cwd : NULL, &si, &pi); free(cmd_mut); CloseHandle(wr); if (nul != INVALID_HANDLE_VALUE) CloseHandle(nul); if (!ok) { CloseHandle(rd); return -1; } size_t cap = 4096, len = 0; char *buf = malloc(cap); if (!buf) { CloseHandle(rd); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return -1; } buf[0] = 0; int truncated = 0, killed = 0; double deadline = os_now_ms() + run_timeout_s() * 1000.0; for (;;) { DWORD avail = 0; if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL)) break; /* writer closed */ if (avail > 0) { char tmp[4096]; DWORD n = 0; if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0) break; cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated); continue; } if (WaitForSingleObject(pi.hProcess, 50) == WAIT_OBJECT_0) { /* drain what is left */ while (PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL) && avail > 0) { char tmp[4096]; DWORD n = 0; if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0) break; cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated); } break; } if ((g_run_cancel && *g_run_cancel) || os_now_ms() > deadline) { killed = (g_run_cancel && *g_run_cancel) ? 2 : 1; TerminateProcess(pi.hProcess, killed == 1 ? 124 : 130); break; } } CloseHandle(rd); WaitForSingleObject(pi.hProcess, INFINITE); DWORD code = 1; GetExitCodeProcess(pi.hProcess, &code); if (truncated) cap_note(&buf, &len, &cap, "\n...(output truncated)\n"); if (killed == 1) { char note[96]; snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n", run_timeout_s()); cap_note(&buf, &len, &cap, note); code = 124; } else if (killed == 2) { cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n"); code = 130; } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); buf[len] = 0; r->out = buf; r->out_len = len; r->exit_code = (int)code; return 0; } int os_run_capture_ex(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r, os_child_hook_fn child_hook, void *ud) { (void)child_hook; (void)ud; return os_run_capture(cwd, argv, max_out, r); } #else int os_run_capture(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r) { return os_run_capture_ex(cwd, argv, max_out, r, NULL, NULL); } int os_run_capture_ex(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r, os_child_hook_fn child_hook, void *ud) { if (!argv || !argv[0] || !r) return -1; if (max_out == 0) max_out = 512 * 1024; memset(r, 0, sizeof(*r)); int pipefd[2]; if (pipe(pipefd) != 0) return -1; pid_t pid = fork(); if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return -1; } if (pid == 0) { setpgid(0, 0); close(pipefd[0]); int devnull = open("/dev/null", O_RDONLY); if (devnull >= 0) { dup2(devnull, STDIN_FILENO); close(devnull); } dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); if (cwd && cwd[0] && chdir(cwd) != 0) _exit(127); if (child_hook && child_hook(ud) != 0) { static const char msg[] = "[cst: sandbox setup failed; command not " "run]\n"; ssize_t w = write(STDERR_FILENO, msg, sizeof(msg) - 1); (void)w; _exit(126); } execvp(argv[0], (char *const *)argv); _exit(127); } setpgid(pid, pid); /* also in parent: avoid racing the child */ close(pipefd[1]); size_t cap = 4096, len = 0; char *buf = malloc(cap); if (!buf) { close(pipefd[0]); kill(-pid, SIGKILL); waitpid(pid, NULL, 0); return -1; } buf[0] = 0; int truncated = 0, killed = 0; int status = 0, reaped = 0, bg_killed = 0; double reaped_at = 0, killed_at = 0; double deadline = os_now_ms() + run_timeout_s() * 1000.0; for (;;) { struct pollfd pfd = {pipefd[0], POLLIN, 0}; int pr = poll(&pfd, 1, 100); if (pr < 0 && errno != EINTR) break; if (pr > 0) { char tmp[4096]; ssize_t n = read(pipefd[0], tmp, sizeof(tmp)); if (n == 0) break; /* every writer closed */ if (n > 0) cap_append(&buf, &len, &cap, max_out, tmp, (size_t)n, &truncated); else if (errno != EINTR && errno != EAGAIN) break; } double now = os_now_ms(); if (!reaped && waitpid(pid, &status, WNOHANG) == pid) { reaped = 1; reaped_at = now; } /* The shell exited but a background child still holds the pipe * (`server &`): give it a moment, then end the group. */ if (reaped && !killed && !bg_killed && now - reaped_at > 1000) { kill(-pid, SIGKILL); bg_killed = 1; killed_at = now; } /* Checked on every pass, so a chatty command still stops promptly. */ if (!reaped && !killed && ((g_run_cancel && *g_run_cancel) || now > deadline)) { killed = (g_run_cancel && *g_run_cancel) ? 2 : 1; killed_at = now; kill(-pid, SIGTERM); } if (killed && !reaped && now - killed_at > 1000) kill(-pid, SIGKILL); /* a writer that left the process group can keep the pipe open */ if ((killed || bg_killed) && now - killed_at > 3000) break; } close(pipefd[0]); if (pid > 0 && !reaped) { if (killed) kill(-pid, SIGKILL); waitpid(pid, &status, 0); } if (truncated) cap_note(&buf, &len, &cap, "\n...(output truncated)\n"); if (bg_killed) cap_note(&buf, &len, &cap, "\n[cst: background processes left running were stopped]\n"); r->out = buf; r->out_len = len; r->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1; if (killed == 1) { char note[96]; snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n", run_timeout_s()); cap_note(&r->out, &r->out_len, &cap, note); r->exit_code = 124; } else if (killed == 2) { cap_note(&r->out, &r->out_len, &cap, "\n[cst: cancelled]\n"); r->exit_code = 130; } return 0; } #endif int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out, os_run_result_t *r) { return os_run_shell_ex(cwd, cmd, max_out, r, NULL, NULL); } int os_run_shell_ex(const char *cwd, const char *cmd, size_t max_out, os_run_result_t *r, os_child_hook_fn child_hook, void *ud) { if (!cmd || !r) return -1; char shell[OS_PATH_MAX]; if (os_resolve_shell(shell, sizeof(shell)) != 0) return -1; /* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd */ int is_busybox = 0; { const char *base = shell; const char *s = strrchr(shell, '/'); #ifdef _WIN32 const char *s2 = strrchr(shell, '\\'); if (s2 && (!s || s2 > s)) s = s2; #endif if (s) base = s + 1; if (strncmp(base, "busybox", 7) == 0) is_busybox = 1; } if (is_busybox) { const char *argv[] = {shell, "sh", "-c", cmd, NULL}; return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud); } else { const char *argv[] = {shell, "-c", cmd, NULL}; return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud); } } void os_localtime(const time_t *t, struct tm *out) { if (!t || !out) return; #ifdef _WIN32 localtime_s(out, t); #else localtime_r(t, out); #endif } int os_getpid(void) { #ifdef _WIN32 return (int)GetCurrentProcessId(); #else return (int)getpid(); #endif } int os_chmod_private(const char *path) { if (!path) return -1; #ifdef _WIN32 (void)path; return 0; #else return chmod(path, 0600); #endif }