1 Commits
Author SHA1 Message Date
WHC 32b4afd772 v0.7.1: allow network inside the run_shell sandbox
ci / linux (clang) (push) Successful in 4m44s
ci / linux (gcc) (push) Successful in 5m36s
ci / musl (push) Failing after 29s
ci / windows-cross (push) Successful in 31m37s
Landlock no longer handles TCP, the Seatbelt profile drops (deny network*),
and the seccomp filter stops refusing IPv4/IPv6 sockets. Sandboxed commands
run without asking in auto mode on every backend (Low-IL no longer asks for
network commands), and connection errors are not treated as sandbox blocks.
2026-10-08 21:05:14 +08:00
8 changed files with 36 additions and 62 deletions
+4 -4
View File
@@ -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` 只用指定的那种。
+2 -3
View File
@@ -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 级别下无法初始化
+6 -5
View File
@@ -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
+7 -8
View File
@@ -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
+1 -1
View File
@@ -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)
+5 -11
View File
@@ -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 "
"\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";
"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;
}
+5 -19
View File
@@ -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",
+4 -9
View File
@@ -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());
snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", ws, out);
CHECK(system(cmd) == 0);