aboutsummaryrefslogtreecommitdiffhomepage
path: root/container/seccomp/libseccomp.go
blob: 962d68444b1f042a0b08935bfe5f491ff7626ab6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package seccomp

/*
#cgo linux pkg-config: --static libseccomp

#include "libseccomp-helper.h"
#include <sys/personality.h>
*/
import "C"
import (
	"errors"
	"fmt"
	"runtime"
	"runtime/cgo"
	"syscall"
	"unsafe"

	"hakurei.app/container/std"
)

// 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 {
	// User facing description of the libseccomp function returning the error.
	Prefix string
	// Negated errno value returned by libseccomp.
	Seccomp syscall.Errno
	// Global errno value on return.
	Errno error
}

func (e *LibraryError) Error() string {
	if e.Seccomp == 0 {
		if e.Errno == nil {
			panic("invalid libseccomp error")
		}
		return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
	}
	if e.Errno == nil {
		return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
	}
	return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
}

func (e *LibraryError) Is(err error) bool {
	if e == nil {
		return err == nil
	}
	if ef, ok := err.(*LibraryError); ok {
		return *e == *ef
	}
	return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
		(e.Errno != nil && errors.Is(err, e.Errno))
}

type (
	// scmpUint is equivalent to [std.ScmpUint].
	scmpUint = C.uint
	// scmpInt is equivalent to [std.ScmpInt].
	scmpInt = C.int

	// syscallRule is equivalent to [std.NativeRule].
	syscallRule = C.struct_hakurei_syscall_rule
)

// ExportFlag configures filter behaviour that are not implemented as rules.
type ExportFlag = C.hakurei_export_flag

const (
	// AllowMultiarch allows multiarch/emulation.
	AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH
	// AllowCAN allows AF_CAN.
	AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN
	// AllowBluetooth allows AF_BLUETOOTH.
	AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH
)

var resPrefix = [...]string{
	0: "",
	1: "seccomp_init failed",
	2: "seccomp_arch_add failed",
	3: "seccomp_arch_add failed (multiarch)",
	4: "internal libseccomp failure",
	5: "seccomp_rule_add failed",
	6: "seccomp_export_bpf_mem failed",
	7: "seccomp_load failed",
}

// 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 [std.NativeRule] and writes the resulting byte slice to p.
// The filter is installed to the current process if p is nil.
func makeFilter(rules []std.NativeRule, flags ExportFlag, p *[]byte) error {
	if len(rules) == 0 {
		return ErrInvalidRules
	}

	var (
		arch      C.uint32_t = 0
		multiarch C.uint32_t = 0
	)
	switch runtime.GOARCH {
	case "386":
		arch = C.SCMP_ARCH_X86
	case "amd64":
		arch = C.SCMP_ARCH_X86_64
		multiarch = C.SCMP_ARCH_X86
	case "arm":
		arch = C.SCMP_ARCH_ARM
	case "arm64":
		arch = C.SCMP_ARCH_AARCH64
		multiarch = C.SCMP_ARCH_ARM
	}

	var ret C.int

	var scmpPinner runtime.Pinner
	for i := range rules {
		rule := &rules[i]
		scmpPinner.Pin(rule)
		if rule.Arg != nil {
			scmpPinner.Pin(rule.Arg)
		}
	}

	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,
		(*syscallRule)(unsafe.Pointer(&rules[0])),
		C.size_t(len(rules)),
		flags,
	)
	scmpPinner.Unpin()
	if p != nil {
		allocateP.Delete()
	}

	if prefix := resPrefix[res]; prefix != "" {
		return &LibraryError{prefix, syscall.Errno(-ret), err}
	}
	return err
}

// Export generates a bpf program from a slice of [std.NativeRule].
// Errors returned by libseccomp is wrapped in [LibraryError].
func Export(rules []std.NativeRule, flags ExportFlag) (data []byte, err error) {
	err = makeFilter(rules, flags, &data)
	return
}

// Load generates a bpf program from a slice of [std.NativeRule] and enforces it on the current process.
// Errors returned by libseccomp is wrapped in [LibraryError].
func Load(rules []std.NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) }

type (
	// Comparison operators.
	scmpCompare = C.enum_scmp_compare

	// Argument datum.
	scmpDatum = C.scmp_datum_t

	// Argument / Value comparison definition.
	scmpArgCmp = C.struct_scmp_arg_cmp
)

const (
	_SCMP_CMP_MIN = C._SCMP_CMP_MIN

	// not equal
	SCMP_CMP_NE = C.SCMP_CMP_NE
	// less than
	SCMP_CMP_LT = C.SCMP_CMP_LT
	// less than or equal
	SCMP_CMP_LE = C.SCMP_CMP_LE
	// equal
	SCMP_CMP_EQ = C.SCMP_CMP_EQ
	// greater than or equal
	SCMP_CMP_GE = C.SCMP_CMP_GE
	// greater than
	SCMP_CMP_GT = C.SCMP_CMP_GT
	// masked equality
	SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ

	_SCMP_CMP_MAX = C._SCMP_CMP_MAX
)

const (
	// PersonaLinux is passed in a [std.ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
	PersonaLinux = C.PER_LINUX
	// PersonaLinux32 is passed in a [std.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, ok bool) {
	v := C.CString(s)
	trap = int(C.seccomp_syscall_resolve_name(v))
	C.free(unsafe.Pointer(v))
	ok = trap != C.__NR_SCMP_ERROR
	return
}