The sanitizers job wedged the only runner by printing AddressSanitizer DEADLYSIGNAL forever (Docker without SYS_PTRACE). Concurrency now cancels in-progress jobs on a new push, and make asan is wrapped in timeout -s KILL 480 with shallow ASAN_OPTIONS. Also copy the path with memcpy after the length check so -O1 asan builds do not warn about snprintf truncation into OS_PATH_MAX.
1467 lines
41 KiB
C
1467 lines
41 KiB
C
#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
|
|
#include "cst_sandbox.h"
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#if defined(__linux__) && !defined(__ANDROID__)
|
|
#define CST_HAVE_LANDLOCK 1
|
|
#include <fcntl.h>
|
|
#include <stdint.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/syscall.h>
|
|
#include <unistd.h>
|
|
#elif defined(__APPLE__)
|
|
#define CST_HAVE_SEATBELT 1
|
|
#include <stdint.h>
|
|
#elif defined(_WIN32)
|
|
#include <windows.h>
|
|
#include <sddl.h>
|
|
#endif
|
|
|
|
#if defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__))
|
|
#include <fcntl.h>
|
|
#include <linux/audit.h>
|
|
#include <linux/filter.h>
|
|
#include <linux/seccomp.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#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;
|
|
}
|