From 5b7b3fa9a4d8ca4ad08d6648752e28ee66f0ffae Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 3 Feb 2025 18:10:29 +0900 Subject: [PATCH] helper/seccomp: implement reader interface via pipe This also does not require the libc tmpfile call. BPF programs emitted by libseccomp seems to be deterministic. The tests would catch regressions as it verifies the program against known good output backed by manual testing. Signed-off-by: Ophestra --- helper/seccomp/api.go | 51 ++++++++++++++ helper/seccomp/export.go | 59 +++++++++++++--- helper/seccomp/export_test.go | 123 ++++++++++++++++++++++++++++++++++ helper/seccomp/seccomp.go | 20 ++++-- 4 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 helper/seccomp/api.go create mode 100644 helper/seccomp/export_test.go 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 )