diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-03-14 22:42:40 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-03-14 22:42:40 +0900 |
| commit | 2647a71be1f287e50011a65653b9e9c806627899 (patch) | |
| tree | 252dd8865632df348b7573ca6f31c04e4cb6c9e5 /seccomp | |
| parent | 7c60a4d8e8baa77b42789c041838fa82dd7d36f9 (diff) | |
seccomp: move out of helper
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'seccomp')
| -rw-r--r-- | seccomp/api.go | 71 | ||||
| -rw-r--r-- | seccomp/export.go | 58 | ||||
| -rw-r--r-- | seccomp/export_test.go | 139 | ||||
| -rw-r--r-- | seccomp/seccomp-build.c | 302 | ||||
| -rw-r--r-- | seccomp/seccomp-build.h | 23 | ||||
| -rw-r--r-- | seccomp/seccomp.go | 123 | ||||
| -rw-r--r-- | seccomp/seccomp_test.go | 65 |
7 files changed, 781 insertions, 0 deletions
diff --git a/seccomp/api.go b/seccomp/api.go new file mode 100644 index 00000000..697b09f3 --- /dev/null +++ b/seccomp/api.go @@ -0,0 +1,71 @@ +package seccomp + +import ( + "context" + "errors" + "syscall" + + "git.gensokyo.uk/security/fortify/helper/proc" +) + +// New returns an inactive Encoder instance. +func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} } + +// Load loads a filter into the kernel. +func Load(opts SyscallOpts) error { return buildFilter(-1, opts) } + +/* +An Encoder writes a BPF program to an output stream. + +Methods of Encoder are not safe for concurrent use. + +An Encoder must not be copied after first use. +*/ +type Encoder struct { + *exporter +} + +func (e *Encoder) Read(p []byte) (n int, err error) { + if err = e.prepare(); err != nil { + return + } + return e.r.Read(p) +} + +func (e *Encoder) Close() error { + if e.r == nil { + return syscall.EINVAL + } + + // this hangs if the cgo thread fails to exit + return errors.Join(e.closeWrite(), <-e.exportErr) +} + +// NewFile returns an instance of exporter implementing [proc.File]. +func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} } + +// File implements [proc.File] and provides access to the read end of exporter pipe. +type File struct { + opts SyscallOpts + proc.BaseFile +} + +func (f *File) ErrCount() int { return 2 } +func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error { + e := newExporter(f.opts) + if err := e.prepare(); err != nil { + return err + } + f.Set(e.r) + go func() { + select { + case err := <-e.exportErr: + dispatchErr(nil) + dispatchErr(err) + case <-ctx.Done(): + dispatchErr(e.closeWrite()) + dispatchErr(<-e.exportErr) + } + }() + return nil +} diff --git a/seccomp/export.go b/seccomp/export.go new file mode 100644 index 00000000..95684235 --- /dev/null +++ b/seccomp/export.go @@ -0,0 +1,58 @@ +package seccomp + +import ( + "os" + "runtime" + "sync" +) + +type exporter struct { + opts SyscallOpts + r, w *os.File + + prepareOnce sync.Once + prepareErr error + closeOnce sync.Once + closeErr error + exportErr <-chan error +} + +func (e *exporter) prepare() error { + e.prepareOnce.Do(func() { + if r, w, err := os.Pipe(); err != nil { + e.prepareErr = err + return + } else { + e.r, e.w = r, w + } + + ec := make(chan error, 1) + go func(fd uintptr) { + ec <- buildFilter(int(fd), e.opts) + close(ec) + _ = e.closeWrite() + runtime.KeepAlive(e.w) + }(e.w.Fd()) + e.exportErr = ec + runtime.SetFinalizer(e, (*exporter).closeWrite) + }) + return e.prepareErr +} + +func (e *exporter) closeWrite() error { + e.closeOnce.Do(func() { + if e.w == nil { + panic("closeWrite called on invalid exporter") + } + e.closeErr = e.w.Close() + + // no need for a finalizer anymore + runtime.SetFinalizer(e, nil) + }) + + return e.closeErr +} + +func newExporter(opts SyscallOpts) *exporter { + return &exporter{opts: opts} +} diff --git a/seccomp/export_test.go b/seccomp/export_test.go new file mode 100644 index 00000000..991517f4 --- /dev/null +++ b/seccomp/export_test.go @@ -0,0 +1,139 @@ +package seccomp_test + +import ( + "crypto/sha512" + "errors" + "io" + "log" + "slices" + "syscall" + "testing" + + "git.gensokyo.uk/security/fortify/seccomp" +) + +func TestExport(t *testing.T) { + testCases := []struct { + name string + opts seccomp.SyscallOpts + want []byte + wantErr bool + }{ + {"compat", 0, []byte{ + 0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07, + 0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec, + 0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7, + 0x31, 0x27, 0x0c, 0x98, 0xea, 0x2d, 0x29, 0x46, + 0xcb, 0x52, 0x31, 0x02, 0x90, 0x63, 0x66, 0x8a, + 0xf2, 0x15, 0x87, 0x91, 0x55, 0xda, 0x21, 0xac, + 0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a, + 0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5, + }, false}, + {"base", seccomp.FlagExt, []byte{ + 0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79, + 0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54, + 0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d, + 0xce, 0x5d, 0xad, 0xc4, 0x73, 0x6c, 0x58, 0x5d, + 0x4d, 0xee, 0xbf, 0xeb, 0x3c, 0x79, 0x69, 0xaf, + 0x3a, 0x07, 0x7e, 0x90, 0xb7, 0x7b, 0xb4, 0x74, + 0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59, + 0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d, + }, false}, + {"everything", seccomp.FlagExt | + seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel | + seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan | + seccomp.FlagBluetooth, []byte{ + 0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34, + 0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57, + 0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f, + 0xe9, 0x34, 0x82, 0x84, 0x7f, 0x50, 0xb5, 0xb7, + 0x58, 0xda, 0x17, 0xe7, 0x4c, 0xa2, 0xbb, 0xc0, + 0x08, 0x13, 0xde, 0x49, 0xa2, 0xb9, 0xbf, 0x83, + 0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69, + 0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb, + }, false}, + {"strict", seccomp.FlagExt | + seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{ + 0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51, + 0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c, + 0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c, + 0x24, 0x4d, 0x8b, 0x8c, 0xf6, 0x86, 0x6d, 0xba, + 0x8e, 0xf4, 0xa3, 0x32, 0x96, 0xf2, 0x87, 0xb6, + 0x6c, 0xcc, 0xc1, 0xd7, 0x8e, 0x97, 0x02, 0x65, + 0x97, 0xf8, 0x4c, 0xc7, 0xde, 0xc1, 0x57, 0x3e, + 0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35, + }, false}, + {"strict compat", 0 | + seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{ + 0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9, + 0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e, + 0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5, + 0xc3, 0xc1, 0x7c, 0x92, 0x52, 0x7f, 0x6e, 0x02, + 0x2e, 0xde, 0x1f, 0x48, 0xbf, 0xf5, 0x92, 0x46, + 0xea, 0x45, 0x2c, 0x0d, 0x1d, 0xe5, 0x48, 0x27, + 0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd, + 0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa, + }, false}, + } + + buf := make([]byte, 8) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + seccomp.CPrintln = log.Println + t.Cleanup(func() { seccomp.CPrintln = nil }) + + e := seccomp.New(tc.opts) + digest := sha512.New() + + if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr { + t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr) + return + } + if err := e.Close(); err != nil { + t.Errorf("Close: error = %v", err) + return + } + if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 { + t.Fatalf("Export() hash = %x, want %x", + got, tc.want) + return + } + }) + } + + t.Run("close without use", func(t *testing.T) { + e := seccomp.New(0) + if err := e.Close(); !errors.Is(err, syscall.EINVAL) { + t.Errorf("Close: error = %v", err) + return + } + }) + + t.Run("close partial read", func(t *testing.T) { + e := seccomp.New(0) + if _, err := e.Read(make([]byte, 0)); err != nil { + t.Errorf("Read: error = %v", err) + return + } + if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) { + t.Errorf("Close: error = %v", err) + return + } + }) +} + +func BenchmarkExport(b *testing.B) { + buf := make([]byte, 8) + for i := 0; i < b.N; i++ { + e := seccomp.New(seccomp.FlagExt | + seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel | + seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan | + seccomp.FlagBluetooth) + if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { + b.Fatalf("cannot export: %v", err) + } + if err := e.Close(); err != nil { + b.Fatalf("cannot close exporter: %v", err) + } + } +} diff --git a/seccomp/seccomp-build.c b/seccomp/seccomp-build.c new file mode 100644 index 00000000..4b2114f4 --- /dev/null +++ b/seccomp/seccomp-build.c @@ -0,0 +1,302 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE // CLONE_NEWUSER +#endif + +#include "seccomp-build.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 { \ + if (opts & F_VERBOSE) 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_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \ + else \ + *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \ + \ + if (*ret_p == -EFAULT) { \ + res = 4; \ + goto out; \ + } else if (*ret_p < 0) { \ + res = 5; \ + goto out; \ + } \ + } \ +} while (0) + +int32_t f_build_filter(int *ret_p, 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; + + // 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_p = seccomp_arch_add(ctx, arch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 2; + goto out; + } + + if (allow_multiarch && multiarch != 0) { + *ret_p = seccomp_arch_add(ctx, multiarch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 3; + 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)); + + if (fd < 0) { + *ret_p = seccomp_load(ctx); + if (*ret_p != 0) { + res = 7; + goto out; + } + } else { + *ret_p = seccomp_export_bpf(ctx, fd); + if (*ret_p != 0) { + res = 6; + goto out; + } + } + +out: + if (ctx) + seccomp_release(ctx); + + return res; +} diff --git a/seccomp/seccomp-build.h b/seccomp/seccomp-build.h new file mode 100644 index 00000000..7f1be761 --- /dev/null +++ b/seccomp/seccomp-build.h @@ -0,0 +1,23 @@ +#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_VERBOSE = 1 << 0, + F_EXT = 1 << 1, + F_DENY_NS = 1 << 2, + F_DENY_TTY = 1 << 3, + F_DENY_DEVEL = 1 << 4, + F_MULTIARCH = 1 << 5, + F_LINUX32 = 1 << 6, + F_CAN = 1 << 7, + F_BLUETOOTH = 1 << 8, +} f_syscall_opts; + +extern void F_println(char *v); +int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
\ No newline at end of file diff --git a/seccomp/seccomp.go b/seccomp/seccomp.go new file mode 100644 index 00000000..05c314c9 --- /dev/null +++ b/seccomp/seccomp.go @@ -0,0 +1,123 @@ +package seccomp + +/* +#cgo linux pkg-config: --static libseccomp + +#include "seccomp-build.h" +*/ +import "C" +import ( + "errors" + "fmt" + "runtime" + "syscall" +) + +var CPrintln func(v ...any) + +// LibraryError represents a libseccomp error. +type LibraryError struct { + Prefix string + Seccomp syscall.Errno + Errno error +} + +func (e *LibraryError) Error() string { + if e.Seccomp == 0 { + if e.Errno == nil { + panic("invalid libseccomp error") + } + return fmt.Sprintf("%s: %s", e.Prefix, e.Errno) + } + if e.Errno == nil { + return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp) + } + return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno) +} + +func (e *LibraryError) Is(err error) bool { + if e == nil { + return err == nil + } + if ef, ok := err.(*LibraryError); ok { + return *e == *ef + } + return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) || + (e.Errno != nil && errors.Is(err, e.Errno)) +} + +var resPrefix = [...]string{ + 0: "", + 1: "seccomp_init failed", + 2: "seccomp_arch_add failed", + 3: "seccomp_arch_add failed (multiarch)", + 4: "internal libseccomp failure", + 5: "seccomp_rule_add failed", + 6: "seccomp_export_bpf failed", + 7: "seccomp_load failed", +} + +type SyscallOpts = C.f_syscall_opts + +const ( + flagVerbose SyscallOpts = C.F_VERBOSE + // FlagExt are project-specific extensions. + FlagExt SyscallOpts = C.F_EXT + // FlagDenyNS denies namespace setup syscalls. + FlagDenyNS SyscallOpts = C.F_DENY_NS + // FlagDenyTTY denies faking input. + FlagDenyTTY SyscallOpts = C.F_DENY_TTY + // FlagDenyDevel denies development-related syscalls. + FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL + // FlagMultiarch allows multiarch/emulation. + FlagMultiarch SyscallOpts = C.F_MULTIARCH + // FlagLinux32 sets PER_LINUX32. + FlagLinux32 SyscallOpts = C.F_LINUX32 + // FlagCan allows AF_CAN. + FlagCan SyscallOpts = C.F_CAN + // FlagBluetooth allows AF_BLUETOOTH. + FlagBluetooth SyscallOpts = C.F_BLUETOOTH +) + +func buildFilter(fd int, 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 + } + + // this removes repeated transitions between C and Go execution + // when producing log output via F_println and CPrintln is nil + if CPrintln != nil { + opts |= flagVerbose + } + + var ret C.int + res, err := C.f_build_filter(&ret, C.int(fd), arch, multiarch, opts) + if prefix := resPrefix[res]; prefix != "" { + return &LibraryError{ + prefix, + -syscall.Errno(ret), + err, + } + } + return err +} + +//export F_println +func F_println(v *C.char) { + if CPrintln != nil { + CPrintln(C.GoString(v)) + } +} diff --git a/seccomp/seccomp_test.go b/seccomp/seccomp_test.go new file mode 100644 index 00000000..f2949ef8 --- /dev/null +++ b/seccomp/seccomp_test.go @@ -0,0 +1,65 @@ +package seccomp_test + +import ( + "errors" + "runtime" + "syscall" + "testing" + + "git.gensokyo.uk/security/fortify/seccomp" +) + +func TestLibraryError(t *testing.T) { + testCases := []struct { + name string + sample *seccomp.LibraryError + want string + wantIs bool + compare error + }{ + { + "full", + &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + "seccomp_export_bpf failed: operation canceled (bad file descriptor)", + true, + &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + }, + { + "errno only", + &seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM}, + "seccomp_init failed: cannot allocate memory", + false, + nil, + }, + { + "seccomp only", + &seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT}, + "internal libseccomp failure: bad address", + true, + syscall.EFAULT, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if errors.Is(tc.sample, tc.compare) != tc.wantIs { + t.Errorf("errors.Is(%#v, %#v) did not return %v", + tc.sample, tc.compare, tc.wantIs) + } + + if got := tc.sample.Error(); got != tc.want { + t.Errorf("Error: %q, want %q", + got, tc.want) + } + }) + } + + t.Run("invalid", func(t *testing.T) { + wantPanic := "invalid libseccomp error" + defer func() { + if r := recover(); r != wantPanic { + t.Errorf("panic: %q, want %q", r, wantPanic) + } + }() + runtime.KeepAlive(new(seccomp.LibraryError).Error()) + }) +} |
