diff --git a/helper/seccomp/api.go b/helper/seccomp/api.go new file mode 100644 index 0000000..5b4a42c --- /dev/null +++ b/helper/seccomp/api.go @@ -0,0 +1,51 @@ +package seccomp + +import ( + "errors" + "io" + "os" + "syscall" +) + +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 +} + +/* +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) +} + +// New returns an inactive Encoder instance. +func New(opts SyscallOpts) *Encoder { + return &Encoder{newExporter(opts)} +} diff --git a/helper/seccomp/export.go b/helper/seccomp/export.go index 1bcb714..4ac620b 100644 --- a/helper/seccomp/export.go +++ b/helper/seccomp/export.go @@ -1,17 +1,56 @@ package seccomp import ( - "io" + "io/fs" "os" + "runtime" + "sync" + "sync/atomic" ) -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 +type exporter struct { + opts SyscallOpts + r, w *os.File + + prepareOnce sync.Once + prepareErr error + closeErr atomic.Pointer[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() { ec <- exportFilter(e.w.Fd(), e.opts); close(ec); _ = e.closeWrite() }() + e.exportErr = ec + runtime.SetFinalizer(e, (*exporter).closeWrite) + }) + return e.prepareErr +} + +func (e *exporter) closeWrite() error { + if !e.closeErr.CompareAndSwap(nil, &fs.ErrInvalid) { + return *e.closeErr.Load() + } + if e.w == nil { + return fs.ErrInvalid + } + err := e.w.Close() + e.closeErr.Store(&err) + + // no need for a finalizer anymore + runtime.SetFinalizer(e, nil) + + return err +} + +func newExporter(opts SyscallOpts) *exporter { + return &exporter{opts: opts} } diff --git a/helper/seccomp/export_test.go b/helper/seccomp/export_test.go new file mode 100644 index 0000000..bbafa3b --- /dev/null +++ b/helper/seccomp/export_test.go @@ -0,0 +1,123 @@ +package seccomp_test + +import ( + "crypto/sha512" + "errors" + "io" + "slices" + "syscall" + "testing" + + "git.gensokyo.uk/security/fortify/helper/seccomp" + "git.gensokyo.uk/security/fortify/internal/fmsg" +) + +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 = fmsg.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 || err.Error() != "seccomp_export_bpf failed: operation canceled" { + t.Errorf("Close: error = %v", err) + return + } + }) +} diff --git a/helper/seccomp/seccomp.go b/helper/seccomp/seccomp.go index ed13118..affe369 100644 --- a/helper/seccomp/seccomp.go +++ b/helper/seccomp/seccomp.go @@ -28,14 +28,22 @@ var resErr = [...]error{ type SyscallOpts = C.f_syscall_opts const ( - flagVerbose SyscallOpts = C.F_VERBOSE - FlagExt SyscallOpts = C.F_EXT - FlagDenyNS SyscallOpts = C.F_DENY_NS - FlagDenyTTY SyscallOpts = C.F_DENY_TTY + 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 SyscallOpts = C.F_LINUX32 - FlagCan SyscallOpts = C.F_CAN + // 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 )