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 <cat@gensokyo.uk>
This commit is contained in:
parent
d58fb8c6ee
commit
5b7b3fa9a4
51
helper/seccomp/api.go
Normal file
51
helper/seccomp/api.go
Normal file
@ -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)}
|
||||
}
|
@ -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}
|
||||
}
|
||||
|
123
helper/seccomp/export_test.go
Normal file
123
helper/seccomp/export_test.go
Normal file
@ -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
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user