container/seccomp: remove export pipe
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m32s

This was only useful when wrapping bwrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-10-13 18:49:58 +09:00
parent 7638a44fa6
commit 123d7fbfd5
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
9 changed files with 180 additions and 293 deletions

View File

@ -1,6 +1,7 @@
package seccomp_test package seccomp_test
import ( import (
"crypto/sha512"
"encoding/hex" "encoding/hex"
"hakurei.app/container/bits" "hakurei.app/container/bits"
@ -12,18 +13,18 @@ type (
seccomp.ExportFlag seccomp.ExportFlag
bits.FilterPreset bits.FilterPreset
} }
bpfLookup map[bpfPreset][]byte bpfLookup map[bpfPreset][sha512.Size]byte
) )
func toHash(s string) []byte { func toHash(s string) [sha512.Size]byte {
if len(s) != 128 { if len(s) != sha512.Size*2 {
panic("bad sha512 string length") panic("bad sha512 string length")
} }
if v, err := hex.DecodeString(s); err != nil { if v, err := hex.DecodeString(s); err != nil {
panic(err.Error()) panic(err.Error())
} else if len(v) != 64 { } else if len(v) != sha512.Size {
panic("unreachable") panic("unreachable")
} else { } else {
return v return ([sha512.Size]byte)(v)
} }
} }

View File

@ -9,14 +9,16 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t multiarch, uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) { size_t rules_sz, hakurei_export_flag flags) {
int i; int i;
int last_allowed_family; int last_allowed_family;
int disallowed; int disallowed;
struct hakurei_syscall_rule *rule; struct hakurei_syscall_rule *rule;
void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */ 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, seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (fd < 0) { if (allocate_p == 0) {
*ret_p = seccomp_load(ctx); *ret_p = seccomp_load(ctx);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 7; res = 7;
goto out; goto out;
} }
} else { } 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) { if (*ret_p != 0) {
res = 6; res = 6;
goto out; goto out;

View File

@ -18,7 +18,8 @@ struct hakurei_syscall_rule {
struct scmp_arg_cmp *arg; struct scmp_arg_cmp *arg;
}; };
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
uint32_t multiarch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags); size_t rules_sz, hakurei_export_flag flags);

View File

@ -3,7 +3,7 @@ package seccomp
/* /*
#cgo linux pkg-config: --static libseccomp #cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h> #include "libseccomp-helper.h"
#include <sys/personality.h> #include <sys/personality.h>
*/ */
import "C" import "C"
@ -11,23 +11,21 @@ import (
"errors" "errors"
"fmt" "fmt"
"runtime" "runtime"
"runtime/cgo"
"syscall" "syscall"
"unsafe" "unsafe"
) )
const ( // ErrInvalidRules is returned for a zero-length rules slice.
PER_LINUX = C.PER_LINUX var ErrInvalidRules = errors.New("invalid native rules slice")
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// LibraryError represents a libseccomp error. // LibraryError represents a libseccomp error.
type LibraryError struct { type LibraryError struct {
// User facing description of the libseccomp function returning the error.
Prefix string Prefix string
// Negated errno value returned by libseccomp.
Seccomp syscall.Errno Seccomp syscall.Errno
// Global errno value on return.
Errno error Errno error
} }
@ -56,7 +54,9 @@ func (e *LibraryError) Is(err error) bool {
} }
type ( type (
// ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall].
ScmpSyscall = C.int ScmpSyscall = C.int
// ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno].
ScmpErrno = C.int ScmpErrno = C.int
) )
@ -88,12 +88,23 @@ var resPrefix = [...]string{
3: "seccomp_arch_add failed (multiarch)", 3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure", 4: "internal libseccomp failure",
5: "seccomp_rule_add failed", 5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed", 6: "seccomp_export_bpf_mem failed",
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
// Export streams filter contents to fd, or installs it to the current process if fd < 0. // cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter
func Export(fd int, rules []NativeRule, flags ExportFlag) error { // 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 { if len(rules) == 0 {
return ErrInvalidRules return ErrInvalidRules
} }
@ -117,33 +128,56 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int var ret C.int
var rulesPinner runtime.Pinner var scmpPinner runtime.Pinner
for i := range rules { for i := range rules {
rule := &rules[i] rule := &rules[i]
rulesPinner.Pin(rule) scmpPinner.Pin(rule)
if rule.Arg != nil { 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, arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), (*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)), C.size_t(len(rules)),
flags, flags,
) )
rulesPinner.Unpin() scmpPinner.Unpin()
if p != nil {
allocateP.Delete()
}
if prefix := resPrefix[res]; prefix != "" { if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{ return &LibraryError{prefix, syscall.Errno(-ret), err}
prefix,
-syscall.Errno(ret),
err,
}
} }
return 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; // ScmpCompare is the equivalent of scmp_compare;
// Comparison operators // Comparison operators
type ScmpCompare = C.enum_scmp_compare type ScmpCompare = C.enum_scmp_compare
@ -184,7 +218,15 @@ type ScmpArgCmp struct {
DatumA, DatumB ScmpDatum 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) { func syscallResolveName(s string) (trap int) {
v := C.CString(s) v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v)) trap = int(C.seccomp_syscall_resolve_name(v))

View File

@ -3,8 +3,6 @@ package seccomp_test
import ( import (
"crypto/sha512" "crypto/sha512"
"errors" "errors"
"io"
"slices"
"syscall" "syscall"
"testing" "testing"
@ -12,6 +10,67 @@ import (
. "hakurei.app/container/seccomp" . "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) { func TestExport(t *testing.T) {
t.Parallel() t.Parallel()
@ -38,61 +97,34 @@ func TestExport(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}] want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New() 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)
if _, err := io.Copy(digest, e); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return return
} } else if got := sha512.Sum512(data); got != want {
if err := e.Close(); err != nil { t.Fatalf("Export: hash = %x, want %x", got, want)
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export: hash = %x, want %x",
got, want)
return 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) { 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() { for b.Loop() {
e := New( data, err := Export(Preset(presetFlags, exportFlags), exportFlags)
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), b.StopTimer()
AllowMultiarch|AllowCAN|AllowBluetooth) if err != nil {
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { b.Fatalf("Export: error = %v", err)
b.Fatalf("cannot export: %v", err)
} }
if err := e.Close(); err != nil { if got := sha512.Sum512(data); got != want {
b.Fatalf("cannot close exporter: %v", err) b.Fatalf("Export: hash = %x, want %x", got, want)
return
} }
b.StartTimer()
} }
} }

View File

@ -9,9 +9,9 @@ import (
) )
func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) { func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX allowedPersonality := PersonaLinux
if presets&bits.PresetLinux32 != 0 { if presets&bits.PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32 allowedPersonality = PersonaLinux32
} }
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))

View File

@ -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
}

View File

@ -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}
}

View File

@ -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())
})
}