#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)); const char *d = "curl: (7) Failed to connect to example.com port 443 after 9 " "ms: Could not connect to server\n"; CHECK(!cst_sandbox_blocked(d, strlen(d), 7)); /* network is not sandboxed */ const char *e = "Access is denied.\r\n"; CHECK(cst_sandbox_blocked(e, strlen(e), 1)); const char *f = "拒绝访问。\r\n"; CHECK(cst_sandbox_blocked(f, strlen(f), 1)); const char *g = "sh: C:/msys64/usr/bin/ls.exe: A dynamic link library (DLL) " "initialization routine failed. Error 0xc0000142\n"; CHECK(cst_sandbox_blocked(g, strlen(g), 1)); } static void test_network_rules(void) { static const char *const NET[] = { "curl -fsSL https://example.com", "cd x && wget http://a/b", "/usr/bin/ssh host ls", "C:\\tools\\curl.exe -O x", "git push origin main", "git --no-pager fetch", "npm install", "python -m pip install requests", "go get example.com/x", "cargo install ripgrep", "Invoke-WebRequest -Uri x", }; static const char *const LOCAL[] = { "ls -la", "git status", "git commit -m push", "npm run build", "make -j8", "pip list", "echo curly", "cat curl.txt", }; for (size_t i = 0; i < sizeof(NET) / sizeof(NET[0]); i++) { if (!cst_shell_uses_network(NET[i])) fprintf(stderr, "expected network: %s\n", NET[i]); CHECK(cst_shell_uses_network(NET[i])); } for (size_t i = 0; i < sizeof(LOCAL) / sizeof(LOCAL[0]); i++) { if (cst_shell_uses_network(LOCAL[i])) fprintf(stderr, "expected local: %s\n", LOCAL[i]); CHECK(!cst_shell_uses_network(LOCAL[i])); } } #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 a TCP connect to a local listener works. */ 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); if (s < 0) _exit(errno == EACCES ? 1 : 2); int rc = connect(s, (struct sockaddr *)&a, sizeof(a)); _exit(rc == 0 ? 0 : errno == EACCES ? 1 : 2); } int st = 0; waitpid(pid, &st, 0); cst_sandbox_release(&plan); close(ls); CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0); } static int blocked(const os_run_result_t *r) { if (!cst_sandbox_blocked(r->out, r->out_len, r->exit_code)) { fprintf(stderr, "expected a sandbox refusal (exit %d): %s\n", r->exit_code, r->out ? r->out : ""); return 0; } return 1; } static char *slurp(const char *path) { FILE *fp = fopen(path, "r"); if (!fp) return NULL; static char buf[256]; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); buf[n] = 0; fclose(fp); return buf; } /* Path tricks the seccomp supervisor has to resolve itself. */ static void test_seccomp_paths(const char *ws, const char *out) { char cmd[3 * OS_PATH_MAX], p[OS_PATH_MAX + 64], q[OS_PATH_MAX + 64]; char readme[OS_PATH_MAX + 64]; os_run_result_t r; snprintf(readme, sizeof(readme), "%s/readme.txt", out); /* ../ out of the workspace */ const char *outname = strrchr(out, '/') + 1; snprintf(cmd, sizeof(cmd), "echo x > '../%s/e1'", outname); snprintf(p, sizeof(p), "%s/e1", out); CHECK(run(ws, cmd, &r) != 0 && blocked(&r) && !exists(p)); os_run_result_free(&r); /* a symlink inside may be created, but not written through */ snprintf(cmd, sizeof(cmd), "ln -s '%s' link && echo x > link/e2", out); snprintf(p, sizeof(p), "%s/e2", out); snprintf(q, sizeof(q), "%s/link", ws); CHECK(run(ws, cmd, &r) != 0 && blocked(&r) && !exists(p)); struct stat st; CHECK(lstat(q, &st) == 0 && S_ISLNK(st.st_mode)); os_run_result_free(&r); snprintf(cmd, sizeof(cmd), "ln -s '%s' rl && echo x >> rl", readme); CHECK(run(ws, cmd, &r) != 0 && blocked(&r)); CHECK(strcmp(slurp(readme), "outside-content\n") == 0); os_run_result_free(&r); /* moving, hard-linking or chmod-ing an outside file */ snprintf(cmd, sizeof(cmd), "mv '%s' ./moved", readme); CHECK(run(ws, cmd, &r) != 0 && blocked(&r) && exists(readme)); os_run_result_free(&r); snprintf(cmd, sizeof(cmd), "ln '%s' hard", readme); snprintf(p, sizeof(p), "%s/hard", ws); CHECK(run(ws, cmd, &r) != 0 && blocked(&r) && !exists(p)); os_run_result_free(&r); snprintf(cmd, sizeof(cmd), "chmod 600 '%s'", readme); CHECK(run(ws, cmd, &r) != 0 && blocked(&r)); os_run_result_free(&r); /* the command's own stdout/stderr */ CHECK(run(ws, "echo hi > /dev/stdout && echo e > /dev/stderr && " "echo fd > /proc/self/fd/1", &r) == 0); CHECK(strstr(r.out, "hi") && strstr(r.out, "fd")); os_run_result_free(&r); /* ordinary tree work inside */ CHECK(run(ws, "mkdir -p a/b/c && touch a/b/c/f && mv a/b/c/f a/g && " "ln -s g a/s && chmod 600 a/g && rm -r a", &r) == 0); os_run_result_free(&r); /* a missing parent stays ENOENT, not a sandbox refusal */ CHECK(run(ws, "touch nodir/x", &r) != 0); CHECK(!cst_sandbox_blocked(r.out, r.out_len, r.exit_code)); os_run_result_free(&r); /* many checked writes */ double t0 = os_now_ms(); CHECK(run(ws, "i=0; while [ $i -lt 300 ]; do echo $i > f$i; " "i=$((i+1)); done; ls | grep -c '^f[0-9]'", &r) == 0); CHECK(strstr(r.out, "300") != NULL); printf("test_sandbox: 300 checked writes in %.0f ms\n", os_now_ms() - t0); os_run_result_free(&r); /* background jobs: one still holding the pipe is served (unless Yama * hides the orphaned job's memory from the supervisor), one detached * does not hang the run */ snprintf(p, sizeof(p), "%s/bg.txt", ws); CHECK(run(ws, "(sleep 0.2; echo late > bg.txt) & echo started", &r) == 0); FILE *yf = fopen("/proc/sys/kernel/yama/ptrace_scope", "r"); int yama = 0; if (yf) { if (fscanf(yf, "%d", &yama) != 1) yama = 0; fclose(yf); } CHECK(yama > 0 || exists(p)); os_run_result_free(&r); t0 = os_now_ms(); CHECK(run(ws, "(sleep 0.3; echo late > bg2.txt) >/dev/null 2>&1 &", &r) == 0); CHECK(os_now_ms() - t0 < 3000); os_run_result_free(&r); /* the workspace root itself cannot be removed */ char cwd[OS_PATH_MAX], ws2[OS_PATH_MAX + 32]; CHECK(getcwd(cwd, sizeof(cwd)) != NULL); snprintf(ws2, sizeof(ws2), "%s/tests/sbx_ws2_XXXXXX", cwd); CHECK(mkdtemp(ws2)); CHECK(run(ws2, "rmdir \"$PWD\"", &r) != 0 && blocked(&r) && exists(ws2)); os_run_result_free(&r); CHECK(rmdir(ws2) == 0); } static void test_confined(int kind) { 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); if (kind == CST_SANDBOX_SECCOMP) test_seccomp_paths(ws, out); /* the parent itself stays unconfined */ fp = fopen(outfile, "w"); CHECK(fp); fclose(fp); test_network(ws); snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", ws, out); CHECK(system(cmd) == 0); } #endif int main(void) { test_rules(); test_blocked(); test_network_rules(); int kind = cst_sandbox_probe(); printf("test_sandbox: sandbox %s\n", cst_sandbox_name(kind)); #if !defined(_WIN32) if (kind == CST_SANDBOX_LANDLOCK || kind == CST_SANDBOX_SECCOMP) test_confined(kind); if (kind != CST_SANDBOX_SECCOMP) { const char *was = getenv("CST_SANDBOX"); char saved[32] = ""; if (was) snprintf(saved, sizeof(saved), "%s", was); setenv("CST_SANDBOX", "seccomp", 1); if (cst_sandbox_reprobe() == CST_SANDBOX_SECCOMP) { printf("test_sandbox: forced seccomp\n"); test_confined(CST_SANDBOX_SECCOMP); } else { FILE *yf = fopen("/proc/sys/kernel/yama/ptrace_scope", "r"); int yama = -1; if (yf) { if (fscanf(yf, "%d", &yama) != 1) yama = -1; fclose(yf); } if (yama >= 2) printf("test_sandbox: seccomp not available here " "(yama.ptrace_scope=%d; cannot peek child memory)\n", yama); else printf("test_sandbox: seccomp not available here\n"); } if (was) setenv("CST_SANDBOX", saved, 1); else unsetenv("CST_SANDBOX"); CHECK(cst_sandbox_reprobe() == kind); } #endif puts("test_sandbox: ok"); return 0; }