@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/seccomp"
|
||||
)
|
||||
|
||||
func TestConfig_Args(t *testing.T) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/seccomp"
|
||||
)
|
||||
|
||||
type SyscallPolicy struct {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package seccomp_test
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"slices"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -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_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);
|
||||
@@ -1,123 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package seccomp_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/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())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user