diff --git a/README.md b/README.md index 8b006d4..180f9ad 100644 --- a/README.md +++ b/README.md @@ -277,16 +277,16 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 `ask` / `auto` 的选择会记在 `settings.json`;`yolo` 只在本次运行有效。headless 行为不变(全部自动)。 - **沙箱**(C 实现,`src/sandbox_exec.c`): - Linux:Landlock(内核 5.13+,直接 syscall,无需 libseccomp)。子进程只能写工作区、临时目录、`/dev/null`、`/dev/tty`; - 读和执行不受限。Landlock ABI ≥ 4(内核 6.7+)时同时禁止 TCP 连接和监听。 + 读、执行和联网不受限。 - Linux 没有 Landlock 时(Android / Termux、WSL、老发行版;内核 5.5+,x86_64 / aarch64):seccomp user-notify (界面显示 `auto · seccomp`)。子进程的写类系统调用(带写标志的 open、创建 / 删除 / 改名 / 链接 / chmod 等)先交给 - agent 里的监督线程,按真实路径判断是否在工作区、临时目录或 `/dev/null` 等之内,不在就返回 `EACCES`;IPv4/IPv6 socket 一律拒绝。 + agent 里的监督线程,按真实路径判断是否在工作区、临时目录或 `/dev/null` 等之内,不在就返回 `EACCES`。联网不受限。 读、执行和已打开文件的写入不检查。它防的是误操作而不是恶意程序:检查之后内核会再读一次路径,多线程程序可以趁机换掉。 探测还会确认能读直接子进程的内存;`ptrace_scope=2` 且无 `CAP_SYS_PTRACE` 时 seccomp 不会启用。 比 shell 活得久的后台进程:桌面发行版开着 Yama(`ptrace_scope` ≥ 1)时它们的受检写入会被拒;命令结束后一律失败。 - - macOS:`sandbox_init` profile,同样只放行工作区和临时目录写入,禁网。 + - macOS:`sandbox_init` profile,同样只放行工作区和临时目录写入,不限制联网。 - Windows 10 1903+:Low 完整性级别的受限令牌 + Job Object(界面显示 `auto · lowil`)。第一次在工作区运行时 - 给它打上 Low 标签,之后子进程只能写工作区和沙箱临时目录,读不到凭据文件;不限制联网,所以联网命令仍会询问。 + 给它打上 Low 标签,之后子进程只能写工作区和沙箱临时目录,读不到凭据文件;不限制联网。 `/sandbox` 查看、`/sandbox reset-labels` 撤销标签,详见 [docs/README-windows.md](docs/README-windows.md)。 - 更老的内核:没有沙箱,自动回落到规则判断(界面显示 `auto · rules`)。 - `CST_SANDBOX=off` 强制关闭沙箱;`CST_SANDBOX=landlock` / `seccomp` / `lowil` 只用指定的那种。 diff --git a/docs/README-windows.md b/docs/README-windows.md index 356c195..7b1067f 100644 --- a/docs/README-windows.md +++ b/docs/README-windows.md @@ -111,9 +111,8 @@ windows-x86_64/ 以及 Windows、Program Files、ProgramData 下面的目录打标签:在这些地方启动时,命令不会运行, 界面会问是否在沙箱外重跑。请在项目目录里启动。 - `/sandbox` 列出打过标签的目录;`/sandbox reset-labels` 恢复它们原来的标签。 -- 沙箱里可以读、执行用户能访问的文件(凭据文件除外),**不限制联网**。所以像联网的命令 - (`curl`、`wget`、`ssh`、`git clone/fetch/pull/push`、`npm install`、`pip install`、 - `winget install`、`Invoke-WebRequest` 等)在 `auto` 模式下仍然会弹窗确认。 +- 沙箱里可以读、执行用户能访问的文件(凭据文件除外),**不限制联网**:`curl`、`git pull`、 + `npm install` 等联网命令在 `auto` 模式下也直接在沙箱里运行。 - 命令像是被拦了(`Access is denied`、`拒绝访问`、`Permission denied` 等)时,会问 “Rerun outside the sandbox?”:`y` 在沙箱外重跑一次,`p` 保存命令前缀规则并重跑,`n` 保留结果。 - MSYS2 / Cygwin 的程序(包括 Git for Windows 自带的 `usr\bin` 工具)在 Low 级别下无法初始化 diff --git a/docs/compat.md b/docs/compat.md index bd75aca..e331012 100644 --- a/docs/compat.md +++ b/docs/compat.md @@ -42,13 +42,14 @@ the `HAVE_MACOS_RUNNER` variable) or by hand with the checklist below. In the default `auto` approval mode, `run_shell` runs inside an OS sandbox when one is available. Commands may read and execute anything but may only write beneath the workspace, the temp directory, `/dev/null` and `/dev/tty`. +The network is not restricted. | Platform | Sandbox | Notes | |---|---|---| -| Linux 5.13+ | Landlock (raw syscalls, no extra library) | ABI ≥ 4 (6.7+) also blocks TCP connect/bind | -| Linux 5.5+ without Landlock (Android / Termux, WSL, older distros), x86_64 / aarch64 | seccomp user-notify | refuses IPv4/IPv6 sockets; see below | -| macOS | `sandbox_init` profile | also denies network | -| Windows 10 1903+ | Low integrity restricted token + Job Object | writes need a Low label (given to the workspace once); network is not blocked, so network commands still ask; see `README-windows.md` | +| Linux 5.13+ | Landlock (raw syscalls, no extra library) | | +| Linux 5.5+ without Landlock (Android / Termux, WSL, older distros), x86_64 / aarch64 | seccomp user-notify | see below | +| macOS | `sandbox_init` profile | | +| Windows 10 1903+ | Low integrity restricted token + Job Object | writes need a Low label (given to the workspace once); see `README-windows.md` | | Older kernels | none | falls back to the read-only rules | The status bar and sidebar show `auto · landlock`, `auto · seccomp`, @@ -165,7 +166,7 @@ Chinese VMs as Administrator, a standard user and a Chinese user name): keep CRLF and BOM; `run_shell` output in Chinese is readable on a 936 system - [ ] `auto · lowil`: first command reports the label; `echo x > C:\outside` - asks to rerun; `curl ...` asks before running; `/sandbox reset-labels` + asks to rerun; `curl ...` runs without asking; `/sandbox reset-labels` restores the label - [ ] Esc during `sleep 30 & sleep 30` returns at once and leaves no `busybox.exe` behind diff --git a/include/cst_sandbox.h b/include/cst_sandbox.h index c104b05..ac6f668 100644 --- a/include/cst_sandbox.h +++ b/include/cst_sandbox.h @@ -2,20 +2,19 @@ #define CST_SANDBOX_H /* - * OS sandbox for run_shell: reads and exec stay allowed everywhere, writes - * are limited to the workspace, the temp dir, /dev/null and /dev/tty, and - * the network is cut where the OS can do it. - * Linux Landlock (network needs ABI 4, kernel 6.7+) + * OS sandbox for run_shell: reads, exec and the network stay allowed; + * writes are limited to the workspace, the temp dir, /dev/null and /dev/tty. + * Linux Landlock * Linux seccomp user-notify when Landlock is missing (Android/Termux, * older kernels): a supervisor thread checks the path of every - * write-type syscall and IPv4/IPv6 sockets are refused. Guards + * write-type syscall. Guards * against mistakes, not hostile code (the path is checked before * the kernel reads it again). * macOS sandbox_init (Seatbelt profile) * Windows Low integrity level: a restricted token (no admin privileges, * Low IL) can only write objects labeled Low, so the workspace and * a temp dir get that label; the credentials file is labeled - * no-read-up. The network stays open (callers ask first). + * no-read-up. * other none: callers fall back to rule-based approval */ @@ -42,7 +41,7 @@ int cst_sandbox_probe(void); int cst_sandbox_reprobe(void); /* "landlock" | "seatbelt" | "seccomp" | "lowil" | "none" */ const char *cst_sandbox_name(int kind); -/* Landlock ABI version (0 when unavailable); ≥4 also blocks TCP. */ +/* Landlock ABI version (0 when unavailable). */ int cst_sandbox_landlock_abi(void); typedef struct { @@ -69,7 +68,7 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd, size_t max_out, os_run_result_t *r); /* Output of a failed sandboxed run that looks like the sandbox got in the - * way (EACCES / EPERM / EROFS / blocked network). */ + * way (EACCES / EPERM / EROFS). */ int cst_sandbox_blocked(const char *out, size_t n, int exit_code); /* lowil: directories this user's cst-agent labeled Low (recorded in the diff --git a/include/cst_version.h b/include/cst_version.h index aded61e..0ca8f85 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -3,7 +3,7 @@ #define CST_VERSION_MAJOR 0 #define CST_VERSION_MINOR 7 -#define CST_VERSION_PATCH 0 +#define CST_VERSION_PATCH 1 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/src/agent_loop.c b/src/agent_loop.c index 67ab57a..498cca5 100644 --- a/src/agent_loop.c +++ b/src/agent_loop.c @@ -259,7 +259,7 @@ static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) { "the task requires it. Never print secrets or API keys.\n"); if (tool_on(cfg, "run_shell")) cst_buf_puts(b, "- run_shell may run in a sandbox (writes only inside the " - "workspace and temp dir, no network). If a command is " + "workspace and temp dir; the network works). If a command is " "blocked the user is asked whether to rerun it outside; " "do not try to work around the sandbox.\n"); } @@ -827,13 +827,9 @@ static void sandbox_escalate(turn_t *t, cst_tool_ctx_t *ctx, } if (ans == CST_PERM_DENY) { const char *note = - cst_sandbox_probe() == CST_SANDBOX_LOWIL - ? "\n[cst: blocked by the sandbox (writes are limited to the " - "workspace and temp dir); the user did not allow running it " - "outside]\n" - : "\n[cst: blocked by the sandbox (writes are limited to " - "the workspace and temp dir, no network); the user did " - "not allow running it outside]\n"; + "\n[cst: blocked by the sandbox (writes are limited to the " + "workspace and temp dir); the user did not allow running it " + "outside]\n"; cst_buf_t b = {0}; if (tr->out) cst_buf_append(&b, tr->out, tr->out_len); @@ -989,11 +985,9 @@ static void exec_begin(turn_t *t, pcall_t *pc, exec_t *x) { if (def->perm == CST_TP_WRITE) { ask = 0; } else if (def->perm == CST_TP_SHELL) { - int sb = cst_sandbox_probe(); - if (sb != CST_SANDBOX_NONE) { + if (cst_sandbox_probe() != CST_SANDBOX_NONE) { ctx->sandbox = 1; - /* the Low-IL sandbox does not stop network access */ - ask = sb == CST_SANDBOX_LOWIL && cmd && cst_shell_uses_network(cmd); + ask = 0; } else if (cmd && cst_shell_is_safe(cmd)) { ask = 0; } diff --git a/src/sandbox_exec.c b/src/sandbox_exec.c index 7997d6b..8cabcf8 100644 --- a/src/sandbox_exec.c +++ b/src/sandbox_exec.c @@ -86,8 +86,6 @@ static int g_abi; #define LL_FS_MAKE_SYM (1ULL << 12) #define LL_FS_REFER (1ULL << 13) /* ABI 2 */ #define LL_FS_TRUNCATE (1ULL << 14) /* ABI 3 */ -#define LL_NET_BIND_TCP (1ULL << 0) /* ABI 4 */ -#define LL_NET_CONNECT_TCP (1ULL << 1) /* ABI 4 */ struct ll_ruleset_attr { uint64_t handled_access_fs; @@ -137,9 +135,7 @@ static int ll_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) { int abi = g_abi; struct ll_ruleset_attr attr; memset(&attr, 0, sizeof(attr)); - attr.handled_access_fs = ll_write_rights(abi); - if (abi >= 4) - attr.handled_access_net = LL_NET_BIND_TCP | LL_NET_CONNECT_TCP; + attr.handled_access_fs = ll_write_rights(abi); /* network stays open */ int fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0); if (fd < 0) return -1; @@ -193,8 +189,7 @@ static int sb_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) { "(version 1)(allow default)(deny file-write*)" "(allow file-write* (subpath \"%s\") (subpath \"%s\") " "(literal \"/dev/null\") (literal \"/dev/tty\") " - "(literal \"/dev/dtracehelper\"))" - "(deny network*)", + "(literal \"/dev/dtracehelper\"))", qws, qtmp); return 0; } @@ -290,9 +285,8 @@ static const int SC_NOTIFY[] = { #endif }; -/* Foreign ABIs are refused, IPv4/IPv6 sockets fail with EACCES, opens - * without write flags pass untouched, write-type calls go to the - * supervisor. */ +/* Foreign ABIs are refused, opens without write flags pass untouched, + * write-type calls go to the supervisor. */ static void sc_build(sc_prog_t *p) { p->n = 0; SC_LD(offsetof(struct seccomp_data, arch)); @@ -303,12 +297,6 @@ static void sc_build(sc_prog_t *p) { sc_emit(p, BPF_JMP | BPF_JGE | BPF_K, 0, 1, 0x40000000U); /* x32 */ SC_RET(SECCOMP_RET_ERRNO | EPERM); #endif - SC_JEQ(__NR_socket, 0, 5); - SC_LD(SC_ARG(0)); - SC_JEQ(AF_INET, 2, 0); - SC_JEQ(AF_INET6, 1, 0); - SC_RET(SECCOMP_RET_ALLOW); - SC_RET(SECCOMP_RET_ERRNO | EACCES); SC_JEQ(__NR_io_uring_setup, 0, 1); SC_RET(SECCOMP_RET_ERRNO | EPERM); SC_JEQ(__NR_openat, 0, 4); @@ -1446,9 +1434,7 @@ int cst_sandbox_blocked(const char *out, size_t n, int exit_code) { static const char *const SIGNS[] = { "Permission denied", "Read-only file system", "Operation not permitted", "EACCES", "EPERM", "EROFS", - "[cst: sandbox", "Network is unreachable", - /* curl when the socket itself was refused (seccomp) */ - "Could not connect to server", "Failed to connect to", + "[cst: sandbox", /* localized strerror(EACCES/EPERM/EROFS) */ "权限不够", "不允许的操作", "只读文件系统", "權限不足", "拒絕不符權限的操作", "許可がありません", "Keine Berechtigung", diff --git a/tests/test_sandbox.c b/tests/test_sandbox.c index fd577bf..9e56578 100644 --- a/tests/test_sandbox.c +++ b/tests/test_sandbox.c @@ -72,7 +72,7 @@ static void test_blocked(void) { 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)); + 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"; @@ -132,7 +132,7 @@ static int run(const char *ws, const char *cmd, os_run_result_t *r) { return r->exit_code; } -/* Child: enter the sandbox, then try a TCP connect to a local listener. */ +/* 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); @@ -152,12 +152,11 @@ static void test_network(const char *ws) { if (pid == 0) { if (cst_sandbox_enter(&plan) != 0) _exit(3); - /* Landlock refuses the connect, seccomp already the socket */ int s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) - _exit(errno == EACCES ? 0 : 2); + _exit(errno == EACCES ? 1 : 2); int rc = connect(s, (struct sockaddr *)&a, sizeof(a)); - _exit(rc == 0 ? 1 : errno == EACCES ? 0 : 2); + _exit(rc == 0 ? 0 : errno == EACCES ? 1 : 2); } int st = 0; waitpid(pid, &st, 0); @@ -337,11 +336,7 @@ static void test_confined(int kind) { CHECK(fp); fclose(fp); - if (kind == CST_SANDBOX_SECCOMP || 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()); + test_network(ws); snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", ws, out); CHECK(system(cmd) == 0);