#ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include "cst_sandbox.h" #include #include #include #include #if defined(__linux__) && !defined(__ANDROID__) #define CST_HAVE_LANDLOCK 1 #include #include #include #include #include #elif defined(__APPLE__) #define CST_HAVE_SEATBELT 1 #include #elif defined(_WIN32) #include #include #endif #if defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__)) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(SECCOMP_RET_USER_NOTIF) && defined(SECCOMP_IOCTL_NOTIF_RECV) && \ defined(SECCOMP_FILTER_FLAG_NEW_LISTENER) #define CST_HAVE_SECCOMP 1 #endif #endif static int g_kind = -1; static int g_abi; /* --- Linux: Landlock ------------------------------------------------------ */ #ifdef CST_HAVE_LANDLOCK /* Defined here so older kernel headers still build. */ #ifndef __NR_landlock_create_ruleset #define __NR_landlock_create_ruleset 444 #endif #ifndef __NR_landlock_add_rule #define __NR_landlock_add_rule 445 #endif #ifndef __NR_landlock_restrict_self #define __NR_landlock_restrict_self 446 #endif #ifndef PR_SET_NO_NEW_PRIVS #define PR_SET_NO_NEW_PRIVS 38 #endif #define LL_CREATE_RULESET_VERSION (1U << 0) #define LL_RULE_PATH_BENEATH 1 #define LL_FS_EXECUTE (1ULL << 0) #define LL_FS_WRITE_FILE (1ULL << 1) #define LL_FS_READ_FILE (1ULL << 2) #define LL_FS_READ_DIR (1ULL << 3) #define LL_FS_REMOVE_DIR (1ULL << 4) #define LL_FS_REMOVE_FILE (1ULL << 5) #define LL_FS_MAKE_CHAR (1ULL << 6) #define LL_FS_MAKE_DIR (1ULL << 7) #define LL_FS_MAKE_REG (1ULL << 8) #define LL_FS_MAKE_SOCK (1ULL << 9) #define LL_FS_MAKE_FIFO (1ULL << 10) #define LL_FS_MAKE_BLOCK (1ULL << 11) #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; uint64_t handled_access_net; }; struct ll_path_beneath_attr { uint64_t allowed_access; int32_t parent_fd; } __attribute__((packed)); static int ll_abi(void) { long v = syscall(__NR_landlock_create_ruleset, NULL, 0, LL_CREATE_RULESET_VERSION); return v > 0 ? (int)v : 0; } /* Only write-type rights are handled, so reading and exec stay allowed * everywhere; the rules grant them back beneath the writable roots. */ static uint64_t ll_write_rights(int abi) { uint64_t r = LL_FS_WRITE_FILE | LL_FS_REMOVE_DIR | LL_FS_REMOVE_FILE | LL_FS_MAKE_CHAR | LL_FS_MAKE_DIR | LL_FS_MAKE_REG | LL_FS_MAKE_SOCK | LL_FS_MAKE_FIFO | LL_FS_MAKE_BLOCK | LL_FS_MAKE_SYM; if (abi >= 2) r |= LL_FS_REFER; if (abi >= 3) r |= LL_FS_TRUNCATE; return r; } static int ll_allow(int ruleset, const char *path, uint64_t rights, int is_dir) { int fd = open(path, O_PATH | O_CLOEXEC | (is_dir ? O_DIRECTORY : 0)); if (fd < 0) return -1; struct ll_path_beneath_attr pb; pb.allowed_access = rights; pb.parent_fd = fd; long rc = syscall(__NR_landlock_add_rule, ruleset, LL_RULE_PATH_BENEATH, &pb, 0); close(fd); return rc == 0 ? 0 : -1; } 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; int fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0); if (fd < 0) return -1; uint64_t dir_rights = attr.handled_access_fs; uint64_t dev_rights = LL_FS_WRITE_FILE | (abi >= 3 ? LL_FS_TRUNCATE : 0); if (ll_allow(fd, ws, dir_rights, 1) != 0) { close(fd); return -1; } if (tmp && tmp[0]) ll_allow(fd, tmp, dir_rights, 1); /* best effort: may not exist */ ll_allow(fd, "/dev/null", dev_rights, 0); ll_allow(fd, "/dev/tty", dev_rights, 0); p->ruleset_fd = fd; return 0; } #endif /* CST_HAVE_LANDLOCK */ /* --- macOS: Seatbelt ------------------------------------------------------ */ #ifdef CST_HAVE_SEATBELT /* Deprecated but still shipped; declared here to avoid the warning. */ extern int sandbox_init(const char *profile, uint64_t flags, char **errorbuf); extern void sandbox_free_error(char *errorbuf); static void sb_quote(char *out, size_t sz, const char *s) { size_t o = 0; for (; *s && o + 3 < sz; s++) { if (*s == '"' || *s == '\\') out[o++] = '\\'; out[o++] = *s; } out[o] = 0; } static int sb_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) { char rws[OS_PATH_MAX], rtmp[OS_PATH_MAX]; char qws[2 * OS_PATH_MAX], qtmp[2 * OS_PATH_MAX]; if (os_realpath(ws, rws, sizeof(rws)) != 0) return -1; if (!tmp || os_realpath(tmp, rtmp, sizeof(rtmp)) != 0) snprintf(rtmp, sizeof(rtmp), "/private/tmp"); sb_quote(qws, sizeof(qws), rws); sb_quote(qtmp, sizeof(qtmp), rtmp); size_t n = strlen(qws) + strlen(qtmp) + 512; p->profile = malloc(n); if (!p->profile) return -1; snprintf(p->profile, n, "(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*)", qws, qtmp); return 0; } #endif /* CST_HAVE_SEATBELT */ /* --- Linux: seccomp user-notify ------------------------------------------- */ #ifdef CST_HAVE_SECCOMP #ifndef __NR_seccomp #if defined(__x86_64__) #define __NR_seccomp 317 #else #define __NR_seccomp 277 #endif #endif #ifndef __NR_openat2 #define __NR_openat2 437 #endif #ifndef __NR_fchmodat2 #define __NR_fchmodat2 452 #endif #ifndef __NR_io_uring_setup #define __NR_io_uring_setup 425 #endif #ifndef SECCOMP_SET_MODE_FILTER #define SECCOMP_SET_MODE_FILTER 1 #endif #ifndef SECCOMP_USER_NOTIF_FLAG_CONTINUE #define SECCOMP_USER_NOTIF_FLAG_CONTINUE (1UL << 0) #endif #ifndef PR_SET_NO_NEW_PRIVS #define PR_SET_NO_NEW_PRIVS 38 #endif #ifndef AT_EMPTY_PATH #define AT_EMPTY_PATH 0x1000 #endif #if defined(__x86_64__) #define SC_ARCH AUDIT_ARCH_X86_64 #else #define SC_ARCH AUDIT_ARCH_AARCH64 #endif #define SC_WRITE_FLAGS (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC) #define SC_PROG_MAX 160 typedef struct { struct sock_filter f[SC_PROG_MAX]; unsigned short n; } sc_prog_t; typedef struct { sc_prog_t prog; int sock[2]; /* the child sends its listener fd on [1] */ int stop[2]; /* release wakes the supervisor through [1] */ pthread_t th; int started; char ws[OS_PATH_MAX]; char tmp[OS_PATH_MAX]; } sc_state_t; static void sc_emit(sc_prog_t *p, uint16_t code, uint8_t jt, uint8_t jf, uint32_t k) { if (p->n < SC_PROG_MAX) { struct sock_filter ins = {code, jt, jf, k}; p->f[p->n++] = ins; } } #define SC_LD(off) sc_emit(p, BPF_LD | BPF_W | BPF_ABS, 0, 0, (uint32_t)(off)) #define SC_RET(k) sc_emit(p, BPF_RET | BPF_K, 0, 0, (uint32_t)(k)) #define SC_JEQ(k, jt, jf) \ sc_emit(p, BPF_JMP | BPF_JEQ | BPF_K, jt, jf, (uint32_t)(k)) #define SC_JSET(k, jt, jf) \ sc_emit(p, BPF_JMP | BPF_JSET | BPF_K, jt, jf, (uint32_t)(k)) #define SC_ARG(i) (offsetof(struct seccomp_data, args) + (i) * sizeof(uint64_t)) /* Syscalls whose path arguments the supervisor always checks. */ static const int SC_NOTIFY[] = { __NR_openat2, __NR_mkdirat, __NR_mknodat, __NR_unlinkat, __NR_symlinkat, __NR_linkat, __NR_renameat2, __NR_truncate, __NR_fchmodat, __NR_fchmodat2, __NR_fchownat, __NR_utimensat, __NR_setxattr, __NR_lsetxattr, __NR_removexattr, __NR_lremovexattr, #ifdef __NR_renameat __NR_renameat, #endif #if defined(__x86_64__) __NR_creat, __NR_mkdir, __NR_mknod, __NR_rmdir, __NR_unlink, __NR_rename, __NR_link, __NR_symlink, __NR_chmod, __NR_chown, __NR_lchown, __NR_utime, __NR_utimes, __NR_futimesat, #endif }; /* Foreign ABIs are refused, IPv4/IPv6 sockets fail with EACCES, 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)); SC_JEQ(SC_ARCH, 1, 0); SC_RET(SECCOMP_RET_ERRNO | EPERM); SC_LD(offsetof(struct seccomp_data, nr)); #if defined(__x86_64__) 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); SC_LD(SC_ARG(2)); SC_JSET(SC_WRITE_FLAGS, 1, 0); SC_RET(SECCOMP_RET_ALLOW); SC_RET(SECCOMP_RET_USER_NOTIF); #if defined(__x86_64__) SC_JEQ(__NR_open, 0, 4); SC_LD(SC_ARG(1)); SC_JSET(SC_WRITE_FLAGS, 1, 0); SC_RET(SECCOMP_RET_ALLOW); SC_RET(SECCOMP_RET_USER_NOTIF); #endif size_t n = sizeof(SC_NOTIFY) / sizeof(SC_NOTIFY[0]); for (size_t i = 0; i < n; i++) SC_JEQ(SC_NOTIFY[i], (uint8_t)(n - i), 0); SC_RET(SECCOMP_RET_ALLOW); SC_RET(SECCOMP_RET_USER_NOTIF); } static int sc_send_fd(int s, int fd) { char c = 0; union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } u; struct iovec io = {&c, 1}; struct msghdr m; memset(&m, 0, sizeof(m)); memset(&u, 0, sizeof(u)); m.msg_iov = &io; m.msg_iovlen = 1; m.msg_control = u.buf; m.msg_controllen = sizeof(u.buf); struct cmsghdr *h = CMSG_FIRSTHDR(&m); h->cmsg_level = SOL_SOCKET; h->cmsg_type = SCM_RIGHTS; h->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(h), &fd, sizeof(int)); return sendmsg(s, &m, MSG_NOSIGNAL) == 1 ? 0 : -1; } static int sc_recv_fd(int s) { char c; union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } u; struct iovec io = {&c, 1}; struct msghdr m; memset(&m, 0, sizeof(m)); m.msg_iov = &io; m.msg_iovlen = 1; m.msg_control = u.buf; m.msg_controllen = sizeof(u.buf); if (recvmsg(s, &m, MSG_CMSG_CLOEXEC) != 1) return -1; struct cmsghdr *h = CMSG_FIRSTHDR(&m); if (!h || h->cmsg_level != SOL_SOCKET || h->cmsg_type != SCM_RIGHTS) return -1; int fd; memcpy(&fd, CMSG_DATA(h), sizeof(int)); return fd; } /* Reads child memory one page at a time (a range may end at a hole). */ static ssize_t sc_peek(int pid, uint64_t addr, void *out, size_t n) { struct iovec l = {out, n}, r = {(void *)(uintptr_t)addr, n}; ssize_t k = process_vm_readv(pid, &l, 1, &r, 1, 0); if (k > 0) return k; char mp[64]; snprintf(mp, sizeof(mp), "/proc/%d/mem", pid); int fd = open(mp, O_RDONLY | O_CLOEXEC); if (fd < 0) return -1; k = pread(fd, out, n, (off_t)addr); close(fd); return k; } /* Path decisions need sc_peek on the sandboxed child. Yama * kernel.yama.ptrace_scope=2 (and no CAP_SYS_PTRACE) denies that, so a * listener-only probe would report seccomp available while every write * fails with EACCES. */ static int sc_can_peek(void) { static const char want[] = "cst-peek-ok"; int sp[2]; if (pipe2(sp, O_CLOEXEC) != 0) return 0; pid_t pid = fork(); if (pid == 0) { char marker[] = "cst-peek-ok"; uint64_t addr = (uint64_t)(uintptr_t)marker; close(sp[0]); if (write(sp[1], &addr, sizeof(addr)) != (ssize_t)sizeof(addr)) _exit(1); close(sp[1]); pause(); _exit(0); } close(sp[1]); if (pid < 0) { close(sp[0]); return 0; } uint64_t addr = 0; int ok = 0; if (read(sp[0], &addr, sizeof(addr)) == (ssize_t)sizeof(addr) && addr) { char got[sizeof(want)]; if (sc_peek((int)pid, addr, got, sizeof(got)) == (ssize_t)sizeof(got) && memcmp(got, want, sizeof(got)) == 0) ok = 1; } close(sp[0]); kill(pid, SIGKILL); int st = 0; while (waitpid(pid, &st, 0) < 0 && errno == EINTR) ; return ok; } static int sc_read_mem(int pid, uint64_t addr, void *out, size_t n) { size_t got = 0; while (got < n) { uint64_t a = addr + got; size_t chunk = 4096 - (size_t)(a & 4095); if (chunk > n - got) chunk = n - got; ssize_t k = sc_peek(pid, a, (char *)out + got, chunk); if (k <= 0) return -1; got += (size_t)k; } return 0; } static int sc_read_str(int pid, uint64_t addr, char *out, size_t n) { size_t got = 0; while (got + 1 < n) { uint64_t a = addr + got; size_t chunk = 4096 - (size_t)(a & 4095); if (chunk > n - 1 - got) chunk = n - 1 - got; ssize_t k = sc_peek(pid, a, out + got, chunk); if (k <= 0) return -1; if (memchr(out + got, 0, (size_t)k)) return 0; got += (size_t)k; } return -1; } static int sc_readlink(const char *link, char *out, size_t n) { ssize_t k = readlink(link, out, n - 1); if (k < 0) return -1; out[k] = 0; return 0; } /* strict: the root itself does not count (it may not be removed). */ static int sc_under(const char *root, const char *p, int strict) { size_t n = strlen(root); if (n == 0) return 0; if (n == 1 && root[0] == '/') return p[0] == '/' && (!strict || p[1]); if (strncmp(p, root, n) != 0) return 0; if (p[n] == 0) return !strict; return p[n] == '/'; } static int sc_inside(const sc_state_t *s, const char *p, int strict) { static const char *const DEV[] = {"/dev/null", "/dev/zero", "/dev/full", "/dev/tty", "/dev/ptmx", "/dev/random", "/dev/urandom"}; if (sc_under(s->ws, p, strict) || sc_under(s->tmp, p, strict)) return 1; for (size_t i = 0; i < sizeof(DEV) / sizeof(DEV[0]); i++) if (strcmp(p, DEV[i]) == 0) return 1; return strncmp(p, "/dev/pts/", 9) == 0; } /* The file behind fd (or the cwd for AT_FDCWD). Pipes, sockets and other * anonymous files are fine; unreadable links are refused. */ static int sc_fd_ok(const sc_state_t *s, int pid, int fd, int strict) { char link[64], tgt[OS_PATH_MAX]; if (fd == AT_FDCWD) snprintf(link, sizeof(link), "/proc/%d/cwd", pid); else snprintf(link, sizeof(link), "/proc/%d/fd/%d", pid, fd); if (sc_readlink(link, tgt, sizeof(tgt)) != 0) return 0; return tgt[0] != '/' || sc_inside(s, tgt, strict); } static int sc_digits(const char **q) { const char *p = *q; while (**q >= '0' && **q <= '9') (*q)++; return *q > p; } /* /proc/N/fd/M or /proc/N/task/T/fd/M */ static int sc_is_fd_link(const char *p) { if (strncmp(p, "/proc/", 6) != 0) return 0; p += 6; if (!sc_digits(&p)) return 0; if (strncmp(p, "/task/", 6) == 0) { p += 6; if (!sc_digits(&p)) return 0; } if (strncmp(p, "/fd/", 4) != 0) return 0; p += 4; return sc_digits(&p) && *p == 0; } /* /proc/self, /dev/fd and /dev/std* name the supervisor when it resolves * them, so they are rewritten to the task first. */ static void sc_rewrite(int pid, const char *in, char *out, size_t n) { static const char *const STD[] = {"/dev/stdin", "/dev/stdout", "/dev/stderr"}; for (int i = 0; i < 3; i++) if (strcmp(in, STD[i]) == 0) { snprintf(out, n, "/proc/%d/fd/%d", pid, i); return; } if (strncmp(in, "/dev/fd/", 8) == 0) { snprintf(out, n, "/proc/%d/fd/%s", pid, in + 8); return; } static const char *const SELF[] = {"/proc/self", "/proc/thread-self"}; for (int i = 0; i < 2; i++) { size_t k = strlen(SELF[i]); if (strncmp(in, SELF[i], k) == 0 && (in[k] == 0 || in[k] == '/')) { snprintf(out, n, "/proc/%d%s", pid, in + k); return; } } snprintf(out, n, "%s", in); } /* full: absolute path as the task sees it. follow: the call follows a * symlink in the last component. */ static int sc_abs_ok(const sc_state_t *s, const char *full, int follow, int strict) { char path[OS_PATH_MAX], real[OS_PATH_MAX], loc[2 * OS_PATH_MAX + 2]; size_t flen = strlen(full); if (flen >= sizeof(path)) return 0; memcpy(path, full, flen + 1); size_t n = strlen(path); while (n > 1 && path[n - 1] == '/') path[--n] = 0; char *slash = strrchr(path, '/'); if (!slash) return 0; const char *base = slash + 1; if (!*base || strcmp(base, ".") == 0 || strcmp(base, "..") == 0) { if (!realpath(path, real)) return errno != ENAMETOOLONG; return sc_inside(s, real, strict); } if (slash == path) { real[0] = 0; } else { *slash = 0; /* a missing or unreadable parent fails the same way in the kernel */ if (!realpath(path, real)) return errno == ENOENT || errno == ENOTDIR || errno == EACCES || errno == ELOOP; if (strcmp(real, "/") == 0) real[0] = 0; } snprintf(loc, sizeof(loc), "%s/%s", real, base); if (strlen(loc) >= OS_PATH_MAX) return 0; if (sc_is_fd_link(loc)) { char tgt[OS_PATH_MAX]; if (sc_readlink(loc, tgt, sizeof(tgt)) != 0) return 0; return tgt[0] != '/' || sc_inside(s, tgt, 0); } if (follow) { struct stat st; if (lstat(loc, &st) == 0 && S_ISLNK(st.st_mode)) { if (!realpath(loc, real)) return 0; /* dangling: following it would create the target */ return sc_inside(s, real, strict); } } return sc_inside(s, loc, strict); } /* One (dirfd, path) argument pair. addr 0 (or "" with AT_EMPTY_PATH) * means the call acts on dirfd itself. */ static int sc_path_ok(const sc_state_t *s, int pid, int dirfd, uint64_t addr, int follow, int empty_ok, int strict) { char rel[OS_PATH_MAX], base[OS_PATH_MAX], full[2 * OS_PATH_MAX]; if (addr == 0) return sc_fd_ok(s, pid, dirfd, strict); if (sc_read_str(pid, addr, rel, sizeof(rel)) != 0) return 0; if (!rel[0]) return empty_ok ? sc_fd_ok(s, pid, dirfd, strict) : 1; if (rel[0] == '/') { sc_rewrite(pid, rel, full, sizeof(full)); } else { char link[64]; if (dirfd == AT_FDCWD) snprintf(link, sizeof(link), "/proc/%d/cwd", pid); else snprintf(link, sizeof(link), "/proc/%d/fd/%d", pid, dirfd); if (sc_readlink(link, base, sizeof(base)) != 0 || base[0] != '/') return 0; snprintf(full, sizeof(full), "%s/%s", base, rel); } return sc_abs_ok(s, full, follow, strict); } /* 1: let the call run, 0: fail it with EACCES. */ static int sc_decide(const sc_state_t *s, const struct seccomp_notif *req) { const uint64_t *a = (const uint64_t *)req->data.args; int pid = (int)req->pid; #define SC_P(d, p, fl, em, st) \ sc_path_ok(s, pid, (int)(d), (p), (fl), (em), (st)) switch (req->data.nr) { case __NR_openat: { int fl = (int)a[2]; return !(fl & SC_WRITE_FLAGS) || SC_P(a[0], a[1], !(fl & O_NOFOLLOW), 0, 0); } case __NR_openat2: { struct { uint64_t flags, mode, resolve; } how; memset(&how, 0, sizeof(how)); size_t sz = a[3] < sizeof(how) ? (size_t)a[3] : sizeof(how); if (sz < sizeof(uint64_t) || sc_read_mem(pid, a[2], &how, sz) != 0) return 0; if (!(how.flags & SC_WRITE_FLAGS)) return 1; return SC_P(a[0], a[1], !(how.flags & O_NOFOLLOW) && !(how.resolve & 0x04 /* NO_SYMLINKS */), 0, 0); } case __NR_mkdirat: case __NR_mknodat: return SC_P(a[0], a[1], 0, 0, 0); case __NR_unlinkat: return SC_P(a[0], a[1], 0, 0, 1); case __NR_symlinkat: return SC_P(a[1], a[2], 0, 0, 0); case __NR_linkat: return SC_P(a[0], a[1], (a[4] & AT_SYMLINK_FOLLOW) != 0, (a[4] & AT_EMPTY_PATH) != 0, 0) && SC_P(a[2], a[3], 0, 0, 0); #ifdef __NR_renameat case __NR_renameat: #endif case __NR_renameat2: return SC_P(a[0], a[1], 0, 0, 1) && SC_P(a[2], a[3], 0, 0, 1); case __NR_truncate: return SC_P(AT_FDCWD, a[0], 1, 0, 0); case __NR_fchmodat: return SC_P(a[0], a[1], 1, 0, 0); case __NR_fchmodat2: return SC_P(a[0], a[1], !(a[3] & AT_SYMLINK_NOFOLLOW), (a[3] & AT_EMPTY_PATH) != 0, 0); case __NR_fchownat: return SC_P(a[0], a[1], !(a[4] & AT_SYMLINK_NOFOLLOW), (a[4] & AT_EMPTY_PATH) != 0, 0); case __NR_utimensat: return SC_P(a[0], a[1], !(a[3] & AT_SYMLINK_NOFOLLOW), 0, 0); case __NR_setxattr: case __NR_removexattr: return SC_P(AT_FDCWD, a[0], 1, 0, 0); case __NR_lsetxattr: case __NR_lremovexattr: return SC_P(AT_FDCWD, a[0], 0, 0, 0); #if defined(__x86_64__) case __NR_open: { int fl = (int)a[1]; return !(fl & SC_WRITE_FLAGS) || SC_P(AT_FDCWD, a[0], !(fl & O_NOFOLLOW), 0, 0); } case __NR_creat: case __NR_chmod: case __NR_chown: case __NR_utime: case __NR_utimes: return SC_P(AT_FDCWD, a[0], 1, 0, 0); case __NR_mkdir: case __NR_mknod: case __NR_lchown: return SC_P(AT_FDCWD, a[0], 0, 0, 0); case __NR_rmdir: case __NR_unlink: return SC_P(AT_FDCWD, a[0], 0, 0, 1); case __NR_rename: return SC_P(AT_FDCWD, a[0], 0, 0, 1) && SC_P(AT_FDCWD, a[1], 0, 0, 1); case __NR_link: return SC_P(AT_FDCWD, a[0], 0, 0, 0) && SC_P(AT_FDCWD, a[1], 0, 0, 0); case __NR_symlink: return SC_P(AT_FDCWD, a[1], 0, 0, 0); case __NR_futimesat: return SC_P(a[0], a[1], 1, 0, 0); #endif default: return 0; } #undef SC_P } static void sc_handle(const sc_state_t *s, int lfd) { struct seccomp_notif req; struct seccomp_notif_resp resp; memset(&req, 0, sizeof(req)); if (ioctl(lfd, SECCOMP_IOCTL_NOTIF_RECV, &req) != 0) return; /* the task died meanwhile */ int allow = sc_decide(s, &req); /* the pid read above still names the task that made the call */ if (ioctl(lfd, SECCOMP_IOCTL_NOTIF_ID_VALID, &req.id) != 0) return; memset(&resp, 0, sizeof(resp)); resp.id = req.id; if (allow) resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE; else resp.error = -EACCES; ioctl(lfd, SECCOMP_IOCTL_NOTIF_SEND, &resp); } /* Waits for the listener fd, then answers until no task uses the filter * or release() asks to stop. Tasks left over after that get ENOSYS from * the kernel for checked calls. */ static void *sc_main(void *arg) { sc_state_t *s = arg; int lfd = -1; for (;;) { struct pollfd pf[2] = {{s->sock[0], POLLIN, 0}, {s->stop[0], POLLIN, 0}}; if (poll(pf, 2, -1) < 0) { if (errno == EINTR) continue; return NULL; } if (pf[1].revents) return NULL; if (pf[0].revents) { lfd = sc_recv_fd(s->sock[0]); break; } } if (lfd < 0) return NULL; for (;;) { struct pollfd pf[2] = {{lfd, POLLIN, 0}, {s->stop[0], POLLIN, 0}}; if (poll(pf, 2, -1) < 0) { if (errno == EINTR) continue; break; } if (pf[0].revents & POLLIN) { sc_handle(s, lfd); continue; } if (pf[0].revents || pf[1].revents) break; } close(lfd); return NULL; } static void sc_free(sc_state_t *s) { if (!s) return; if (s->started) { char c = 1; ssize_t w = write(s->stop[1], &c, 1); (void)w; pthread_join(s->th, NULL); } for (int i = 0; i < 2; i++) { if (s->sock[i] >= 0) close(s->sock[i]); if (s->stop[i] >= 0) close(s->stop[i]); } free(s); } static int sc_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) { sc_state_t *s = calloc(1, sizeof(*s)); if (!s) return -1; s->sock[0] = s->sock[1] = s->stop[0] = s->stop[1] = -1; if (!realpath(ws, s->ws)) goto fail; if (!tmp || !tmp[0] || !realpath(tmp, s->tmp)) s->tmp[0] = 0; sc_build(&s->prog); if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, s->sock) != 0 || pipe2(s->stop, O_CLOEXEC) != 0) goto fail; if (pthread_create(&s->th, NULL, sc_main, s) != 0) goto fail; s->started = 1; p->sc = s; return 0; fail: sc_free(s); return -1; } static int sc_install(const sc_prog_t *prog, int sock) { struct sock_fprog fp; fp.len = prog->n; fp.filter = (struct sock_filter *)prog->f; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) return -1; int lfd = (int)syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &fp); if (lfd < 0) return -1; int rc = sc_send_fd(sock, lfd); close(lfd); return rc; } /* The whole round trip in a throwaway child: install a listener filter on * getppid, answer it with CONTINUE (kernel 5.5+). A child killed by the * platform's own seccomp policy only costs that child. */ static int sc_probe(void) { sc_prog_t prog; sc_prog_t *p = &prog; p->n = 0; SC_LD(offsetof(struct seccomp_data, arch)); SC_JEQ(SC_ARCH, 1, 0); SC_RET(SECCOMP_RET_ALLOW); SC_LD(offsetof(struct seccomp_data, nr)); SC_JEQ(__NR_getppid, 0, 1); SC_RET(SECCOMP_RET_USER_NOTIF); SC_RET(SECCOMP_RET_ALLOW); int sp[2]; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp) != 0) return 0; pid_t self = getpid(); pid_t pid = fork(); if (pid == 0) { close(sp[0]); if (sc_install(&prog, sp[1]) != 0) _exit(3); _exit(syscall(__NR_getppid) == self ? 0 : 4); } close(sp[1]); if (pid < 0) { close(sp[0]); return 0; } int ok = 0; struct pollfd pf = {sp[0], POLLIN, 0}; if (poll(&pf, 1, 3000) > 0) { int lfd = sc_recv_fd(sp[0]); if (lfd >= 0) { struct pollfd pl = {lfd, POLLIN, 0}; if (poll(&pl, 1, 3000) > 0 && (pl.revents & POLLIN)) { struct seccomp_notif req; struct seccomp_notif_resp resp; memset(&req, 0, sizeof(req)); if (ioctl(lfd, SECCOMP_IOCTL_NOTIF_RECV, &req) == 0) { memset(&resp, 0, sizeof(resp)); resp.id = req.id; resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE; ok = ioctl(lfd, SECCOMP_IOCTL_NOTIF_SEND, &resp) == 0; } } close(lfd); } } close(sp[0]); if (!ok) kill(pid, SIGKILL); int st = 0; while (waitpid(pid, &st, 0) < 0 && errno == EINTR) ; return ok && WIFEXITED(st) && WEXITSTATUS(st) == 0 && sc_can_peek(); } #endif /* CST_HAVE_SECCOMP */ /* --- Windows: Low integrity level ----------------------------------------- */ #ifdef _WIN32 #define LW_LABEL "S:(ML;OICI;NW;;;LW)" #define LW_RECORD "sandbox-labels" /* Same user, no admin privileges, Low IL. No LUA_TOKEN: a deny-only * Administrators group cannot open an admin's (SSH) window station and the * child fails in DLL init; the integrity check does not need it. */ static HANDLE lw_token(void) { HANDLE self = NULL, tok = NULL; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ASSIGN_PRIMARY, &self)) return NULL; BOOL ok = CreateRestrictedToken(self, DISABLE_MAX_PRIVILEGE, 0, NULL, 0, NULL, 0, NULL, &tok); CloseHandle(self); if (!ok) return NULL; PSID low = NULL; if (!ConvertStringSidToSidW(L"S-1-16-4096", &low)) { CloseHandle(tok); return NULL; } TOKEN_MANDATORY_LABEL tml; memset(&tml, 0, sizeof(tml)); tml.Label.Sid = low; tml.Label.Attributes = SE_GROUP_INTEGRITY; ok = SetTokenInformation(tok, TokenIntegrityLevel, &tml, (DWORD)(sizeof(tml) + GetLengthSid(low))); LocalFree(low); if (!ok) { CloseHandle(tok); return NULL; } return tok; } static int lw_record_path(char *out, size_t n) { char dir[OS_PATH_MAX]; if (os_config_dir(dir, sizeof(dir)) != 0) return -1; return snprintf(out, n, "%s/%s", dir, LW_RECORD) < (int)n ? 0 : -1; } static char *lw_read_record(void) { char path[OS_PATH_MAX]; if (lw_record_path(path, sizeof(path)) != 0) return NULL; FILE *f = fopen(path, "rb"); if (!f) return NULL; size_t cap = 4096, len = 0; char *s = malloc(cap); size_t k; while (s && (k = fread(s + len, 1, cap - len - 1, f)) > 0) { len += k; if (cap - len < 2) { char *g = realloc(s, cap * 2); if (!g) { free(s); s = NULL; break; } s = g; cap *= 2; } } fclose(f); if (s) s[len] = 0; return s; } /* dir is a recorded directory or inside one (the label is inherited). */ static int lw_recorded(const char *dir) { char *s = lw_read_record(); int found = 0; for (char *line = s; line && *line && !found;) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; char *tab = strchr(line, '\t'); if (tab) *tab = 0; found = line[0] && os_path_under(line, dir); line = nl ? nl + 1 : NULL; } free(s); return found; } static int lw_record_add(const char *dir, const char *old_sddl) { char path[OS_PATH_MAX]; if (lw_record_path(path, sizeof(path)) != 0) return -1; FILE *f = fopen(path, "ab"); if (!f) return -1; fprintf(f, "%s\t%s\n", dir, old_sddl); return fclose(f) == 0 ? 0 : -1; } static int lw_same_dir(const char *a, const char *b) { return a && b && a[0] && b[0] && os_path_under(a, b) && os_path_under(b, a); } /* An environment directory in the form os_realpath gives the workspace * (long names, '/'), or NULL. */ static const char *lw_env_dir(const char *name, char *buf, size_t n) { const char *v = getenv(name); if (!v || !v[0] || os_realpath(v, buf, n) != 0) return NULL; os_path_norm_slashes(buf); return buf; } /* Labeling a whole drive, the profile or system folders would open far more * than a project to every sandboxed command. */ static int lw_refused(const char *dir) { size_t n = strlen(dir); while (n > 0 && (dir[n - 1] == '/' || dir[n - 1] == '\\')) n--; if (n <= 2) return 1; static const char *const SELF[] = {"USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP"}; static const char *const TREE[] = {"SystemRoot", "ProgramFiles", "ProgramFiles(x86)", "ProgramData"}; char buf[OS_PATH_MAX]; for (size_t i = 0; i < sizeof(SELF) / sizeof(SELF[0]); i++) if (lw_same_dir(lw_env_dir(SELF[i], buf, sizeof(buf)), dir)) return 1; for (size_t i = 0; i < sizeof(TREE) / sizeof(TREE[0]); i++) { const char *v = lw_env_dir(TREE[i], buf, sizeof(buf)); if (v && os_path_under(v, dir)) return 1; } return 0; } static int lw_prepare(const char *ws, cst_sandbox_plan_t *p) { static int creds_done, tmp_done; char buf[OS_PATH_MAX + 256]; if (!ws || !ws[0]) return -1; if (!creds_done) { /* files saved by older versions have no no-read-up label yet */ char dir[OS_PATH_MAX]; if (os_config_dir(dir, sizeof(dir)) == 0 && snprintf(buf, sizeof(buf), "%s/credentials", dir) < (int)sizeof(buf)) { os_stat_t st; if (os_lstat(buf, &st) != 0 || os_chmod_private(buf) == 0) creds_done = 1; } } char tmp[OS_PATH_MAX]; if (os_temp_dir(tmp, sizeof(tmp)) != 0 || snprintf(buf, sizeof(buf), "%s/cst-agent-sandbox", tmp) >= (int)sizeof(tmp)) return -1; memcpy(tmp, buf, strlen(buf) + 1); if (!tmp_done) { os_mkdir_p(tmp, 0700); if (os_win_set_label(tmp, LW_LABEL) != 0) return -1; tmp_done = 1; } if (!lw_recorded(ws)) { char old[512]; if (lw_refused(ws) || os_win_get_label(ws, old, sizeof(old)) != 0 || os_win_set_label(ws, LW_LABEL) != 0) return -1; lw_record_add(ws, old); snprintf(buf, sizeof(buf), "[cst: sandbox: labeled %s Low integrity so sandboxed commands " "can write there; /sandbox reset-labels undoes it]\n", ws); p->note = strdup(buf); } HANDLE tok = lw_token(); if (!tok) { free(p->note); p->note = NULL; return -1; } p->token = tok; for (char *c = tmp; *c; c++) if (*c == '/') *c = '\\'; p->tmpdir = strdup(tmp); return p->tmpdir ? 0 : -1; } static int lw_run(const char *ws, const char *cmd, size_t max_out, const cst_sandbox_plan_t *p, os_run_result_t *r) { size_t tl = strlen(p->tmpdir) + 8; char *te = malloc(tl), *tm = malloc(tl); int rc = -1; if (te && tm) { snprintf(te, tl, "TEMP=%s", p->tmpdir); snprintf(tm, tl, "TMP=%s", p->tmpdir); const char *env[] = {te, tm, NULL}; rc = os_run_shell_token(ws, cmd, max_out, r, p->token, env); } free(te); free(tm); if (rc == 0 && p->note) { size_t nl = strlen(p->note); char *o = malloc(nl + r->out_len + 1); if (o) { memcpy(o, p->note, nl); if (r->out) memcpy(o + nl, r->out, r->out_len); o[nl + r->out_len] = 0; free(r->out); r->out = o; r->out_len += nl; } } return rc; } #endif /* _WIN32 */ char *cst_sandbox_labels(void) { #ifdef _WIN32 char *s = lw_read_record(); if (!s) return NULL; size_t o = 0; for (char *line = s; *line;) { char *nl = strchr(line, '\n'); size_t len = nl ? (size_t)(nl - line) : strlen(line); size_t path = strcspn(line, "\t\n"); if (path) { memmove(s + o, line, path); o += path; s[o++] = '\n'; } line += len + (nl ? 1 : 0); } s[o] = 0; if (!o) { free(s); return NULL; } return s; #else return NULL; #endif } int cst_sandbox_reset_labels(char **report) { if (report) *report = NULL; #ifdef _WIN32 char *s = lw_read_record(); size_t cap = 256, len = 0; char *rep = malloc(cap), *keep = NULL; size_t kcap = 0, klen = 0; int ok = 0, bad = 0; if (rep) rep[0] = 0; for (char *line = s; line && *line;) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; char *tab = strchr(line, '\t'); const char *old = tab ? tab + 1 : "S:"; if (tab) *tab = 0; if (line[0]) { os_stat_t st; int gone = os_lstat(line, &st) != 0; int done = gone || os_win_set_label(line, old[0] ? old : "S:") == 0; char msg[OS_PATH_MAX + 64]; int m = snprintf(msg, sizeof(msg), "%s %s\n", gone ? "gone " : done ? "restored" : "FAILED ", line); if (m > 0 && rep) { while (len + (size_t)m + 1 > cap) { char *g = realloc(rep, cap *= 2); if (!g) break; rep = g; } if (len + (size_t)m + 1 <= cap) { memcpy(rep + len, msg, (size_t)m + 1); len += (size_t)m; } } if (done) { ok++; } else { bad++; size_t need = strlen(line) + strlen(old) + 3; if (klen + need + 1 > kcap) { char *g = realloc(keep, kcap = (klen + need + 1) * 2); if (g) keep = g; } if (keep && klen + need + 1 <= kcap) klen += (size_t)snprintf(keep + klen, kcap - klen, "%s\t%s\n", line, old); } } line = nl ? nl + 1 : NULL; } free(s); char path[OS_PATH_MAX]; if (lw_record_path(path, sizeof(path)) == 0) { if (klen) { FILE *f = fopen(path, "wb"); if (f) { fwrite(keep, 1, klen, f); fclose(f); } } else { remove(path); } } free(keep); if (report) *report = rep; else free(rep); return bad ? -1 : ok; #else return 0; #endif } /* --- common --------------------------------------------------------------- */ int cst_sandbox_probe(void) { if (g_kind >= 0) return g_kind; int kind = CST_SANDBOX_NONE; const char *e = getenv("CST_SANDBOX"); if (e && (strcmp(e, "0") == 0 || strcmp(e, "off") == 0 || strcmp(e, "none") == 0)) { g_kind = kind; return g_kind; } int want_ll = !e || strcmp(e, "seccomp") != 0; int want_sc = !e || strcmp(e, "landlock") != 0; #ifdef CST_HAVE_LANDLOCK if (want_ll) { g_abi = ll_abi(); if (g_abi > 0) kind = CST_SANDBOX_LANDLOCK; } #endif #ifdef CST_HAVE_SECCOMP if (kind == CST_SANDBOX_NONE && want_sc && sc_probe()) kind = CST_SANDBOX_SECCOMP; #endif #ifdef CST_HAVE_SEATBELT kind = CST_SANDBOX_SEATBELT; #endif #ifdef _WIN32 if (!e || strcmp(e, "lowil") == 0) { HANDLE tok = lw_token(); if (tok) { CloseHandle(tok); kind = CST_SANDBOX_LOWIL; } } #endif (void)want_ll; (void)want_sc; g_kind = kind; return g_kind; } int cst_sandbox_reprobe(void) { g_kind = -1; g_abi = 0; return cst_sandbox_probe(); } int cst_sandbox_landlock_abi(void) { cst_sandbox_probe(); return g_abi; } const char *cst_sandbox_name(int kind) { switch (kind) { case CST_SANDBOX_LANDLOCK: return "landlock"; case CST_SANDBOX_SEATBELT: return "seatbelt"; case CST_SANDBOX_SECCOMP: return "seccomp"; case CST_SANDBOX_LOWIL: return "lowil"; default: return "none"; } } int cst_sandbox_prepare(const char *workspace, const char *tmpdir, cst_sandbox_plan_t *plan) { memset(plan, 0, sizeof(*plan)); plan->ruleset_fd = -1; int kind = cst_sandbox_probe(); int rc = -1; #ifdef CST_HAVE_LANDLOCK if (kind == CST_SANDBOX_LANDLOCK) rc = ll_prepare(workspace, tmpdir, plan); #endif #ifdef CST_HAVE_SEATBELT if (kind == CST_SANDBOX_SEATBELT) rc = sb_prepare(workspace, tmpdir, plan); #endif #ifdef CST_HAVE_SECCOMP if (kind == CST_SANDBOX_SECCOMP) rc = sc_prepare(workspace, tmpdir, plan); #endif #ifdef _WIN32 if (kind == CST_SANDBOX_LOWIL && (rc = lw_prepare(workspace, plan)) != 0) cst_sandbox_release(plan); #endif (void)workspace; (void)tmpdir; plan->kind = rc == 0 ? kind : CST_SANDBOX_NONE; return rc; } int cst_sandbox_enter(void *arg) { const cst_sandbox_plan_t *p = arg; if (!p) return -1; #ifdef CST_HAVE_LANDLOCK if (p->kind == CST_SANDBOX_LANDLOCK) { if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) return -1; return syscall(__NR_landlock_restrict_self, p->ruleset_fd, 0) == 0 ? 0 : -1; } #endif #ifdef CST_HAVE_SEATBELT if (p->kind == CST_SANDBOX_SEATBELT) { char *err = NULL; if (sandbox_init(p->profile, 0, &err) != 0) { if (err) sandbox_free_error(err); return -1; } return 0; } #endif #ifdef CST_HAVE_SECCOMP if (p->kind == CST_SANDBOX_SECCOMP && p->sc) { const sc_state_t *s = p->sc; return sc_install(&s->prog, s->sock[1]); } #endif return -1; } void cst_sandbox_release(cst_sandbox_plan_t *plan) { if (!plan) return; #ifdef CST_HAVE_LANDLOCK if (plan->ruleset_fd >= 0) close(plan->ruleset_fd); #endif #ifdef CST_HAVE_SECCOMP sc_free(plan->sc); #endif #ifdef _WIN32 if (plan->token) CloseHandle(plan->token); #endif plan->token = NULL; free(plan->tmpdir); plan->tmpdir = NULL; free(plan->note); plan->note = NULL; plan->sc = NULL; free(plan->profile); plan->profile = NULL; plan->ruleset_fd = -1; plan->kind = CST_SANDBOX_NONE; } int cst_sandbox_run_shell(const char *workspace, const char *cmd, size_t max_out, os_run_result_t *r) { char tmp[OS_PATH_MAX]; if (os_temp_dir(tmp, sizeof(tmp)) != 0) tmp[0] = 0; cst_sandbox_plan_t plan; if (cst_sandbox_prepare(workspace, tmp, &plan) != 0) { memset(r, 0, sizeof(*r)); const char *msg = "[cst: sandbox unavailable; command not run]\n"; #ifdef _WIN32 if (workspace && !lw_recorded(workspace) && lw_refused(workspace)) msg = "[cst: sandbox: will not label a drive root, the profile or a " "system folder writable; command not run. Start cst-agent in a " "project folder]\n"; #endif r->out = malloc(strlen(msg) + 1); if (r->out) { memcpy(r->out, msg, strlen(msg) + 1); r->out_len = strlen(msg); } r->exit_code = 126; return 0; } #ifdef _WIN32 int rc = lw_run(workspace, cmd, max_out, &plan, r); #else int rc = os_run_shell_ex(workspace, cmd, max_out, r, cst_sandbox_enter, &plan); #endif cst_sandbox_release(&plan); return rc; } static int has(const char *out, size_t n, const char *needle) { size_t k = strlen(needle); for (size_t i = 0; i + k <= n; i++) if (memcmp(out + i, needle, k) == 0) return 1; return 0; } int cst_sandbox_blocked(const char *out, size_t n, int exit_code) { if (exit_code == 0 || !out) return 0; 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", /* localized strerror(EACCES/EPERM/EROFS) */ "权限不够", "不允许的操作", "只读文件系统", "權限不足", "拒絕不符權限的操作", "許可がありません", "Keine Berechtigung", "Permission non accord", "Permiso denegado", "Отказано в доступе", /* Windows ERROR_ACCESS_DENIED from cmd, PowerShell and Win32 tools */ "Access is denied", "Access to the path", "UnauthorizedAccess", "拒绝访问", "存取被拒", "Zugriff verweigert", "Accès refusé", /* Cygwin/MSYS2 programs cannot initialize at Low IL */ "DLL initialization routine failed", "0xc0000142", }; for (size_t i = 0; i < sizeof(SIGNS) / sizeof(SIGNS[0]); i++) if (has(out, n, SIGNS[i])) return 1; return 0; }