aboutsummaryrefslogtreecommitdiffhomepage
path: root/container
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-13 18:49:58 +0900
committerOphestra <cat@gensokyo.uk>2025-10-13 18:51:35 +0900
commit123d7fbfd5a2955dc5042c0a85211e376d5a8e65 (patch)
tree7e03e35309083b81873f3cf1939d2f4d27d41659 /container
parent7638a44fa613f23434318bdf136723aa010e8638 (diff)
container/seccomp: remove export pipe
This was only useful when wrapping bwrap. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container')
-rw-r--r--container/seccomp/hash_test.go11
-rw-r--r--container/seccomp/libseccomp-helper.c26
-rw-r--r--container/seccomp/libseccomp-helper.h9
-rw-r--r--container/seccomp/libseccomp.go96
-rw-r--r--container/seccomp/libseccomp_test.go124
-rw-r--r--container/seccomp/presets.go4
-rw-r--r--container/seccomp/proc.go72
-rw-r--r--container/seccomp/seccomp.go60
-rw-r--r--container/seccomp/seccomp_test.go71
9 files changed, 180 insertions, 293 deletions
diff --git a/container/seccomp/hash_test.go b/container/seccomp/hash_test.go
index 24f6b1c7..5c995582 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 b09c3eb2..e5980d7e 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 330fc99b..0fad2d4a 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 21236a06..2044a668 100644
--- a/container/seccomp/libseccomp.go
+++ b/container/seccomp/libseccomp.go
@@ -3,7 +3,7 @@ package seccomp
/*
#cgo linux pkg-config: --static libseccomp
-#include <libseccomp-helper.h>
+#include "libseccomp-helper.h"
#include <sys/personality.h>
*/
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 d947105c..5d58ac7a 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 abd3d5cd..f12d9f76 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 fb6f5430..00000000
--- 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 664b31c6..00000000
--- 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 c19f2c49..00000000
--- 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())
- })
-}