about summary refs log tree commit diff stats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/chaseccomp/.gitignore6
-rw-r--r--lib/chaseccomp/Makefile22
-rw-r--r--lib/chaseccomp/README.md41
-rw-r--r--lib/chaseccomp/buffer.chasc39
-rw-r--r--lib/chaseccomp/chaseccomp.c48
-rw-r--r--lib/chaseccomp/chaseccomp.h70
-rw-r--r--lib/chaseccomp/common.chasc74
-rwxr-xr-xlib/chaseccomp/gen_defs78
-rwxr-xr-xlib/chaseccomp/gen_syscalls88
-rw-r--r--lib/chaseccomp/network.chasc21
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