#ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include "cst_perms.h" #include "cst_sandbox.h" #include #include #include #define CHECK(c) \ do { \ if (!(c)) { \ fprintf(stderr, "%s:%d: CHECK(%s) failed\n", __FILE__, __LINE__, #c); \ exit(1); \ } \ } while (0) static void test_rules(void) { static const char *const SAFE[] = { "ls -la", "cat README.md | head -n 20", "git status && git diff --stat", "grep -rn TODO src || true", "find . -name '*.c' | wc -l", "rg foo src 2>&1 | head", "echo hi >/dev/null", "pwd; which cc", "git log --oneline -5", "tree -L 2", }; static const char *const UNSAFE[] = { "rm -rf build", "echo hi > out.txt", "cat a >> b", "ls $(pwd)", "ls `pwd`", "diff <(ls a) <(ls b)", "sudo ls", "find . -name x -delete", "find . -exec rm {} ;", "git push", "git commit -m x", "make", "curl https://example.com", "ls && rm x", "echo ok | sh", "", "tee out.txt", "git branch -D main", }; for (size_t i = 0; i < sizeof(SAFE) / sizeof(SAFE[0]); i++) { if (!cst_shell_is_safe(SAFE[i])) fprintf(stderr, "expected safe: %s\n", SAFE[i]); CHECK(cst_shell_is_safe(SAFE[i])); } for (size_t i = 0; i < sizeof(UNSAFE) / sizeof(UNSAFE[0]); i++) { if (cst_shell_is_safe(UNSAFE[i])) fprintf(stderr, "expected unsafe: %s\n", UNSAFE[i]); CHECK(!cst_shell_is_safe(UNSAFE[i])); } } static void test_blocked(void) { const char *a = "sh: 1: cannot create /etc/x: Permission denied\n"; CHECK(cst_sandbox_blocked(a, strlen(a), 2)); CHECK(!cst_sandbox_blocked(a, strlen(a), 0)); const char *b = "error: no such file\n"; CHECK(!cst_sandbox_blocked(b, strlen(b), 1)); const char *c = "touch: cannot touch 'x': Read-only file system\n"; CHECK(cst_sandbox_blocked(c, strlen(c), 1)); } #if !defined(_WIN32) #include #include #include #include #include #include #include static int exists(const char *p) { struct stat st; return stat(p, &st) == 0; } static int run(const char *ws, const char *cmd, os_run_result_t *r) { memset(r, 0, sizeof(*r)); CHECK(cst_sandbox_run_shell(ws, cmd, 0, r) == 0); return r->exit_code; } /* Child: enter the sandbox, then try a TCP connect to a local listener. */ static void test_network(const char *ws) { int ls = socket(AF_INET, SOCK_STREAM, 0); CHECK(ls >= 0); struct sockaddr_in a; memset(&a, 0, sizeof(a)); a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); CHECK(bind(ls, (struct sockaddr *)&a, sizeof(a)) == 0); CHECK(listen(ls, 1) == 0); socklen_t al = sizeof(a); CHECK(getsockname(ls, (struct sockaddr *)&a, &al) == 0); cst_sandbox_plan_t plan; CHECK(cst_sandbox_prepare(ws, NULL, &plan) == 0); pid_t pid = fork(); CHECK(pid >= 0); if (pid == 0) { if (cst_sandbox_enter(&plan) != 0) _exit(3); int s = socket(AF_INET, SOCK_STREAM, 0); int rc = connect(s, (struct sockaddr *)&a, sizeof(a)); _exit(rc == 0 ? 1 : errno == EACCES ? 0 : 2); } int st = 0; waitpid(pid, &st, 0); cst_sandbox_release(&plan); close(ls); CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0); } static void test_landlock(void) { char cwd[OS_PATH_MAX], ws[OS_PATH_MAX + 32], out[OS_PATH_MAX + 32]; char cmd[3 * OS_PATH_MAX]; CHECK(getcwd(cwd, sizeof(cwd)) != NULL); /* Both live in the build tree, outside TMPDIR, so "outside" is denied. */ snprintf(ws, sizeof(ws), "%s/tests/sbx_ws_XXXXXX", cwd); snprintf(out, sizeof(out), "%s/tests/sbx_out_XXXXXX", cwd); CHECK(mkdtemp(ws) && mkdtemp(out)); char outfile[OS_PATH_MAX + 64], readme[OS_PATH_MAX + 64]; snprintf(outfile, sizeof(outfile), "%s/escape.txt", out); snprintf(readme, sizeof(readme), "%s/readme.txt", out); FILE *fp = fopen(readme, "w"); CHECK(fp); fputs("outside-content\n", fp); fclose(fp); os_run_result_t r; CHECK(run(ws, "echo hi > inside.txt && mkdir sub && echo x > sub/y && " "rm sub/y && cat inside.txt", &r) == 0); CHECK(strstr(r.out, "hi") != NULL); os_run_result_free(&r); snprintf(cmd, sizeof(cmd), "echo nope > '%s'", outfile); CHECK(run(ws, cmd, &r) != 0); CHECK(!exists(outfile)); if (!cst_sandbox_blocked(r.out, r.out_len, r.exit_code)) fprintf(stderr, "unexpected output (exit %d): %s\n", r.exit_code, r.out ? r.out : ""); CHECK(cst_sandbox_blocked(r.out, r.out_len, r.exit_code)); os_run_result_free(&r); snprintf(cmd, sizeof(cmd), "cat '%s'", readme); CHECK(run(ws, cmd, &r) == 0); CHECK(strstr(r.out, "outside-content") != NULL); os_run_result_free(&r); CHECK(run(ws, "echo x >/dev/null && echo ok", &r) == 0); os_run_result_free(&r); char tmp[OS_PATH_MAX]; CHECK(os_temp_dir(tmp, sizeof(tmp)) == 0); snprintf(cmd, sizeof(cmd), "f='%s/cst_sbx_%d'; echo t > \"$f\" && rm \"$f\"", tmp, (int)getpid()); CHECK(run(ws, cmd, &r) == 0); os_run_result_free(&r); /* the parent itself stays unconfined */ fp = fopen(outfile, "w"); CHECK(fp); fclose(fp); if (cst_sandbox_landlock_abi() >= 4) test_network(ws); else printf("test_sandbox: landlock ABI %d < 4, network rule not tested\n", cst_sandbox_landlock_abi()); snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", ws, out); CHECK(system(cmd) == 0); } #endif int main(void) { test_rules(); test_blocked(); int kind = cst_sandbox_probe(); printf("test_sandbox: sandbox %s\n", cst_sandbox_name(kind)); #if !defined(_WIN32) if (kind == CST_SANDBOX_LANDLOCK) test_landlock(); #endif puts("test_sandbox: ok"); return 0; }