Skip to content

Commit

Permalink
all: add seccomp userspace notification API to libseccomp-golang
Browse files Browse the repository at this point in the history
This commit adds the seccomp userspace notification API
present in version >= 2.5.0 of the libseccomp library.

This API allows userspace to get a notification when a filter
configured with a notification action triggers. The trigger suspends
processing of the syscall until the notification is delivered to
userspace and acknowledged back.

To support the implementation, the following changes were necessary:

- Added package init function to ensure libseccomp is properly initialized.  It
  calls GetApi() in order to initialize the cgo libseccomp API level. This is
  necessary in order for libseccomp to properly handle other libseccomp APIs.

- Fix errors reported by go vet such as "can't check non-constant format in
  call to Sprintf"

This patch includes updated test updates for the new feature:

- The Travis CI pipeline was previously running the tests on libseccomp from
  Ubuntu Bionic. This patch adds a matrix to test on various libseccomp
  versions (2.2.1, 2.4.4, 2.5.0). This is to check we don't break compatibility
  with older versions and return errors appropriately when running on an old
  version without seccomp notify support. This is necessary for downstream
  projects like runc that keeps support for CentOS 7. This uses PKG_CONFIG_PATH
  and LD_LIBRARY_PATH to compile and run the tests with different versions of
  libseccomp installed in a prefix. This also splits 'make check' into two
  separate 'make' commands, so that the tests run even if the vet fails.

- Introduce execInSubprocess to run each test in a new process. This is because
  the kernel does not allow to remove a seccomp filters from a process, so a
  process cannot be reused for a subsequent test. Logs from subprocesses
  are read from the parent process and printed with the appropriate
  indentation.

- Fix seccomp TestLogAct. This test was written in such as way that once the
  filter was loaded, it blocked almost all system calls, thereby making
  disabling the filter impossible and sometimes causing the Go runtime to fail
  to allocate memory. This fix simplifies the test and fixes these isuses.

- Test timeout is reduced to a reasonable limit to help detect freezes as
  explained in the Makefile.

- The main test for seccomp notification mechanism: TestNotif The test works
  with a couple of goroutines. One goroutine configures a seccomp filter with
  the notification action and generates syscalls that trigger the action. The
  other goroutine acts as a notifcation handler, verifies that the notification
  received from the kernel is correct, and generates an appropriate response.

- An additional test for seccomp notification when it is not supported:
  TestNotifUnsupported. This gets tested on older kernels or with older
  libseccomp.

- Handle the case where libseccomp returns EINTR or ENOENT, as reported
  here: seccomp/libseccomp#302.

This patch is based on initial work in PR 50 by:
- Cesar Talledo <ctalledo@nestybox.com>
- Rodny Molina <rmolina@nestybox.com>

Co-authored-by: Rodrigo Campos <rodrigo@kinvolk.io>
Signed-off-by: Alban Crequy <alban@kinvolk.io>
Signed-off-by: Rodrigo Campos <rodrigo@kinvolk.io>
Acked-by: Tom Hromatka <tom.hromatka@oracle.com>
Signed-off-by: Paul Moore <paul@paul-moore.com>
  • Loading branch information
2 people authored and pcmoore committed Apr 29, 2021
1 parent 541420d commit 78c92cb
Show file tree
Hide file tree
Showing 5 changed files with 732 additions and 62 deletions.
28 changes: 24 additions & 4 deletions .travis.yml
Expand Up @@ -19,19 +19,39 @@ os:

language: go

jobs:
include:
- name: "last libseccomp 2.5.0"
env:
- SECCOMP_VER=2.5.0
- SECCOMP_SHA256SUM=1ffa7038d2720ad191919816db3479295a4bcca1ec14e02f672539f4983014f3
- name: "compat libseccomp 2.4.4"
env:
- SECCOMP_VER=2.4.4
- SECCOMP_SHA256SUM=4e79738d1ef3c9b7ca9769f1f8b8d84fc17143c2c1c432e53b9c64787e0ff3eb
- name: "compat libseccomp 2.2.1"
env:
- SECCOMP_VER=2.2.1
- SECCOMP_SHA256SUM=0ba1789f54786c644af54cdffc9fd0dd0a8bb2b2ee153933f658855d2851a740

addons:
apt:
packages:
- build-essential
# TODO: use the main libseccomp git repo instead of a distro package
- libseccomp2
- libseccomp-dev
- astyle
- golint
- gperf

install:
- go get -u golang.org/x/lint/golint

