helper/seccomp: separate seccomp package
All checks were successful
Test / Create distribution (push) Successful in 1m39s
Test / Run NixOS test (push) Successful in 3m31s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-01-25 12:59:11 +09:00
parent 016da20443
commit 163f15e93f
7 changed files with 47 additions and 45 deletions

View File

@@ -1,308 +0,0 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE // CLONE_NEWUSER
#endif
#include "seccomp-export.h"
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/personality.h>
#include <sched.h>
#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;
}

View File

@@ -1,23 +0,0 @@
#include <stdint.h>
#include <seccomp.h>
#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);

View File

@@ -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)
}

View File

@@ -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))
}
}