From c8f872cc9bce316fd926171b4aec8460890eace2 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/export.go | 72 ++++++++++++++++++++ helper/seccomp/export_test.go | 123 ++++++++++++++++++++++++++++++++++ helper/seccomp/seccomp.go | 20 ++++-- 3 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 helper/seccomp/export_test.go diff --git a/helper/seccomp/export.go b/helper/seccomp/export.go index 1bcb714..394365c 100644 --- a/helper/seccomp/export.go +++ b/helper/seccomp/export.go @@ -1,10 +1,17 @@ package seccomp import ( + "errors" "io" + "io/fs" "os" + "runtime" + "sync" + "sync/atomic" + "syscall" ) +// Export returns a temporary file containing a bpf program emitted by libseccomp. func Export(opts SyscallOpts) (f *os.File, err error) { if f, err = tmpfile(); err != nil { return @@ -15,3 +22,68 @@ func Export(opts SyscallOpts) (f *os.File, err error) { _, err = f.Seek(0, io.SeekStart) return } + +/* +An Exporter writes a BPF program to an output stream. + +Methods of Exporter are not safe for concurrent use. + +An Exporter must not be copied after first use. +*/ +type Exporter struct { + opts SyscallOpts + r, w *os.File + + prepareOnce sync.Once + prepareErr error + closeErr atomic.Pointer[error] + exportErr <-chan error +} + +func (e *Exporter) closeWrite() error { + if !e.closeErr.CompareAndSwap(nil, &fs.ErrInvalid) { + return *e.closeErr.Load() + } + err := e.w.Close() + e.closeErr.Store(&err) + return err +} + +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).Close) + }) + return e.prepareErr +} + +func (e *Exporter) Read(p []byte) (n int, err error) { + if err = e.prepare(); err != nil { + return + } + return e.r.Read(p) +} + +func (e *Exporter) Close() error { + if e.r == nil { + return syscall.EINVAL + } + + runtime.SetFinalizer(e, nil) + + // this hangs if the cgo thread fails to exit + return errors.Join(e.closeWrite(), <-e.exportErr) +} + +func New(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..c3961f5 --- /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 after incomplete 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 )