Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b4afd772 |
@@ -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` 只用指定的那种。
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
-12
@@ -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;
|
||||
}
|
||||
|
||||
+5
-19
@@ -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",
|
||||
|
||||
+5
-10
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user