diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chaseccomp/.gitignore | 6 | ||||
-rw-r--r-- | lib/chaseccomp/Makefile | 22 | ||||
-rw-r--r-- | lib/chaseccomp/README.md | 41 | ||||
-rw-r--r-- | lib/chaseccomp/buffer.chasc | 39 | ||||
-rw-r--r-- | lib/chaseccomp/chaseccomp.c | 48 | ||||
-rw-r--r-- | lib/chaseccomp/chaseccomp.h | 70 | ||||
-rw-r--r-- | lib/chaseccomp/common.chasc | 74 | ||||
-rwxr-xr-x | lib/chaseccomp/gen_defs | 78 | ||||
-rwxr-xr-x | lib/chaseccomp/gen_syscalls | 88 | ||||
-rw-r--r-- | lib/chaseccomp/network.chasc | 21 |
10 files changed, 487 insertions, 0 deletions
diff --git a/lib/chaseccomp/.gitignore b/lib/chaseccomp/.gitignore new file mode 100644 index 00000000..1a123c0e --- /dev/null +++ b/lib/chaseccomp/.gitignore @@ -0,0 +1,6 @@ +/chasc_*.h +/chasc_*.c +/*.expanded +/chaseccomp +*.o +*.d diff --git a/lib/chaseccomp/Makefile b/lib/chaseccomp/Makefile new file mode 100644 index 00000000..9b17b38c --- /dev/null +++ b/lib/chaseccomp/Makefile @@ -0,0 +1,22 @@ +CFLAGS += -Wall + +all: chaseccomp.o + +chasc_defs_%.c: %.chasc common.chasc gen_defs chaseccomp.h + ./gen_defs <$< >$@ + +chasc_defs_%: chasc_defs_%.c + $(CC) $< -o $@ + +%.chasc.expanded: chasc_defs_% + ./$< >$@ + +chasc_%.h: %.chasc.expanded gen_syscalls + ./gen_syscalls $< >$@ + +chaseccomp.o: chaseccomp.c chaseccomp.h chasc_network.h chasc_buffer.h + $(CC) $(CFLAGS) chaseccomp.c -c -o $@ + +clean: + rm -f *.d *.o + rm -f *.chasc.expanded chasc_*.h chasc_*.c diff --git a/lib/chaseccomp/README.md b/lib/chaseccomp/README.md new file mode 100644 index 00000000..06413b05 --- /dev/null +++ b/lib/chaseccomp/README.md @@ -0,0 +1,41 @@ +# chaseccomp + +chaseccomp is a simple BPF assembler for interfacing with seccomp. + +Input files look something like: + +``` +# check if the arch is correct +load arch +ifne CHASECCOMP_AUDIT_ARCH_NR deny +# load syscall number +load nr +# check if it's exit_group +ifeq SYS_exit_group allow +# if not, die +ret kill +: allow +# else, allow +ret allow +``` + +It has labels, conditional jumps, and support for C identifiers. In +fact, C identifiers are just passed on to the C compiler. + +It also has an "ifeqdef" statement, which wraps each "ifeq" in an +ifdef. This lets us use the same filters for all platforms - if it +doesn't support a syscall, its "allow" rule just doesn't get compiled +in. + +Ideally, the filter should be constructed by sorting the syscalls in +order of usage frequency and then checking each syscall with ifeqdef. + +The assembler runs in three steps: + +* gen_defs generates a C file from $<.chasc (and any chasc file it + includes) +* The C file is compiled and executed, thereby disabling filters for + syscalls that do not apply to this platform. The output is in + $<.chasc.expanded. +* gen_syscalls takes $<.chasc.expanded and outputs chasc_$<.h. + This is the final header file we include in the actual program. diff --git a/lib/chaseccomp/buffer.chasc b/lib/chaseccomp/buffer.chasc new file mode 100644 index 00000000..0a4aa555 --- /dev/null +++ b/lib/chaseccomp/buffer.chasc @@ -0,0 +1,39 @@ +include common.chasc + +# syscall nr is loaded in common.chasc + +# for sendfd/recvfd +ifeqdef SYS_recvmsg allow +ifeqdef SYS_sendmsg allow + +# accept socket(2), but only with AF_UNIX +ifne SYS_socket not_socket +# load domain +load args[0] +ifeq AF_UNIX allow +load nr +: not_socket + +# following syscalls are rarely called +ifeqdef SYS_accept allow # for accepting requests from pager +ifeqdef SYS_accept4 allow # for when accept is implemented as accept4 +ifeqdef SYS_bind allow # for outgoing requests to loader +ifeqdef SYS_clock_gettime allow # used by QuickJS in atomics and cpuTime() +ifeqdef SYS_clock_gettime64 allow # 64-bit clock_gettime on 32-bit platforms +ifeqdef SYS_clone allow # for when fork is implemented as clone +ifeqdef SYS_connect allow # for outgoing requests to loader +ifeqdef SYS_fork allow # for when fork is really fork +ifeqdef SYS_getpid allow # for determining current PID after we fork +ifeqdef SYS_gettimeofday allow # used by QuickJS in Date.now() +ifeqdef SYS_pipe allow # for pipes to child process +ifeqdef SYS_pipe2 allow # for when pipe is implemented as pipe2 +ifeqdef SYS_rt_sigreturn allow # newer kernels have this instead of sigreturn +ifeqdef SYS_set_robust_list allow # glibc seems to need it for whatever reason +ifeqdef SYS_sigreturn allow # called by signal trampoline + +: deny +ret trap +: eperm +ret errno EPERM +: allow +ret allow diff --git a/lib/chaseccomp/chaseccomp.c b/lib/chaseccomp/chaseccomp.c new file mode 100644 index 00000000..9001b973 --- /dev/null +++ b/lib/chaseccomp/chaseccomp.c @@ -0,0 +1,48 @@ +/* + * ref. seccomp(2) + * also bpf(4), except I can't find it on Linux... check a BSD. + */ + +#include <stdlib.h> +#include <stddef.h> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <string.h> +#include <stdio.h> +#include <sys/mman.h> +#include <errno.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <fcntl.h> +#include <stdint.h> + +#include "chaseccomp.h" + +int cha_enter_buffer_sandbox(void) +{ + struct sock_filter filter[] = { +#include "chasc_buffer.h" + }; + struct sock_fprog prog = { .len = COUNTOF(filter), .filter = filter }; + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) + return 0; + if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog)) + return 0; + return 1; +} + +int cha_enter_network_sandbox(void) +{ + struct sock_filter filter[] = { +#include "chasc_network.h" + }; + struct sock_fprog prog = { .len = COUNTOF(filter), .filter = filter }; + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) + return 0; + if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog)) + return 0; + return 1; +} diff --git a/lib/chaseccomp/chaseccomp.h b/lib/chaseccomp/chaseccomp.h new file mode 100644 index 00000000..69d5aa43 --- /dev/null +++ b/lib/chaseccomp/chaseccomp.h @@ -0,0 +1,70 @@ +#include <stdint.h> + +/* + * seccomp + */ +#define SECCOMP_SET_MODE_FILTER 1 + +#define SECCOMP_RET_KILL_PROCESS 0x80000000u +#define SECCOMP_RET_ALLOW 0x7FFF0000u +#define SECCOMP_RET_TRAP 0x00030000u +#define SECCOMP_RET_ERRNO 0x00050000u +#define SECCOMP_RET_DATA 0x0000FFFFu + +struct seccomp_data { + int nr; + uint32_t arch; + uint64_t instruction_pointer; + uint64_t args[6]; +}; + +/* + * BPF + */ + +/* instruction classes */ +#define BPF_LD 0x00 +#define BPF_JMP 0x05 +#define BPF_RET 0x06 + +/* ld/ldx fields */ +#define BPF_ABS 0x20 +#define BPF_W 0x00 + +/* alu/jmp fields */ +#define BPF_JEQ 0x10 +#define BPF_JGT 0x20 + +#define BPF_K 0x00 + +struct sock_filter { + uint16_t code; + uint8_t jt; + uint8_t jf; + uint32_t k; +}; + +struct sock_fprog { + unsigned short len; + struct sock_filter *filter; +}; + +#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k } +#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k } + +/* + * chaseccomp stuff + */ + +#define COUNTOF(x) (sizeof(x) / sizeof(*(x))) + +#define CHA_BPF_LOAD(field) \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \ + (offsetof(struct seccomp_data, field))) + +/* Note: we always operate in BPF_K source mode, which equals 0. */ + +#define CHA_BPF_RET(val) BPF_STMT(BPF_RET | BPF_K, val) +#define CHA_BPF_JE(data, n) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, data, n, 0) +#define CHA_BPF_JNE(data, m) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, data, 0, m) +#define CHA_BPF_JLE(data, m) BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, data, 0, m) diff --git a/lib/chaseccomp/common.chasc b/lib/chaseccomp/common.chasc new file mode 100644 index 00000000..37f886dc --- /dev/null +++ b/lib/chaseccomp/common.chasc @@ -0,0 +1,74 @@ +load arch +ifne CHASECCOMP_AUDIT_ARCH_NR deny + +# load syscall nr +load nr + +# socket/file i/o +ifeqdef SYS_read allow +ifeqdef SYS_write allow + +# polling +ifeqdef SYS_poll allow +ifeqdef SYS_ppoll allow # poll is sometimes implemented as ppoll, e.g. musl + +# memory allocation +ifeqdef SYS_mmap allow +ifeqdef SYS_mmap2 allow +ifeqdef SYS_mremap allow +ifeqdef SYS_munmap allow +ifeqdef SYS_brk allow + +# less common socket/file i/o +ifeqdef SYS_lseek allow +ifeqdef SYS_close allow + +# prevent glibc from getting our process murdered +ifeqdef SYS_fstat eperm +ifeqdef SYS_fstat64 eperm +ifeqdef SYS_fstatat64 eperm +ifeqdef SYS_newfstatat eperm +ifeqdef SYS_statx eperm + +# accept fcntl(2), but only with F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_SETFL. +# (F_SETFL is 4, others are 0..3) +ifeqdef SYS_fcntl64 is_fcntl64 +ifne SYS_fcntl not_fcntl +# additional test for fcntl64 on 32-bit systems +: is_fcntl64 +load args[1] +ifle F_SETFL allow +load nr +: not_fcntl + +# following syscalls are rarely called +ifeqdef SYS_futex allow # bionic libc & WSL both need it +ifeqdef SYS_exit allow # for quit +ifeqdef SYS_exit_group allow # for quit +ifeqdef SYS_restart_syscall allow # for resuming poll on SIGCONT +ifeqdef SYS_getrandom allow # glibc calls it when initializing its malloc + +# bionic-specific stuff +ifdef __BIONIC__ +ifeqdef SYS_rt_sigprocmap allow +ifeqdef SYS_madvise allow #TODO can we make this less broad? + +# bionic uses prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME) for its pages. +ifne SYS_prctl not_prctl +load args[0] +ifne PR_SET_VMA deny +load args[1] +ifeq PR_SET_VMA_ANON_NAME allow +load nr +: not_prctl + +# bionic also calls mprotect(2) with PROT_READ and PROT_WRITE flags. +# Crucially, we don't want to allow PROT_EXEC. +ifne SYS_mprotect not_mprotect +define CHA_PROT_READ_OR_PROT_WRITE (PROT_READ | PROT_WRITE) +load args[2] +ifne CHA_PROT_READ_OR_PROT_WRITE allow +load nr +: not_mprotect + +endif /* __BIONIC__ */ diff --git a/lib/chaseccomp/gen_defs b/lib/chaseccomp/gen_defs new file mode 100755 index 00000000..f4932b71 --- /dev/null +++ b/lib/chaseccomp/gen_defs @@ -0,0 +1,78 @@ +#!/bin/sh + +cat <<EOF +#include <stdlib.h> +#include <stddef.h> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <signal.h> +#include <stdio.h> + +#include "chaseccomp.h" + +static void determine_audit_arch(int sig, siginfo_t *info, void *ucontext) +{ + printf("define CHASECCOMP_AUDIT_ARCH_NR %u\n", info->si_arch); +} + +#define DIE(s) do { perror(s); exit(1); } while (0) + +int main(void) +{ + struct sigaction act = { + .sa_flags = SA_SIGINFO, + .sa_sigaction = determine_audit_arch, + }; + struct sock_filter filter[] = { + CHA_BPF_LOAD(nr), /* get syscall nr */ + CHA_BPF_JNE(SYS_exit, 3), /* if syscall is _exit */ + CHA_BPF_LOAD(args[0]), /* then load arg */ + CHA_BPF_JNE(999999, 1), /* if arg1 is 999999 */ + CHA_BPF_RET(SECCOMP_RET_TRAP), /* then trap */ + CHA_BPF_RET(SECCOMP_RET_ALLOW), /* otherwise allow */ + }; + struct sock_fprog prog = { .len = COUNTOF(filter), .filter = filter }; + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) + DIE("prctl"); + + if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog)) + DIE("seccomp"); + + sigaction(SIGSYS, &act, NULL); + syscall(SYS_exit, 999999); +EOF + +f() { + while read -r line + do case $line in + 'include '*) <"${line#* }" f ;; + *) printf '%s\n' "$line" ;; + esac + done +} + +f | while read -r line +do line=${line%%#*} + case $line in + '') ;; + if??def' '*) + inst=${line%%def*} + line=${line#* } + val=${line%% *} + label=${line#* } + printf '#ifdef %s\n' "$val" + printf '\tprintf("%s %s %s\\n");\n' "$inst" "$val" "$label" + printf '#endif\n' + ;; + ifdef*|endif*) + printf '#%s\n' "$line" ;; + *) printf '\tprintf("%s\\n");\n' "$line" ;; + esac +done + +cat <<EOF + exit(0); +} +EOF diff --git a/lib/chaseccomp/gen_syscalls b/lib/chaseccomp/gen_syscalls new file mode 100755 index 00000000..a4d7d5b5 --- /dev/null +++ b/lib/chaseccomp/gen_syscalls @@ -0,0 +1,88 @@ +#!/bin/sh + +die() { + echo "$*" >&2 + exit 1 +} + +test "$1" || die "usage: gen_syscalls [file]" + +find_label() { + printf '%s\n' "$labels" | while read -r line + do case $line in + "$1 "*) printf '%d\n' "${line#* }" + break + ;; + esac + done +} + +line_cut_next() { + next=${line%% *} + oline=$line + line=${line#* } + if test "$line" = "$oline"; then line= ; fi +} + +cut_label() { + line_cut_next + label=$(find_label "$next") + test "$label" || die "missing label $next" + label=$(($label - 1 - $ip)) +} + +put_cmp() { + line_cut_next + val=$next + cut_label + printf '\t%s(%s, %d),\n' "$inst" "$val" "$label" +} + +put_load() { + line_cut_next + printf '\t%s(%s),\n' "$inst" "$next" +} + +put_ret() { + line_cut_next + case $next in + allow) val=SECCOMP_RET_ALLOW ;; + trap) val=SECCOMP_RET_TRAP ;; + kill) val=SECCOMP_RET_KILL_PROCESS ;; + errno) val="SECCOMP_RET_ERRNO | ($line & SECCOMP_RET_DATA)" ;; + *) die "wrong retval $line" ;; + esac + printf '\t%s(%s),\n' "$inst" "$val" +} + +ip=0 +while read -r line +do line=${line%%#*} + case $line in + ''|define' '*) ;; + ': '*) line_cut_next + if test -n "$labels" + then labels="$labels +$line $ip" + else labels="$line $ip" + fi + ;; + *) ip=$((ip + 1)) ;; + esac +done < "$1" + +ip=0 +while read -r line +do line_cut_next + case $next in + :) continue ;; + define) printf '#%s %s\n' "$next $line" ; continue ;; + ret) inst=CHA_BPF_RET put_ret ;; + ifeq) inst=CHA_BPF_JE put_cmp ;; + ifne) inst=CHA_BPF_JNE put_cmp ;; + ifle) inst=CHA_BPF_JLE put_cmp ;; + load) inst=CHA_BPF_LOAD put_load ;; + *) die "unexpected instruction $inst" ;; + esac + ip=$(($ip + 1)) +done < "$1" diff --git a/lib/chaseccomp/network.chasc b/lib/chaseccomp/network.chasc new file mode 100644 index 00000000..8e7468d1 --- /dev/null +++ b/lib/chaseccomp/network.chasc @@ -0,0 +1,21 @@ +include common.chasc + +# syscall nr is loaded in common.chasc + +# for curl +ifeqdef SYS_send allow +ifeqdef SYS_recv allow +ifeqdef SYS_recvfrom allow +ifeqdef SYS_sendto allow +ifeqdef SYS_recvmsg allow +ifeqdef SYS_sendmsg allow + +# used indirectly by OpenSSL EVP_RAND_CTX_new (through drbg) +ifeqdef SYS_getpid allow + +: deny +ret kill +: eperm +ret errno EPERM +: allow +ret allow |