diff --git a/container/seccomp/hash_test.go b/container/seccomp/hash_test.go index 24f6b1c..5c99558 100644 --- a/container/seccomp/hash_test.go +++ b/container/seccomp/hash_test.go @@ -1,6 +1,7 @@ package seccomp_test import ( + "crypto/sha512" "encoding/hex" "hakurei.app/container/bits" @@ -12,18 +13,18 @@ type ( seccomp.ExportFlag bits.FilterPreset } - bpfLookup map[bpfPreset][]byte + bpfLookup map[bpfPreset][sha512.Size]byte ) -func toHash(s string) []byte { - if len(s) != 128 { +func toHash(s string) [sha512.Size]byte { + if len(s) != sha512.Size*2 { panic("bad sha512 string length") } if v, err := hex.DecodeString(s); err != nil { panic(err.Error()) - } else if len(v) != 64 { + } else if len(v) != sha512.Size { panic("unreachable") } else { - return v + return ([sha512.Size]byte)(v) } } diff --git a/container/seccomp/libseccomp-helper.c b/container/seccomp/libseccomp-helper.c index b09c3eb..e5980d7 100644 --- a/container/seccomp/libseccomp-helper.c +++ b/container/seccomp/libseccomp-helper.c @@ -9,14 +9,16 @@ #define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) -int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, - uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags) { +int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, + uint32_t arch, uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags) { int i; int last_allowed_family; int disallowed; struct hakurei_syscall_rule *rule; + void *buf; + size_t len = 0; int32_t res = 0; /* refer to resPrefix for message */ @@ -108,14 +110,26 @@ int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, 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) { + if (allocate_p == 0) { *ret_p = seccomp_load(ctx); if (*ret_p != 0) { res = 7; goto out; } } else { - *ret_p = seccomp_export_bpf(ctx, fd); + *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len); + if (*ret_p != 0) { + res = 6; + goto out; + } + + buf = hakurei_scmp_allocate(allocate_p, len); + if (buf == NULL) { + res = 4; + goto out; + } + + *ret_p = seccomp_export_bpf_mem(ctx, buf, &len); if (*ret_p != 0) { res = 6; goto out; diff --git a/container/seccomp/libseccomp-helper.h b/container/seccomp/libseccomp-helper.h index 330fc99..0fad2d4 100644 --- a/container/seccomp/libseccomp-helper.h +++ b/container/seccomp/libseccomp-helper.h @@ -18,7 +18,8 @@ struct hakurei_syscall_rule { struct scmp_arg_cmp *arg; }; -int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, - uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags); \ No newline at end of file +extern void *hakurei_scmp_allocate(uintptr_t f, size_t len); +int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, + uint32_t arch, uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags); \ No newline at end of file diff --git a/container/seccomp/libseccomp.go b/container/seccomp/libseccomp.go index 21236a0..2044a66 100644 --- a/container/seccomp/libseccomp.go +++ b/container/seccomp/libseccomp.go @@ -3,7 +3,7 @@ package seccomp /* #cgo linux pkg-config: --static libseccomp -#include +#include "libseccomp-helper.h" #include */ import "C" @@ -11,24 +11,22 @@ import ( "errors" "fmt" "runtime" + "runtime/cgo" "syscall" "unsafe" ) -const ( - PER_LINUX = C.PER_LINUX - PER_LINUX32 = C.PER_LINUX32 -) - -var ( - ErrInvalidRules = errors.New("invalid native rules slice") -) +// ErrInvalidRules is returned for a zero-length rules slice. +var ErrInvalidRules = errors.New("invalid native rules slice") // LibraryError represents a libseccomp error. type LibraryError struct { - Prefix string + // User facing description of the libseccomp function returning the error. + Prefix string + // Negated errno value returned by libseccomp. Seccomp syscall.Errno - Errno error + // Global errno value on return. + Errno error } func (e *LibraryError) Error() string { @@ -56,8 +54,10 @@ func (e *LibraryError) Is(err error) bool { } type ( + // ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall]. ScmpSyscall = C.int - ScmpErrno = C.int + // ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno]. + ScmpErrno = C.int ) // A NativeRule specifies an arch-specific action taken by seccomp under certain conditions. @@ -88,12 +88,23 @@ var resPrefix = [...]string{ 3: "seccomp_arch_add failed (multiarch)", 4: "internal libseccomp failure", 5: "seccomp_rule_add failed", - 6: "seccomp_export_bpf failed", + 6: "seccomp_export_bpf_mem failed", 7: "seccomp_load failed", } -// Export streams filter contents to fd, or installs it to the current process if fd < 0. -func Export(fd int, rules []NativeRule, flags ExportFlag) error { +// cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter +// which allocates the buffer that the resulting bpf program is copied into, and writes its slice header +// to a value held by the caller. +type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer) + +//export hakurei_scmp_allocate +func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) { + return cgo.Handle(f).Value().(cbAllocateBuffer)(len) +} + +// makeFilter generates a bpf program from a slice of [NativeRule] and writes the resulting byte slice to p. +// The filter is installed to the current process if p is nil. +func makeFilter(rules []NativeRule, flags ExportFlag, p *[]byte) error { if len(rules) == 0 { return ErrInvalidRules } @@ -117,33 +128,56 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error { var ret C.int - var rulesPinner runtime.Pinner + var scmpPinner runtime.Pinner for i := range rules { rule := &rules[i] - rulesPinner.Pin(rule) + scmpPinner.Pin(rule) if rule.Arg != nil { - rulesPinner.Pin(rule.Arg) + scmpPinner.Pin(rule.Arg) } } - res, err := C.hakurei_export_filter( - &ret, C.int(fd), + + var allocateP cgo.Handle + if p != nil { + allocateP = cgo.NewHandle(func(len C.size_t) (buf unsafe.Pointer) { + // this is so the slice header gets a Go pointer + *p = make([]byte, len) + + buf = unsafe.Pointer(unsafe.SliceData(*p)) + scmpPinner.Pin(buf) + return + }) + } + + res, err := C.hakurei_scmp_make_filter( + &ret, C.uintptr_t(allocateP), arch, multiarch, (*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), C.size_t(len(rules)), flags, ) - rulesPinner.Unpin() + scmpPinner.Unpin() + if p != nil { + allocateP.Delete() + } if prefix := resPrefix[res]; prefix != "" { - return &LibraryError{ - prefix, - -syscall.Errno(ret), - err, - } + return &LibraryError{prefix, syscall.Errno(-ret), err} } return err } +// Export generates a bpf program from a slice of [NativeRule]. +// Errors returned by libseccomp is wrapped in [LibraryError]. +func Export(rules []NativeRule, flags ExportFlag) (data []byte, err error) { + err = makeFilter(rules, flags, &data) + return +} + +// Load generates a bpf program from a slice of [NativeRule] and enforces it on the current process. +// Errors returned by libseccomp is wrapped in [LibraryError]. +func Load(rules []NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) } + // ScmpCompare is the equivalent of scmp_compare; // Comparison operators type ScmpCompare = C.enum_scmp_compare @@ -184,7 +218,15 @@ type ScmpArgCmp struct { DatumA, DatumB ScmpDatum } -// only used for testing +const ( + // PersonaLinux is passed in a [ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY. + PersonaLinux = C.PER_LINUX + // PersonaLinux32 is passed in a [ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY. + PersonaLinux32 = C.PER_LINUX32 +) + +// syscallResolveName resolves a syscall number by name via seccomp_syscall_resolve_name. +// This function is only for testing the lookup tables and included here for convenience. func syscallResolveName(s string) (trap int) { v := C.CString(s) trap = int(C.seccomp_syscall_resolve_name(v)) diff --git a/container/seccomp/libseccomp_test.go b/container/seccomp/libseccomp_test.go index d947105..5d58ac7 100644 --- a/container/seccomp/libseccomp_test.go +++ b/container/seccomp/libseccomp_test.go @@ -3,8 +3,6 @@ package seccomp_test import ( "crypto/sha512" "errors" - "io" - "slices" "syscall" "testing" @@ -12,6 +10,67 @@ import ( . "hakurei.app/container/seccomp" ) +func TestLibraryError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + sample *LibraryError + want string + wantIs bool + compare error + }{ + { + "full", + &LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + "seccomp_export_bpf failed: operation canceled (bad file descriptor)", + true, + &LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + }, + { + "errno only", + &LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM}, + "seccomp_init failed: cannot allocate memory", + false, + nil, + }, + { + "seccomp only", + &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) { + t.Parallel() + + 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) { + t.Parallel() + + wantPanic := "invalid libseccomp error" + defer func() { + if r := recover(); r != wantPanic { + t.Errorf("panic: %q, want %q", r, wantPanic) + } + }() + _ = new(LibraryError).Error() + }) +} + func TestExport(t *testing.T) { t.Parallel() @@ -38,61 +97,34 @@ func TestExport(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - e := New(Preset(tc.presets, tc.flags), tc.flags) want := bpfExpected[bpfPreset{tc.flags, tc.presets}] - digest := sha512.New() - - if _, err := io.Copy(digest, e); (err != nil) != tc.wantErr { - t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr) + if data, err := Export(Preset(tc.presets, tc.flags), tc.flags); (err != nil) != tc.wantErr { + t.Errorf("Export: error = %v, wantErr %v", err, tc.wantErr) return - } - if err := e.Close(); err != nil { - t.Errorf("Close: error = %v", err) - } - if got := digest.Sum(nil); !slices.Equal(got, want) { - t.Fatalf("Export: hash = %x, want %x", - got, want) + } else if got := sha512.Sum512(data); got != want { + t.Fatalf("Export: hash = %x, want %x", got, want) return } }) } - - t.Run("close without use", func(t *testing.T) { - e := New(Preset(0, 0), 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 := New(Preset(0, 0), 0) - if _, err := e.Read(nil); err != nil { - t.Errorf("Read: error = %v", err) - return - } - // the underlying implementation uses buffered io, so the outcome of this is nondeterministic; - // that is not harmful however, so both outcomes are checked for here - 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) + const exportFlags = AllowMultiarch | AllowCAN | AllowBluetooth + const presetFlags = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel | PresetLinux32 + var want = bpfExpected[bpfPreset{exportFlags, presetFlags}] + for b.Loop() { - e := New( - Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, - AllowMultiarch|AllowCAN|AllowBluetooth), - AllowMultiarch|AllowCAN|AllowBluetooth) - if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { - b.Fatalf("cannot export: %v", err) + data, err := Export(Preset(presetFlags, exportFlags), exportFlags) + + b.StopTimer() + if err != nil { + b.Fatalf("Export: error = %v", err) } - if err := e.Close(); err != nil { - b.Fatalf("cannot close exporter: %v", err) + if got := sha512.Sum512(data); got != want { + b.Fatalf("Export: hash = %x, want %x", got, want) + return } + b.StartTimer() } } diff --git a/container/seccomp/presets.go b/container/seccomp/presets.go index abd3d5c..f12d9f7 100644 --- a/container/seccomp/presets.go +++ b/container/seccomp/presets.go @@ -9,9 +9,9 @@ import ( ) func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) { - allowedPersonality := PER_LINUX + allowedPersonality := PersonaLinux if presets&bits.PresetLinux32 != 0 { - allowedPersonality = PER_LINUX32 + allowedPersonality = PersonaLinux32 } presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) diff --git a/container/seccomp/proc.go b/container/seccomp/proc.go deleted file mode 100644 index fb6f543..0000000 --- a/container/seccomp/proc.go +++ /dev/null @@ -1,72 +0,0 @@ -package seccomp - -import ( - "context" - "errors" - "syscall" - - "hakurei.app/helper/proc" -) - -// New returns an inactive Encoder instance. -func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} } - -// Load loads a filter into the kernel. -func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) } - -/* -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(rules []NativeRule, flags ExportFlag) proc.File { - return &File{rules: rules, flags: flags} -} - -// File implements [proc.File] and provides access to the read end of exporter pipe. -type File struct { - rules []NativeRule - flags ExportFlag - proc.BaseFile -} - -func (f *File) ErrCount() int { return 2 } -func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error { - e := newExporter(f.rules, f.flags) - 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 -} diff --git a/container/seccomp/seccomp.go b/container/seccomp/seccomp.go deleted file mode 100644 index 664b31c..0000000 --- a/container/seccomp/seccomp.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package seccomp provides high level wrappers around libseccomp. -package seccomp - -import ( - "os" - "runtime" - "sync" -) - -type exporter struct { - rules []NativeRule - flags ExportFlag - 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 <- Export(int(fd), e.rules, e.flags) - 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(rules []NativeRule, flags ExportFlag) *exporter { - return &exporter{rules: rules, flags: flags} -} diff --git a/container/seccomp/seccomp_test.go b/container/seccomp/seccomp_test.go deleted file mode 100644 index c19f2c4..0000000 --- a/container/seccomp/seccomp_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package seccomp_test - -import ( - "errors" - "runtime" - "syscall" - "testing" - - "hakurei.app/container/seccomp" -) - -func TestLibraryError(t *testing.T) { - t.Parallel() - - 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) { - t.Parallel() - - 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) { - t.Parallel() - - 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()) - }) -}