# run all of the tests independently, fail if any of the tests error
script:
- wget https://github.com/seccomp/libseccomp/releases/download/v$SECCOMP_VER/libseccomp-$SECCOMP_VER.tar.gz
- echo $SECCOMP_SHA256SUM libseccomp-$SECCOMP_VER.tar.gz | sha256sum -c
- tar xf libseccomp-$SECCOMP_VER.tar.gz
- pushd libseccomp-$SECCOMP_VER && ./configure --prefix=/opt/libseccomp-$SECCOMP_VER && make && sudo make install && popd
- make check-syntax
- make lint
- make check
- PKG_CONFIG_PATH=/opt/libseccomp-$SECCOMP_VER/lib/pkgconfig LD_LIBRARY_PATH=/opt/libseccomp-$SECCOMP_VER/lib make vet
- PKG_CONFIG_PATH=/opt/libseccomp-$SECCOMP_VER/lib/pkgconfig LD_LIBRARY_PATH=/opt/libseccomp-$SECCOMP_VER/lib make test
17 changes: 16 additions & 1 deletion Makefile
Expand Up @@ -18,8 +18,23 @@ fix-syntax:
vet:
go vet -v

# Previous bugs have made the tests freeze until the timeout. Golang default
# timeout for tests is 10 minutes, which is too long, considering current tests
# can be executed in less than 1 second. Reduce the timeout, so problems can
# be noticed earlier in the CI.
TEST_TIMEOUT=10s

# Some tests run with SetTsync(false) and some tests with SetTsync(true). Once
# the threads are not using the same seccomp filters anymore, the kernel will
# refuse to use Tsync, causing next tests to fail. This issue could be left
# unnoticed if the test with SetTsync(false) is executed last.
#
# Run tests twice ensure that no test leave the testing process in a state
# unable to run following tests, regardless of the subset of tests selected.
TEST_COUNT=2

test:
go test -v
go test -v -timeout $(TEST_TIMEOUT) -count $(TEST_COUNT)

