mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
7a8d7162f9
Having -EPERM is the default was a fairly significant mistake from a
future-proofing standpoint in that it makes any new syscall return a
non-ignorable error (from glibc's point of view). We need to correct
this now because faccessat2(2) is something glibc critically needs to
have support for, but they're blocked on container runtimes because we
return -EPERM unconditionally (leading to confusion in glibc). This is
also a problem we're probably going to keep running into in the future.
Unfortunately there are several issues which stop us from having a clean
solution to this problem:
1. libseccomp has several limitations which require us to emulate
behaviour we want:
a. We cannot do logic based on syscall number, meaning we cannot
specify a "largest known syscall number";
b. libseccomp doesn't know in which kernel version a syscall was
added, and has no API for "minimum kernel version" so we cannot
simply ask libseccomp to generate sane -ENOSYS rules for us.
c. Additional seccomp rules for the same syscall are not treated as
distinct rules -- if rules overlap, seccomp will merge them. This
means we cannot add per-syscall -EPERM fallbacks;
d. There is no inverse operation for SCMP_CMP_MASKED_EQ;
e. libseccomp does not allow you to specify multiple rules for a
single argument, making it impossible to invert OR rules for
arguments.
2. The runtime-spec does not have any way of specifying:
a. The errno for the default action;
b. The minimum kernel version or "newest syscall at time of profile
creation"; nor
c. Which syscalls were intentionally excluded from the allow list
(weird syscalls that are no longer used were excluded entirely,
but Docker et al expect those syscalls to get EPERM not ENOSYS).
3. Certain syscalls should not return -ENOSYS (especially only for
certain argument combinations) because this could also trigger glibc
confusion. This means we have to return -EPERM for certain syscalls
but not as a global default.
4. There is not an obvious (and reasonable) upper limit to syscall
numbers, so we cannot create a set of rules for each syscall above
the largest syscall number in libseccomp. This means we must handle
inverse rules as described below.
5. Any syscall can be specified multiple times, which can make
generation of hotfix rules much harder.
As a result, we have to work around all of these things by coming up
with a heuristic to stop the bleeding. In the future we could hopefully
improve the situation in the runtime-spec and libseccomp.
The solution applied here is to prepend a "stub" filter which returns
-ENOSYS if the requested syscall has a larger syscall number than any
syscall mentioned in the filter. The reason for this specific rule is
that syscall numbers are (roughly) allocated sequentially and thus newer
syscalls will (usually) have a larger syscall number -- thus causing our
filters to produce -ENOSYS if the filter was written before the syscall
existed.
Sadly this is not a perfect solution because syscalls can be added
out-of-order and the syscall table can contain holes for several
releases. Unfortuntely we do not have a nicer solution at the moment
because there is no library which provides information about which Linux
version a syscall was introduced in. Until that exists, this workaround
will have to be good enough.
The above behaviour only happens if the default action is a blocking
action (in other words it is not SCMP_ACT_LOG or SCMP_ACT_ALLOW). If the
default action is permissive then we don't do any patching.
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
271 lines
7.3 KiB
Go
271 lines
7.3 KiB
Go
// +build linux,cgo,seccomp
|
|
|
|
package seccomp
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
"github.com/opencontainers/runc/libcontainer/seccomp/patchbpf"
|
|
|
|
libseccomp "github.com/seccomp/libseccomp-golang"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var (
|
|
actAllow = libseccomp.ActAllow
|
|
actTrap = libseccomp.ActTrap
|
|
actKill = libseccomp.ActKill
|
|
actTrace = libseccomp.ActTrace.SetReturnCode(int16(unix.EPERM))
|
|
actLog = libseccomp.ActLog
|
|
actErrno = libseccomp.ActErrno.SetReturnCode(int16(unix.EPERM))
|
|
)
|
|
|
|
const (
|
|
// Linux system calls can have at most 6 arguments
|
|
syscallMaxArguments int = 6
|
|
)
|
|
|
|
// Filters given syscalls in a container, preventing them from being used
|
|
// Started in the container init process, and carried over to all child processes
|
|
// Setns calls, however, require a separate invocation, as they are not children
|
|
// of the init until they join the namespace
|
|
func InitSeccomp(config *configs.Seccomp) error {
|
|
if config == nil {
|
|
return errors.New("cannot initialize Seccomp - nil config passed")
|
|
}
|
|
|
|
defaultAction, err := getAction(config.DefaultAction, nil)
|
|
if err != nil {
|
|
return errors.New("error initializing seccomp - invalid default action")
|
|
}
|
|
|
|
filter, err := libseccomp.NewFilter(defaultAction)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating filter: %s", err)
|
|
}
|
|
|
|
// Add extra architectures
|
|
for _, arch := range config.Architectures {
|
|
scmpArch, err := libseccomp.GetArchFromString(arch)
|
|
if err != nil {
|
|
return fmt.Errorf("error validating Seccomp architecture: %s", err)
|
|
}
|
|
if err := filter.AddArch(scmpArch); err != nil {
|
|
return fmt.Errorf("error adding architecture to seccomp filter: %s", err)
|
|
}
|
|
}
|
|
|
|
// Unset no new privs bit
|
|
if err := filter.SetNoNewPrivsBit(false); err != nil {
|
|
return fmt.Errorf("error setting no new privileges: %s", err)
|
|
}
|
|
|
|
// Add a rule for each syscall
|
|
for _, call := range config.Syscalls {
|
|
if call == nil {
|
|
return errors.New("encountered nil syscall while initializing Seccomp")
|
|
}
|
|
if err := matchCall(filter, call); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := patchbpf.PatchAndLoad(config, filter); err != nil {
|
|
return fmt.Errorf("error loading seccomp filter into kernel: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsEnabled returns if the kernel has been configured to support seccomp.
|
|
func IsEnabled() bool {
|
|
// Try to read from /proc/self/status for kernels > 3.8
|
|
s, err := parseStatusFile("/proc/self/status")
|
|
if err != nil {
|
|
// Check if Seccomp is supported, via CONFIG_SECCOMP.
|
|
if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL {
|
|
// Make sure the kernel has CONFIG_SECCOMP_FILTER.
|
|
if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
_, ok := s["Seccomp"]
|
|
return ok
|
|
}
|
|
|
|
// Convert Libcontainer Action to Libseccomp ScmpAction
|
|
func getAction(act configs.Action, errnoRet *uint) (libseccomp.ScmpAction, error) {
|
|
switch act {
|
|
case configs.Kill:
|
|
return actKill, nil
|
|
case configs.Errno:
|
|
if errnoRet != nil {
|
|
return libseccomp.ActErrno.SetReturnCode(int16(*errnoRet)), nil
|
|
}
|
|
return actErrno, nil
|
|
case configs.Trap:
|
|
return actTrap, nil
|
|
case configs.Allow:
|
|
return actAllow, nil
|
|
case configs.Trace:
|
|
if errnoRet != nil {
|
|
return libseccomp.ActTrace.SetReturnCode(int16(*errnoRet)), nil
|
|
}
|
|
return actTrace, nil
|
|
case configs.Log:
|
|
return actLog, nil
|
|
default:
|
|
return libseccomp.ActInvalid, errors.New("invalid action, cannot use in rule")
|
|
}
|
|
}
|
|
|
|
// Convert Libcontainer Operator to Libseccomp ScmpCompareOp
|
|
func getOperator(op configs.Operator) (libseccomp.ScmpCompareOp, error) {
|
|
switch op {
|
|
case configs.EqualTo:
|
|
return libseccomp.CompareEqual, nil
|
|
case configs.NotEqualTo:
|
|
return libseccomp.CompareNotEqual, nil
|
|
case configs.GreaterThan:
|
|
return libseccomp.CompareGreater, nil
|
|
case configs.GreaterThanOrEqualTo:
|
|
return libseccomp.CompareGreaterEqual, nil
|
|
case configs.LessThan:
|
|
return libseccomp.CompareLess, nil
|
|
case configs.LessThanOrEqualTo:
|
|
return libseccomp.CompareLessOrEqual, nil
|
|
case configs.MaskEqualTo:
|
|
return libseccomp.CompareMaskedEqual, nil
|
|
default:
|
|
return libseccomp.CompareInvalid, errors.New("invalid operator, cannot use in rule")
|
|
}
|
|
}
|
|
|
|
// Convert Libcontainer Arg to Libseccomp ScmpCondition
|
|
func getCondition(arg *configs.Arg) (libseccomp.ScmpCondition, error) {
|
|
cond := libseccomp.ScmpCondition{}
|
|
|
|
if arg == nil {
|
|
return cond, errors.New("cannot convert nil to syscall condition")
|
|
}
|
|
|
|
op, err := getOperator(arg.Op)
|
|
if err != nil {
|
|
return cond, err
|
|
}
|
|
|
|
return libseccomp.MakeCondition(arg.Index, op, arg.Value, arg.ValueTwo)
|
|
}
|
|
|
|
// Add a rule to match a single syscall
|
|
func matchCall(filter *libseccomp.ScmpFilter, call *configs.Syscall) error {
|
|
if call == nil || filter == nil {
|
|
return errors.New("cannot use nil as syscall to block")
|
|
}
|
|
|
|
if len(call.Name) == 0 {
|
|
return errors.New("empty string is not a valid syscall")
|
|
}
|
|
|
|
// If we can't resolve the syscall, assume it's not supported on this kernel
|
|
// Ignore it, don't error out
|
|
callNum, err := libseccomp.GetSyscallFromName(call.Name)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Convert the call's action to the libseccomp equivalent
|
|
callAct, err := getAction(call.Action, call.ErrnoRet)
|
|
if err != nil {
|
|
return fmt.Errorf("action in seccomp profile is invalid: %s", err)
|
|
}
|
|
|
|
// Unconditional match - just add the rule
|
|
if len(call.Args) == 0 {
|
|
if err := filter.AddRule(callNum, callAct); err != nil {
|
|
return fmt.Errorf("error adding seccomp filter rule for syscall %s: %s", call.Name, err)
|
|
}
|
|
} else {
|
|
// If two or more arguments have the same condition,
|
|
// Revert to old behavior, adding each condition as a separate rule
|
|
argCounts := make([]uint, syscallMaxArguments)
|
|
conditions := []libseccomp.ScmpCondition{}
|
|
|
|
for _, cond := range call.Args {
|
|
newCond, err := getCondition(cond)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating seccomp syscall condition for syscall %s: %s", call.Name, err)
|
|
}
|
|
|
|
argCounts[cond.Index] += 1
|
|
|
|
conditions = append(conditions, newCond)
|
|
}
|
|
|
|
hasMultipleArgs := false
|
|
for _, count := range argCounts {
|
|
if count > 1 {
|
|
hasMultipleArgs = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasMultipleArgs {
|
|
// Revert to old behavior
|
|
// Add each condition attached to a separate rule
|
|
for _, cond := range conditions {
|
|
condArr := []libseccomp.ScmpCondition{cond}
|
|
|
|
if err := filter.AddRuleConditional(callNum, callAct, condArr); err != nil {
|
|
return fmt.Errorf("error adding seccomp rule for syscall %s: %s", call.Name, err)
|
|
}
|
|
}
|
|
} else {
|
|
// No conditions share same argument
|
|
// Use new, proper behavior
|
|
if err := filter.AddRuleConditional(callNum, callAct, conditions); err != nil {
|
|
return fmt.Errorf("error adding seccomp rule for syscall %s: %s", call.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseStatusFile(path string) (map[string]string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
s := bufio.NewScanner(f)
|
|
status := make(map[string]string)
|
|
|
|
for s.Scan() {
|
|
text := s.Text()
|
|
parts := strings.Split(text, ":")
|
|
|
|
if len(parts) <= 1 {
|
|
continue
|
|
}
|
|
|
|
status[parts[0]] = parts[1]
|
|
}
|
|
if err := s.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// Version returns major, minor, and micro.
|
|
func Version() (uint, uint, uint) {
|
|
return libseccomp.GetLibraryVersion()
|
|
}
|