From 163f15e93f009d15ecc2f93932e26728aff9904b Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 25 Jan 2025 12:59:11 +0900 Subject: helper/seccomp: separate seccomp package Signed-off-by: Ophestra --- helper/bwrap/seccomp-export.c | 308 ---------------------------------------- helper/bwrap/seccomp-export.h | 23 --- helper/bwrap/seccomp-resolve.go | 43 ++---- helper/bwrap/seccomp.go | 84 ----------- helper/seccomp/export.go | 17 +++ helper/seccomp/seccomp-export.c | 308 ++++++++++++++++++++++++++++++++++++++++ helper/seccomp/seccomp-export.h | 23 +++ helper/seccomp/seccomp.go | 82 +++++++++++ internal/proc/priv/shim/main.go | 4 +- main.go | 4 +- 10 files changed, 449 insertions(+), 447 deletions(-) delete mode 100644 helper/bwrap/seccomp-export.c delete mode 100644 helper/bwrap/seccomp-export.h delete mode 100644 helper/bwrap/seccomp.go create mode 100644 helper/seccomp/export.go create mode 100644 helper/seccomp/seccomp-export.c create mode 100644 helper/seccomp/seccomp-export.h create mode 100644 helper/seccomp/seccomp.go diff --git a/helper/bwrap/seccomp-export.c b/helper/bwrap/seccomp-export.c deleted file mode 100644 index 78d27e0b..00000000 --- a/helper/bwrap/seccomp-export.c +++ /dev/null @@ -1,308 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE // CLONE_NEWUSER -#endif - -#include "seccomp-export.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if (SCMP_VER_MAJOR < 2) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) -#error This package requires libseccomp >= v2.5.1 -#endif - -struct f_syscall_act { - int syscall; - int m_errno; - struct scmp_arg_cmp *arg; -}; - -#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) - -#define SECCOMP_RULESET_ADD(ruleset) do { \ - F_println("adding seccomp ruleset \"" #ruleset "\""); \ - for (int i = 0; i < LEN(ruleset); i++) { \ - assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \ - \ - if (ruleset[i].arg) \ - ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \ - else \ - ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \ - \ - if (ret == -EFAULT) { \ - res = 4; \ - goto out; \ - } else if (ret < 0) { \ - res = 5; \ - errno = -ret; \ - goto out; \ - } \ - } \ -} while (0) - - -int f_tmpfile_fd() { - FILE *f = tmpfile(); - if (f == NULL) - return -1; - return fileno(f); -} - -int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) { - int32_t res = 0; // refer to resErr for meaning - int allow_multiarch = opts & F_MULTIARCH; - int allowed_personality = PER_LINUX; - - if (opts & F_LINUX32) - allowed_personality = PER_LINUX32; - - // flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 - - struct f_syscall_act deny_common[] = { - // Block dmesg - {SCMP_SYS(syslog), EPERM}, - // Useless old syscall - {SCMP_SYS(uselib), EPERM}, - // Don't allow disabling accounting - {SCMP_SYS(acct), EPERM}, - // Don't allow reading current quota use - {SCMP_SYS(quotactl), EPERM}, - - // Don't allow access to the kernel keyring - {SCMP_SYS(add_key), EPERM}, - {SCMP_SYS(keyctl), EPERM}, - {SCMP_SYS(request_key), EPERM}, - - // Scary VM/NUMA ops - {SCMP_SYS(move_pages), EPERM}, - {SCMP_SYS(mbind), EPERM}, - {SCMP_SYS(get_mempolicy), EPERM}, - {SCMP_SYS(set_mempolicy), EPERM}, - {SCMP_SYS(migrate_pages), EPERM}, - }; - - // fortify: project-specific extensions - struct f_syscall_act deny_common_ext[] = { - // system calls for changing the system clock - {SCMP_SYS(adjtimex), EPERM}, - {SCMP_SYS(clock_adjtime), EPERM}, - {SCMP_SYS(clock_adjtime64), EPERM}, - {SCMP_SYS(clock_settime), EPERM}, - {SCMP_SYS(clock_settime64), EPERM}, - {SCMP_SYS(settimeofday), EPERM}, - - // loading and unloading of kernel modules - {SCMP_SYS(delete_module), EPERM}, - {SCMP_SYS(finit_module), EPERM}, - {SCMP_SYS(init_module), EPERM}, - - // system calls for rebooting and reboot preparation - {SCMP_SYS(kexec_file_load), EPERM}, - {SCMP_SYS(kexec_load), EPERM}, - {SCMP_SYS(reboot), EPERM}, - - // system calls for enabling/disabling swap devices - {SCMP_SYS(swapoff), EPERM}, - {SCMP_SYS(swapon), EPERM}, - }; - - struct f_syscall_act deny_ns[] = { - // Don't allow subnamespace setups: - {SCMP_SYS(unshare), EPERM}, - {SCMP_SYS(setns), EPERM}, - {SCMP_SYS(mount), EPERM}, - {SCMP_SYS(umount), EPERM}, - {SCMP_SYS(umount2), EPERM}, - {SCMP_SYS(pivot_root), EPERM}, - {SCMP_SYS(chroot), EPERM}, -#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__) - // Architectures with CONFIG_CLONE_BACKWARDS2: the child stack - // and flags arguments are reversed so the flags come second - {SCMP_SYS(clone), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)}, -#else - // Normally the flags come first - {SCMP_SYS(clone), EPERM, &SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)}, -#endif - - // seccomp can't look into clone3()'s struct clone_args to check whether - // the flags are OK, so we have no choice but to block clone3(). - // Return ENOSYS so user-space will fall back to clone(). - // (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d) - {SCMP_SYS(clone3), ENOSYS}, - - // New mount manipulation APIs can also change our VFS. There's no - // legitimate reason to do these in the sandbox, so block all of them - // rather than thinking about which ones might be dangerous. - // (CVE-2021-41133) - {SCMP_SYS(open_tree), ENOSYS}, - {SCMP_SYS(move_mount), ENOSYS}, - {SCMP_SYS(fsopen), ENOSYS}, - {SCMP_SYS(fsconfig), ENOSYS}, - {SCMP_SYS(fsmount), ENOSYS}, - {SCMP_SYS(fspick), ENOSYS}, - {SCMP_SYS(mount_setattr), ENOSYS}, - }; - - // fortify: project-specific extensions - struct f_syscall_act deny_ns_ext[] = { - // changing file ownership - {SCMP_SYS(chown), EPERM}, - {SCMP_SYS(chown32), EPERM}, - {SCMP_SYS(fchown), EPERM}, - {SCMP_SYS(fchown32), EPERM}, - {SCMP_SYS(fchownat), EPERM}, - {SCMP_SYS(lchown), EPERM}, - {SCMP_SYS(lchown32), EPERM}, - - // system calls for changing user ID and group ID credentials - {SCMP_SYS(setgid), EPERM}, - {SCMP_SYS(setgid32), EPERM}, - {SCMP_SYS(setgroups), EPERM}, - {SCMP_SYS(setgroups32), EPERM}, - {SCMP_SYS(setregid), EPERM}, - {SCMP_SYS(setregid32), EPERM}, - {SCMP_SYS(setresgid), EPERM}, - {SCMP_SYS(setresgid32), EPERM}, - {SCMP_SYS(setresuid), EPERM}, - {SCMP_SYS(setresuid32), EPERM}, - {SCMP_SYS(setreuid), EPERM}, - {SCMP_SYS(setreuid32), EPERM}, - {SCMP_SYS(setuid), EPERM}, - {SCMP_SYS(setuid32), EPERM}, - }; - - struct f_syscall_act deny_tty[] = { - // Don't allow faking input to the controlling tty (CVE-2017-5226) - {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)}, - // In the unlikely event that the controlling tty is a Linux virtual - // console (/dev/tty2 or similar), copy/paste operations have an effect - // similar to TIOCSTI (CVE-2023-28100) - {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCLINUX)}, - }; - - struct f_syscall_act deny_devel[] = { - // Profiling operations; we expect these to be done by tools from outside - // the sandbox. In particular perf has been the source of many CVEs. - {SCMP_SYS(perf_event_open), EPERM}, - // Don't allow you to switch to bsd emulation or whatnot - {SCMP_SYS(personality), EPERM, &SCMP_A0(SCMP_CMP_NE, allowed_personality)}, - - {SCMP_SYS(ptrace), EPERM} - }; - - struct f_syscall_act deny_emu[] = { - // modify_ldt is a historic source of interesting information leaks, - // so it's disabled as a hardening measure. - // However, it is required to run old 16-bit applications - // as well as some Wine patches, so it's allowed in multiarch. - {SCMP_SYS(modify_ldt), EPERM}, - }; - - // fortify: project-specific extensions - struct f_syscall_act deny_emu_ext[] = { - {SCMP_SYS(subpage_prot), ENOSYS}, - {SCMP_SYS(switch_endian), ENOSYS}, - {SCMP_SYS(vm86), ENOSYS}, - {SCMP_SYS(vm86old), ENOSYS}, - }; - - // Blocklist all but unix, inet, inet6 and netlink - struct - { - int family; - f_syscall_opts flags_mask; - } socket_family_allowlist[] = { - // NOTE: Keep in numerical order - { AF_UNSPEC, 0 }, - { AF_LOCAL, 0 }, - { AF_INET, 0 }, - { AF_INET6, 0 }, - { AF_NETLINK, 0 }, - { AF_CAN, F_CAN }, - { AF_BLUETOOTH, F_BLUETOOTH }, - }; - - scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); - if (ctx == NULL) { - res = 1; - goto out; - } else - errno = 0; - - int ret; - - // We only really need to handle arches on multiarch systems. - // If only one arch is supported the default is fine - if (arch != 0) { - // This *adds* the target arch, instead of replacing the - // native one. This is not ideal, because we'd like to only - // allow the target arch, but we can't really disallow the - // native arch at this point, because then bubblewrap - // couldn't continue running. - ret = seccomp_arch_add(ctx, arch); - if (ret < 0 && ret != -EEXIST) { - res = 2; - errno = -ret; - goto out; - } - - if (allow_multiarch && multiarch != 0) { - ret = seccomp_arch_add(ctx, multiarch); - if (ret < 0 && ret != -EEXIST) { - res = 3; - errno = -ret; - goto out; - } - } - } - - SECCOMP_RULESET_ADD(deny_common); - if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns); - if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty); - if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel); - if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu); - if (opts & F_EXT) { - SECCOMP_RULESET_ADD(deny_common_ext); - if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns_ext); - if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu_ext); - } - - // Socket filtering doesn't work on e.g. i386, so ignore failures here - // However, we need to user seccomp_rule_add_exact to avoid libseccomp doing - // something else: https://github.com/seccomp/libseccomp/issues/8 - int last_allowed_family = -1; - for (int i = 0; i < LEN(socket_family_allowlist); i++) { - if (socket_family_allowlist[i].flags_mask != 0 && - (socket_family_allowlist[i].flags_mask & opts) != socket_family_allowlist[i].flags_mask) - continue; - - for (int disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) { - // Blocklist the in-between valid families - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed)); - } - last_allowed_family = socket_family_allowlist[i].family; - } - // Blocklist the rest - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); - - ret = seccomp_export_bpf(ctx, fd); - if (ret != 0) { - res = 6; - errno = -ret; - goto out; - } - -out: - if (ctx) - seccomp_release(ctx); - - return res; -} diff --git a/helper/bwrap/seccomp-export.h b/helper/bwrap/seccomp-export.h deleted file mode 100644 index 90640d8f..00000000 --- a/helper/bwrap/seccomp-export.h +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include - -#if (SCMP_VER_MAJOR < 2) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) -#error This package requires libseccomp >= v2.5.1 -#endif - -typedef enum { - F_EXT = 1 << 0, - F_DENY_NS = 1 << 1, - F_DENY_TTY = 1 << 2, - F_DENY_DEVEL = 1 << 3, - F_MULTIARCH = 1 << 4, - F_LINUX32 = 1 << 5, - F_CAN = 1 << 6, - F_BLUETOOTH = 1 << 7, -} f_syscall_opts; - -extern void F_println(char *v); -int f_tmpfile_fd(); -int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts); \ No newline at end of file diff --git a/helper/bwrap/seccomp-resolve.go b/helper/bwrap/seccomp-resolve.go index 01df4dcc..860d720b 100644 --- a/helper/bwrap/seccomp-resolve.go +++ b/helper/bwrap/seccomp-resolve.go @@ -2,9 +2,9 @@ package bwrap import ( "fmt" - "io" "os" + "git.gensokyo.uk/security/fortify/helper/seccomp" "git.gensokyo.uk/security/fortify/internal/fmsg" ) @@ -53,24 +53,24 @@ func (c *Config) resolveSeccomp() (*os.File, error) { // resolve seccomp filter opts var ( - opts syscallOpts + opts seccomp.SyscallOpts optd []string optCond = [...]struct { v bool - o syscallOpts + o seccomp.SyscallOpts d string }{ - {!c.Syscall.Compat, flagExt, "fortify"}, - {!c.UserNS, flagDenyNS, "denyns"}, - {c.NewSession, flagDenyTTY, "denytty"}, - {c.Syscall.DenyDevel, flagDenyDevel, "denydevel"}, - {c.Syscall.Multiarch, flagMultiarch, "multiarch"}, - {c.Syscall.Linux32, flagLinux32, "linux32"}, - {c.Syscall.Can, flagCan, "can"}, - {c.Syscall.Bluetooth, flagBluetooth, "bluetooth"}, + {!c.Syscall.Compat, seccomp.FlagExt, "fortify"}, + {!c.UserNS, seccomp.FlagDenyNS, "denyns"}, + {c.NewSession, seccomp.FlagDenyTTY, "denytty"}, + {c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"}, + {c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"}, + {c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"}, + {c.Syscall.Can, seccomp.FlagCan, "can"}, + {c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"}, } ) - if CPrintln != nil { + if seccomp.CPrintln != nil { optd = make([]string, 1, len(optCond)+1) optd[0] = "common" } @@ -82,22 +82,9 @@ func (c *Config) resolveSeccomp() (*os.File, error) { } } } - if CPrintln != nil { - CPrintln(fmt.Sprintf("seccomp flags: %s", optd)) + if seccomp.CPrintln != nil { + seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd)) } - // export seccomp filter to tmpfile - if f, err := tmpfile(); err != nil { - return nil, err - } else { - return f, exportAndSeek(f, opts) - } -} - -func exportAndSeek(f *os.File, opts syscallOpts) error { - if err := exportFilter(f.Fd(), opts); err != nil { - return err - } - _, err := f.Seek(0, io.SeekStart) - return err + return seccomp.Export(opts) } diff --git a/helper/bwrap/seccomp.go b/helper/bwrap/seccomp.go deleted file mode 100644 index f77abc66..00000000 --- a/helper/bwrap/seccomp.go +++ /dev/null @@ -1,84 +0,0 @@ -package bwrap - -/* -#cgo linux pkg-config: --static libseccomp - -#include "seccomp-export.h" -*/ -import "C" -import ( - "errors" - "fmt" - "os" - "runtime" -) - -var CPrintln func(v ...any) - -var resErr = [...]error{ - 0: nil, - 1: errors.New("seccomp_init failed"), - 2: errors.New("seccomp_arch_add failed"), - 3: errors.New("seccomp_arch_add failed (multiarch)"), - 4: errors.New("internal libseccomp failure"), - 5: errors.New("seccomp_rule_add failed"), - 6: errors.New("seccomp_export_bpf failed"), -} - -type ( - syscallOpts = C.f_syscall_opts -) - -const ( - flagExt syscallOpts = C.F_EXT - flagDenyNS syscallOpts = C.F_DENY_NS - flagDenyTTY syscallOpts = C.F_DENY_TTY - flagDenyDevel syscallOpts = C.F_DENY_DEVEL - flagMultiarch syscallOpts = C.F_MULTIARCH - flagLinux32 syscallOpts = C.F_LINUX32 - flagCan syscallOpts = C.F_CAN - flagBluetooth syscallOpts = C.F_BLUETOOTH -) - -func tmpfile() (*os.File, error) { - fd, err := C.f_tmpfile_fd() - if err != nil { - return nil, err - } - return os.NewFile(uintptr(fd), "tmpfile"), err -} - -func exportFilter(fd uintptr, opts syscallOpts) error { - var ( - arch C.uint32_t = 0 - multiarch C.uint32_t = 0 - ) - switch runtime.GOARCH { - case "386": - arch = C.SCMP_ARCH_X86 - case "amd64": - arch = C.SCMP_ARCH_X86_64 - multiarch = C.SCMP_ARCH_X86 - case "arm": - arch = C.SCMP_ARCH_ARM - case "arm64": - arch = C.SCMP_ARCH_AARCH64 - multiarch = C.SCMP_ARCH_ARM - } - - res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts) - if re := resErr[res]; re != nil { - if err == nil { - return re - } - return fmt.Errorf("%s: %v", re.Error(), err) - } - return err -} - -//export F_println -func F_println(v *C.char) { - if CPrintln != nil { - CPrintln(C.GoString(v)) - } -} diff --git a/helper/seccomp/export.go b/helper/seccomp/export.go new file mode 100644 index 00000000..1bcb7146 --- /dev/null +++ b/helper/seccomp/export.go @@ -0,0 +1,17 @@ +package seccomp + +import ( + "io" + "os" +) + +func Export(opts SyscallOpts) (f *os.File, err error) { + if f, err = tmpfile(); err != nil { + return + } + if err = exportFilter(f.Fd(), opts); err != nil { + return + } + _, err = f.Seek(0, io.SeekStart) + return +} diff --git a/helper/seccomp/seccomp-export.c b/helper/seccomp/seccomp-export.c new file mode 100644 index 00000000..78d27e0b --- /dev/null +++ b/helper/seccomp/seccomp-export.c @@ -0,0 +1,308 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE // CLONE_NEWUSER +#endif + +#include "seccomp-export.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if (SCMP_VER_MAJOR < 2) || \ + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) +#error This package requires libseccomp >= v2.5.1 +#endif + +struct f_syscall_act { + int syscall; + int m_errno; + struct scmp_arg_cmp *arg; +}; + +#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) + +#define SECCOMP_RULESET_ADD(ruleset) do { \ + F_println("adding seccomp ruleset \"" #ruleset "\""); \ + for (int i = 0; i < LEN(ruleset); i++) { \ + assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \ + \ + if (ruleset[i].arg) \ + ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \ + else \ + ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \ + \ + if (ret == -EFAULT) { \ + res = 4; \ + goto out; \ + } else if (ret < 0) { \ + res = 5; \ + errno = -ret; \ + goto out; \ + } \ + } \ +} while (0) + + +int f_tmpfile_fd() { + FILE *f = tmpfile(); + if (f == NULL) + return -1; + return fileno(f); +} + +int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) { + int32_t res = 0; // refer to resErr for meaning + int allow_multiarch = opts & F_MULTIARCH; + int allowed_personality = PER_LINUX; + + if (opts & F_LINUX32) + allowed_personality = PER_LINUX32; + + // flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 + + struct f_syscall_act deny_common[] = { + // Block dmesg + {SCMP_SYS(syslog), EPERM}, + // Useless old syscall + {SCMP_SYS(uselib), EPERM}, + // Don't allow disabling accounting + {SCMP_SYS(acct), EPERM}, + // Don't allow reading current quota use + {SCMP_SYS(quotactl), EPERM}, + + // Don't allow access to the kernel keyring + {SCMP_SYS(add_key), EPERM}, + {SCMP_SYS(keyctl), EPERM}, + {SCMP_SYS(request_key), EPERM}, + + // Scary VM/NUMA ops + {SCMP_SYS(move_pages), EPERM}, + {SCMP_SYS(mbind), EPERM}, + {SCMP_SYS(get_mempolicy), EPERM}, + {SCMP_SYS(set_mempolicy), EPERM}, + {SCMP_SYS(migrate_pages), EPERM}, + }; + + // fortify: project-specific extensions + struct f_syscall_act deny_common_ext[] = { + // system calls for changing the system clock + {SCMP_SYS(adjtimex), EPERM}, + {SCMP_SYS(clock_adjtime), EPERM}, + {SCMP_SYS(clock_adjtime64), EPERM}, + {SCMP_SYS(clock_settime), EPERM}, + {SCMP_SYS(clock_settime64), EPERM}, + {SCMP_SYS(settimeofday), EPERM}, + + // loading and unloading of kernel modules + {SCMP_SYS(delete_module), EPERM}, + {SCMP_SYS(finit_module), EPERM}, + {SCMP_SYS(init_module), EPERM}, + + // system calls for rebooting and reboot preparation + {SCMP_SYS(kexec_file_load), EPERM}, + {SCMP_SYS(kexec_load), EPERM}, + {SCMP_SYS(reboot), EPERM}, + + // system calls for enabling/disabling swap devices + {SCMP_SYS(swapoff), EPERM}, + {SCMP_SYS(swapon), EPERM}, + }; + + struct f_syscall_act deny_ns[] = { + // Don't allow subnamespace setups: + {SCMP_SYS(unshare), EPERM}, + {SCMP_SYS(setns), EPERM}, + {SCMP_SYS(mount), EPERM}, + {SCMP_SYS(umount), EPERM}, + {SCMP_SYS(umount2), EPERM}, + {SCMP_SYS(pivot_root), EPERM}, + {SCMP_SYS(chroot), EPERM}, +#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__) + // Architectures with CONFIG_CLONE_BACKWARDS2: the child stack + // and flags arguments are reversed so the flags come second + {SCMP_SYS(clone), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)}, +#else + // Normally the flags come first + {SCMP_SYS(clone), EPERM, &SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)}, +#endif + + // seccomp can't look into clone3()'s struct clone_args to check whether + // the flags are OK, so we have no choice but to block clone3(). + // Return ENOSYS so user-space will fall back to clone(). + // (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d) + {SCMP_SYS(clone3), ENOSYS}, + + // New mount manipulation APIs can also change our VFS. There's no + // legitimate reason to do these in the sandbox, so block all of them + // rather than thinking about which ones might be dangerous. + // (CVE-2021-41133) + {SCMP_SYS(open_tree), ENOSYS}, + {SCMP_SYS(move_mount), ENOSYS}, + {SCMP_SYS(fsopen), ENOSYS}, + {SCMP_SYS(fsconfig), ENOSYS}, + {SCMP_SYS(fsmount), ENOSYS}, + {SCMP_SYS(fspick), ENOSYS}, + {SCMP_SYS(mount_setattr), ENOSYS}, + }; + + // fortify: project-specific extensions + struct f_syscall_act deny_ns_ext[] = { + // changing file ownership + {SCMP_SYS(chown), EPERM}, + {SCMP_SYS(chown32), EPERM}, + {SCMP_SYS(fchown), EPERM}, + {SCMP_SYS(fchown32), EPERM}, + {SCMP_SYS(fchownat), EPERM}, + {SCMP_SYS(lchown), EPERM}, + {SCMP_SYS(lchown32), EPERM}, + + // system calls for changing user ID and group ID credentials + {SCMP_SYS(setgid), EPERM}, + {SCMP_SYS(setgid32), EPERM}, + {SCMP_SYS(setgroups), EPERM}, + {SCMP_SYS(setgroups32), EPERM}, + {SCMP_SYS(setregid), EPERM}, + {SCMP_SYS(setregid32), EPERM}, + {SCMP_SYS(setresgid), EPERM}, + {SCMP_SYS(setresgid32), EPERM}, + {SCMP_SYS(setresuid), EPERM}, + {SCMP_SYS(setresuid32), EPERM}, + {SCMP_SYS(setreuid), EPERM}, + {SCMP_SYS(setreuid32), EPERM}, + {SCMP_SYS(setuid), EPERM}, + {SCMP_SYS(setuid32), EPERM}, + }; + + struct f_syscall_act deny_tty[] = { + // Don't allow faking input to the controlling tty (CVE-2017-5226) + {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)}, + // In the unlikely event that the controlling tty is a Linux virtual + // console (/dev/tty2 or similar), copy/paste operations have an effect + // similar to TIOCSTI (CVE-2023-28100) + {SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCLINUX)}, + }; + + struct f_syscall_act deny_devel[] = { + // Profiling operations; we expect these to be done by tools from outside + // the sandbox. In particular perf has been the source of many CVEs. + {SCMP_SYS(perf_event_open), EPERM}, + // Don't allow you to switch to bsd emulation or whatnot + {SCMP_SYS(personality), EPERM, &SCMP_A0(SCMP_CMP_NE, allowed_personality)}, + + {SCMP_SYS(ptrace), EPERM} + }; + + struct f_syscall_act deny_emu[] = { + // modify_ldt is a historic source of interesting information leaks, + // so it's disabled as a hardening measure. + // However, it is required to run old 16-bit applications + // as well as some Wine patches, so it's allowed in multiarch. + {SCMP_SYS(modify_ldt), EPERM}, + }; + + // fortify: project-specific extensions + struct f_syscall_act deny_emu_ext[] = { + {SCMP_SYS(subpage_prot), ENOSYS}, + {SCMP_SYS(switch_endian), ENOSYS}, + {SCMP_SYS(vm86), ENOSYS}, + {SCMP_SYS(vm86old), ENOSYS}, + }; + + // Blocklist all but unix, inet, inet6 and netlink + struct + { + int family; + f_syscall_opts flags_mask; + } socket_family_allowlist[] = { + // NOTE: Keep in numerical order + { AF_UNSPEC, 0 }, + { AF_LOCAL, 0 }, + { AF_INET, 0 }, + { AF_INET6, 0 }, + { AF_NETLINK, 0 }, + { AF_CAN, F_CAN }, + { AF_BLUETOOTH, F_BLUETOOTH }, + }; + + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL) { + res = 1; + goto out; + } else + errno = 0; + + int ret; + + // We only really need to handle arches on multiarch systems. + // If only one arch is supported the default is fine + if (arch != 0) { + // This *adds* the target arch, instead of replacing the + // native one. This is not ideal, because we'd like to only + // allow the target arch, but we can't really disallow the + // native arch at this point, because then bubblewrap + // couldn't continue running. + ret = seccomp_arch_add(ctx, arch); + if (ret < 0 && ret != -EEXIST) { + res = 2; + errno = -ret; + goto out; + } + + if (allow_multiarch && multiarch != 0) { + ret = seccomp_arch_add(ctx, multiarch); + if (ret < 0 && ret != -EEXIST) { + res = 3; + errno = -ret; + goto out; + } + } + } + + SECCOMP_RULESET_ADD(deny_common); + if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns); + if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty); + if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel); + if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu); + if (opts & F_EXT) { + SECCOMP_RULESET_ADD(deny_common_ext); + if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns_ext); + if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu_ext); + } + + // Socket filtering doesn't work on e.g. i386, so ignore failures here + // However, we need to user seccomp_rule_add_exact to avoid libseccomp doing + // something else: https://github.com/seccomp/libseccomp/issues/8 + int last_allowed_family = -1; + for (int i = 0; i < LEN(socket_family_allowlist); i++) { + if (socket_family_allowlist[i].flags_mask != 0 && + (socket_family_allowlist[i].flags_mask & opts) != socket_family_allowlist[i].flags_mask) + continue; + + for (int disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) { + // Blocklist the in-between valid families + seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed)); + } + last_allowed_family = socket_family_allowlist[i].family; + } + // Blocklist the rest + seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); + + ret = seccomp_export_bpf(ctx, fd); + if (ret != 0) { + res = 6; + errno = -ret; + goto out; + } + +out: + if (ctx) + seccomp_release(ctx); + + return res; +} diff --git a/helper/seccomp/seccomp-export.h b/helper/seccomp/seccomp-export.h new file mode 100644 index 00000000..90640d8f --- /dev/null +++ b/helper/seccomp/seccomp-export.h @@ -0,0 +1,23 @@ +#include +#include + +#if (SCMP_VER_MAJOR < 2) || \ + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) +#error This package requires libseccomp >= v2.5.1 +#endif + +typedef enum { + F_EXT = 1 << 0, + F_DENY_NS = 1 << 1, + F_DENY_TTY = 1 << 2, + F_DENY_DEVEL = 1 << 3, + F_MULTIARCH = 1 << 4, + F_LINUX32 = 1 << 5, + F_CAN = 1 << 6, + F_BLUETOOTH = 1 << 7, +} f_syscall_opts; + +extern void F_println(char *v); +int f_tmpfile_fd(); +int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts); \ No newline at end of file diff --git a/helper/seccomp/seccomp.go b/helper/seccomp/seccomp.go new file mode 100644 index 00000000..b3294672 --- /dev/null +++ b/helper/seccomp/seccomp.go @@ -0,0 +1,82 @@ +package seccomp + +/* +#cgo linux pkg-config: --static libseccomp + +#include "seccomp-export.h" +*/ +import "C" +import ( + "errors" + "fmt" + "os" + "runtime" +) + +var CPrintln func(v ...any) + +var resErr = [...]error{ + 0: nil, + 1: errors.New("seccomp_init failed"), + 2: errors.New("seccomp_arch_add failed"), + 3: errors.New("seccomp_arch_add failed (multiarch)"), + 4: errors.New("internal libseccomp failure"), + 5: errors.New("seccomp_rule_add failed"), + 6: errors.New("seccomp_export_bpf failed"), +} + +type SyscallOpts = C.f_syscall_opts + +const ( + FlagExt SyscallOpts = C.F_EXT + FlagDenyNS SyscallOpts = C.F_DENY_NS + FlagDenyTTY SyscallOpts = C.F_DENY_TTY + FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL + FlagMultiarch SyscallOpts = C.F_MULTIARCH + FlagLinux32 SyscallOpts = C.F_LINUX32 + FlagCan SyscallOpts = C.F_CAN + FlagBluetooth SyscallOpts = C.F_BLUETOOTH +) + +func tmpfile() (*os.File, error) { + fd, err := C.f_tmpfile_fd() + if err != nil { + return nil, err + } + return os.NewFile(uintptr(fd), "tmpfile"), err +} + +func exportFilter(fd uintptr, opts SyscallOpts) error { + var ( + arch C.uint32_t = 0 + multiarch C.uint32_t = 0 + ) + switch runtime.GOARCH { + case "386": + arch = C.SCMP_ARCH_X86 + case "amd64": + arch = C.SCMP_ARCH_X86_64 + multiarch = C.SCMP_ARCH_X86 + case "arm": + arch = C.SCMP_ARCH_ARM + case "arm64": + arch = C.SCMP_ARCH_AARCH64 + multiarch = C.SCMP_ARCH_ARM + } + + res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts) + if re := resErr[res]; re != nil { + if err == nil { + return re + } + return fmt.Errorf("%s: %v", re.Error(), err) + } + return err +} + +//export F_println +func F_println(v *C.char) { + if CPrintln != nil { + CPrintln(C.GoString(v)) + } +} diff --git a/internal/proc/priv/shim/main.go b/internal/proc/priv/shim/main.go index 85c5bafe..7ad0a44b 100644 --- a/internal/proc/priv/shim/main.go +++ b/internal/proc/priv/shim/main.go @@ -8,7 +8,7 @@ import ( "git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/helper" - "git.gensokyo.uk/security/fortify/helper/bwrap" + "git.gensokyo.uk/security/fortify/helper/seccomp" "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/proc" @@ -128,7 +128,7 @@ func Main() { helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent if fmsg.Verbose() { - bwrap.CPrintln = fmsg.Println + seccomp.CPrintln = fmsg.Println } if b, err := helper.NewBwrap( conf, innerInit, diff --git a/main.go b/main.go index 2a478063..36396edf 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/fst" - "git.gensokyo.uk/security/fortify/helper/bwrap" + "git.gensokyo.uk/security/fortify/helper/seccomp" "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/fmsg" @@ -310,7 +310,7 @@ func runApp(config *fst.Config) { ctx, cancel := context.WithCancel(context.Background()) if fmsg.Verbose() { - bwrap.CPrintln = fmsg.Println + seccomp.CPrintln = fmsg.Println } // handle signals for graceful shutdown -- cgit v1.3.1