lint:
@$(if $(shell which golint),true,$(error "install golint and include it in your PATH"))
Expand Down
186 changes: 165 additions & 21 deletions seccomp.go
Expand Up @@ -20,6 +20,13 @@ import (

// C wrapping code

// To compile libseccomp-golang against a specific version of libseccomp:
// cd ../libseccomp && mkdir -p prefix
// ./configure --prefix=$PWD/prefix && make && make install
// cd ../libseccomp-golang
// PKG_CONFIG_PATH=$PWD/../libseccomp/prefix/lib/pkgconfig/ make
// LD_PRELOAD=$PWD/../libseccomp/prefix/lib/libseccomp.so.2.5.0 PKG_CONFIG_PATH=$PWD/../libseccomp/prefix/lib/pkgconfig/ make test

// #cgo pkg-config: libseccomp
// #include <stdlib.h>
// #include <seccomp.h>
Expand All @@ -34,19 +41,28 @@ type VersionError struct {
minimum string
}

// Caches the libseccomp API level
var apiLevel uint

func init() {
// This forces the cgo libseccomp to initialize its internal API support state,
// which is necessary on older versions of libseccomp in order to work
// correctly.
GetAPI()
}

func (e VersionError) Error() string {
format := "Libseccomp version too low: "
messageStr := ""
if e.message != "" {
format += e.message + ": "
messageStr = e.message + ": "
}
format += "minimum supported is "
minimumStr := ""
if e.minimum != "" {
format += e.minimum + ": "
minimumStr = e.minimum
} else {
format += "2.2.0: "
minimumStr = "2.2.0"
}
format += "detected %d.%d.%d"
return fmt.Sprintf(format, verMajor, verMinor, verMicro)
return fmt.Sprintf("Libseccomp version too low: %sminimum supported is %s: detected %d.%d.%d", messageStr, minimumStr, verMajor, verMinor, verMicro)
}

// ScmpArch represents a CPU architecture. Seccomp can restrict syscalls on a
Expand All @@ -69,9 +85,61 @@ type ScmpCondition struct {
Operand2 uint64 `json:"operand_two,omitempty"`
}

// ScmpSyscall represents a Linux System Call
// Seccomp userspace notification structures associated with filters that use the ActNotify action.

// ScmpSyscall identifies a Linux System Call by its number.
type ScmpSyscall int32

// ScmpFd represents a file-descriptor used for seccomp userspace notifications.
type ScmpFd int32

// ScmpNotifData describes the system call context that triggered a notification.
//
// Syscall: the syscall number
// Arch: the filter architecture
// InstrPointer: address of the instruction that triggered a notification
// Args: arguments (up to 6) for the syscall
//
type ScmpNotifData struct {
Syscall ScmpSyscall `json:"syscall,omitempty"`
Arch ScmpArch `json:"arch,omitempty"`
InstrPointer uint64 `json:"instr_pointer,omitempty"`
Args []uint64 `json:"args,omitempty"`
}

// ScmpNotifReq represents a seccomp userspace notification. See NotifReceive() for
// info on how to pull such a notification.
//
// ID: notification ID
// Pid: process that triggered the notification event
// Flags: filter flags (see seccomp(2))
// Data: system call context that triggered the notification
//
type ScmpNotifReq struct {
ID uint64 `json:"id,omitempty"`
Pid uint32 `json:"pid,omitempty"`
Flags uint32 `json:"flags,omitempty"`
Data ScmpNotifData `json:"data,omitempty"`
}

// ScmpNotifResp represents a seccomp userspace notification response. See NotifRespond()
// for info on how to push such a response.
//
// ID: notification ID (must match the corresponding ScmpNotifReq ID)
// Error: must be 0 if no error occurred, or an error constant from package
// syscall (e.g., syscall.EPERM, etc). In the latter case, it's used
// as an error return from the syscall that created the notification.
// Val: return value for the syscall that created the notification. Only
// relevant if Error is 0.
// Flags: userspace notification response flag (e.g., NotifRespFlagContinue)
//
type ScmpNotifResp struct {
ID uint64 `json:"id,omitempty"`
Error int32 `json:"error,omitempty"`
Val uint64 `json:"val,omitempty"`
Flags uint32 `json:"flags,omitempty"`
}

// Exported Constants

const (
Expand Down Expand Up @@ -134,6 +202,9 @@ const (
ActKill ScmpAction = iota
// ActTrap throws SIGSYS
ActTrap ScmpAction = iota
// ActNotify triggers a userspace notification. This action is only usable when
// libseccomp API level 5 or higher is supported.
ActNotify ScmpAction = iota
// ActErrno causes the syscall to return a negative error code. This
// code can be set with the SetReturnCode method
ActErrno ScmpAction = iota
Expand Down Expand Up @@ -191,6 +262,15 @@ var (
ErrSyscallDoesNotExist = fmt.Errorf("could not resolve syscall name")
)

const (
// Userspace notification response flags

// NotifRespFlagContinue tells the kernel to continue executing the system
// call that triggered the notification. Must only be used when the notication
// response's error is 0.
NotifRespFlagContinue uint32 = 1
)

// Helpers for types

// GetArchFromString returns an ScmpArch constant from a string representing an
Expand Down Expand Up @@ -328,6 +408,8 @@ func (a ScmpAction) String() string {
case ActTrace:
return fmt.Sprintf("Action: Notify tracing processes with code %d",
(a >> 16))
case ActNotify:
return "Action: Notify userspace"
case ActLog:
return "Action: Log system call"
case ActAllow:
Expand Down Expand Up @@ -369,7 +451,12 @@ func GetLibraryVersion() (major, minor, micro uint) {
// See the seccomp_api_get(3) man page for details on available API levels:
// https://github.com/seccomp/libseccomp/blob/main/doc/man/man3/seccomp_api_get.3
func GetAPI() (uint, error) {
return getAPI()
api, err := getAPI()
if err != nil {
return api, err
}
apiLevel = api
return api, err
}

// SetAPI forcibly sets the API level. General use of this function is strongly
Expand All @@ -379,7 +466,11 @@ func GetAPI() (uint, error) {
// See the seccomp_api_get(3) man page for details on available API levels:
// https://github.com/seccomp/libseccomp/blob/main/doc/man/man3/seccomp_api_get.3
func SetAPI(api uint) error {
return setAPI(api)
if err := setAPI(api); err != nil {
return err
}
apiLevel = api
return nil
}

// Syscall functions
Expand Down Expand Up @@ -524,11 +615,12 @@ type ScmpFilter struct {
lock sync.Mutex
}

// NewFilter creates and returns a new filter context.
// Accepts a default action to be taken for syscalls which match no rules in
// the filter.
// Returns a reference to a valid filter context, or nil and an error if the
// filter context could not be created or an invalid default action was given.
// NewFilter creates and returns a new filter context. Accepts a default action to be
// taken for syscalls which match no rules in the filter. The newly created filter applies
// to all threads of the calling process. Use SetTsync() to change this behavior prior to
// loading the filter.
// Returns a reference to a valid filter context, or nil and an error
// if the filter context could not be created or an invalid default action was given.
func NewFilter(defaultAction ScmpAction) (*ScmpFilter, error) {
if err := ensureSupportedVersion(); err != nil {
return nil, err
Expand All @@ -548,8 +640,8 @@ func NewFilter(defaultAction ScmpAction) (*ScmpFilter, error) {
filter.valid = true
runtime.SetFinalizer(filter, filterFinalizer)

// Enable TSync so all goroutines will receive the same rules
// If the kernel does not support TSYNC, allow us to continue without error
// Enable TSync so all goroutines will receive the same rules.
// If the kernel does not support TSYNC, allow us to continue without error.
if err := filter.setFilterAttr(filterAttrTsync, 0x1); err != nil && err != syscall.ENOTSUP {
filter.Release()
return nil, fmt.Errorf("could not create filter - error setting tsync bit: %v", err)
Expand All @@ -558,6 +650,27 @@ func NewFilter(defaultAction ScmpAction) (*ScmpFilter, error) {
return filter, nil
}

// SetTsync sets or clears the filter's thread-sync (TSYNC) attribute. When set, this attribute
// tells the kernel to synchronize all threads of the calling process to the same seccomp filter.
// When using filters with the seccomp notification action (ActNotify), the TSYNC attribute
// must be cleared prior to loading the filter. Refer to the seccomp manual page (seccomp(2)) for
// further details.
func (f *ScmpFilter) SetTsync(val bool) error {
var cval C.uint32_t

if val == true {
cval = 1
} else {
cval = 0
}

err := f.setFilterAttr(filterAttrTsync, cval)
if err != nil && val == false && err == syscall.ENOTSUP {
return nil
}
return err
}

// IsValid determines whether a filter context is valid to use.
// Some operations (Release and Merge) render filter contexts invalid and
// consequently prevent further use.
Expand Down Expand Up @@ -796,8 +909,7 @@ func (f *ScmpFilter) GetNoNewPrivsBit() (bool, error) {
func (f *ScmpFilter) GetLogBit() (bool, error) {
log, err := f.getFilterAttr(filterAttrLog)
if err != nil {
api, apiErr := getAPI()
if (apiErr != nil && api == 0) || (apiErr == nil && api < 3) {
if apiLevel < 3 {
return false, fmt.Errorf("getting the log bit is only supported in libseccomp 2.4.0 and newer with API level 3 or higher")
}

Expand Down Expand Up @@ -874,8 +986,7 @@ func (f *ScmpFilter) SetLogBit(state bool) error {

err := f.setFilterAttr(filterAttrLog, toSet)
if err != nil {
api, apiErr := getAPI()
if (apiErr != nil && api == 0) || (apiErr == nil && api < 3) {
if apiLevel < 3 {
return fmt.Errorf("setting the log bit is only supported in libseccomp 2.4.0 and newer with API level 3 or higher")
}
}
Expand Down Expand Up @@ -1011,3 +1122,36 @@ func (f *ScmpFilter) ExportBPF(file *os.File) error {

return nil
}

// Userspace Notification API

// GetNotifFd returns the userspace notification file descriptor associated with the given
// filter context. Such a file descriptor is only valid after the filter has been loaded
// and only when the filter uses the ActNotify action. The file descriptor can be used to
// retrieve and respond to notifications associated with the filter (see NotifReceive(),
// NotifRespond(), and NotifIDValid()).
func (f *ScmpFilter) GetNotifFd() (ScmpFd, error) {
return f.getNotifFd()
}

// NotifReceive retrieves a seccomp userspace notification from a filter whose ActNotify
// action has triggered. The caller is expected to process the notification and return a
// response via NotifRespond(). Each invocation of this function returns one
// notification. As multiple notifications may be pending at any time, this function is
// normally called within a polling loop.
func NotifReceive(fd ScmpFd) (*ScmpNotifReq, error) {
return notifReceive(fd)
}

// NotifRespond responds to a notification retrieved via NotifReceive(). The response Id
// must match that of the corresponding notification retrieved via NotifReceive().
func NotifRespond(fd ScmpFd, scmpResp *ScmpNotifResp) error {
return notifRespond(fd, scmpResp)
}

// NotifIDValid checks if a notification is still valid. An return value of nil means the
// notification is still valid. Otherwise the notification is not valid. This can be used
// to mitigate time-of-check-time-of-use (TOCTOU) attacks as described in seccomp_notify_id_valid(2).
func NotifIDValid(fd ScmpFd, id uint64) error {
return notifIDValid(fd, id)
}

0 comments on commit 78c92cb

Please sign in to comment.