filter.go (329 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package seccomp
import (
"errors"
"fmt"
"io"
"strings"
"golang.org/x/net/bpf"
"github.com/elastic/go-seccomp-bpf/arch"
)
const (
syscallNumOffset = 0
archOffset = 4
)
// FilterFlag is a flag that is passed to the seccomp. Multiple flags can be
// OR'ed together.
type FilterFlag uint32
var filterFlagNames = map[FilterFlag]string{
FilterFlagTSync: "tsync",
FilterFlagLog: "log",
}
// String returns a string representation of the FilterFlag.
func (f FilterFlag) String() string {
if name, found := filterFlagNames[f]; found {
return name
}
var list []string
for flag, name := range filterFlagNames {
if f&flag != 0 {
f ^= flag
list = append(list, name)
}
}
if f != 0 {
list = append(list, "unknown")
}
return strings.Join(list, "|")
}
// MarshalText marshals the value to text.
func (f FilterFlag) MarshalText() ([]byte, error) {
return []byte(f.String()), nil
}
// Action specifies what to do when a syscall matches during filter evaluation.
type Action uint32
var actionNames = map[Action]string{
ActionKillThread: "kill_thread",
ActionKillProcess: "kill_process",
ActionTrap: "trap",
ActionErrno: "errno",
ActionTrace: "trace",
ActionLog: "log",
ActionAllow: "allow",
}
// Unpack sets the Action value based on the string.
func (a *Action) Unpack(s string) error {
s = strings.ToLower(s)
for action, name := range actionNames {
if name == s {
*a = action
return nil
}
}
return fmt.Errorf("invalid action: %v", s)
}
// String returns a string representation of the Action.
func (a Action) String() string {
name, found := actionNames[a]
if found {
return name
}
return "unknown"
}
// MarshalText marshals the value to text.
func (a Action) MarshalText() ([]byte, error) {
return []byte(a.String()), nil
}
// Filter contains all the parameters necessary to install a Linux seccomp
// filter for the process.
type Filter struct {
NoNewPrivs bool `config:"no_new_privs" json:"no_new_privs"` // Set the process's no new privs bit.
Flag FilterFlag `config:"flag" json:"flag"` // Flag to pass to the seccomp call.
Policy Policy `config:"policy" json:"policy"` // Policy that will be assembled into a BPF filter.
}
// Policy defines the BPF seccomp filter.
type Policy struct {
DefaultAction Action `config:"default_action" json:"default_action" yaml:"default_action"` // Action when no syscalls match.
Syscalls []SyscallGroup `config:"syscalls" json:"syscalls" yaml:"syscalls"` // Groups of syscalls and actions.
arch *arch.Info
}
// SyscallGroup is a logical block within a Policy that contains a set of
// syscalls to match against and an action to take.
type SyscallGroup struct {
Names []string `config:"names" json:"names" yaml:"names"` // List of syscall names (all must exist).
NamesWithCondtions []NameWithConditions `config:"names_with_args" json:"names_with_args" yaml:"names_with_args"` // List of syscall with argument filters
Action Action `config:"action" validate:"required" json:"action" yaml:"action"` // Action to take upon a match.
arch *arch.Info
}
// ArgumentConditions consist of a list of up to six conditions for the six arguments.
type ArgumentConditions []Condition
func (a ArgumentConditions) Validate() []string {
var problems []string
for _, condition := range a {
if condition.Argument < 0 || condition.Argument > 5 {
problems = append(problems, fmt.Sprintf("argument must be between 0 and 5 (inclusive), but is %v", condition.Argument))
}
}
return problems
}
type NameWithConditions struct {
Name string `config:"name" validate:"required" json:"name" yaml:"name"`
Conditions ArgumentConditions `config:"arguments" validate:"required" json:"arguments" yaml:"arguments"`
}
type Condition struct {
Argument uint32 `config:"argument" default:"0" json:"position" yaml:"position"`
Operation Operation `config:"operation" validate:"required" json:"operation" yaml:"operation"`
Value uint64 `config:"value" default:"0" json:"value" yaml:"value"`
}
type Operation string
const (
Equal Operation = "Equal"
NotEqual Operation = "NotEqual"
GreaterThan Operation = "GreaterThan"
LessThan Operation = "LessThan"
GreaterOrEqual Operation = "GreaterOrEqual"
LessOrEqual Operation = "LessOrEqual"
BitsSet Operation = "BitsSet"
BitsNotSet Operation = "BitsNotSet"
)
var Operations = []Operation{Equal, NotEqual, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual, BitsSet, BitsNotSet}
// Unpack sets the Operation value based on the string.
func (o *Operation) Unpack(s string) error {
s = strings.ToLower(s)
for _, name := range Operations {
if strings.ToLower(string(name)) == s {
*o = name
return nil
}
}
return fmt.Errorf("invalid operation: %v", s)
}
// Validate validates that the configuration has both a default action and a
// set of syscalls.
func (p *Policy) Validate() error {
if _, found := actionNames[p.DefaultAction]; !found {
return fmt.Errorf("invalid default_action value %d", p.DefaultAction)
}
if len(p.Syscalls) == 0 {
return errors.New("syscalls must not be empty")
}
return nil
}
// Assemble assembles the policy into a list of BPF instructions. If the policy
// contains any unknown syscalls or invalid actions an error will be returned.
func (p *Policy) Assemble() ([]bpf.Instruction, error) {
if err := p.Validate(); err != nil {
return nil, err
}
// Ensure arch has been set for the policy.
if p.arch == nil {
arch, err := arch.GetInfo("")
if err != nil {
return nil, err
}
p.arch = arch
}
// Build the syscall filters.
var instructions []bpf.Instruction
for _, group := range p.Syscalls {
if group.arch == nil {
group.arch = p.arch
}
groupInsts, err := group.Assemble(p.DefaultAction)
if err != nil {
return nil, err
}
instructions = append(instructions, groupInsts...)
}
// Filter out x32 to prevent bypassing blacklists by using the 32-bit ABI.
var x32Filter []bpf.Instruction
if p.arch.ID == arch.X86_64.ID {
x32Filter = []bpf.Instruction{
bpf.JumpIf{Cond: bpf.JumpGreaterOrEqual, Val: uint32(arch.X32.SeccompMask), SkipFalse: 1},
bpf.RetConstant{Val: uint32(ActionErrno) | uint32(errnoENOSYS)},
}
}
program := make([]bpf.Instruction, 0, len(x32Filter)+len(instructions)+5)
program = append(program, bpf.LoadAbsolute{Off: archOffset, Size: sizeOfUint32})
// If the loaded arch ID is not equal p.arch.ID, jump to the final Ret instruction.
jumpN := len(x32Filter) + len(instructions) - 1
if jumpN <= 255 {
program = append(program, bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: uint32(p.arch.ID), SkipTrue: uint8(jumpN)})
} else {
// JumpIf can not handle long jumps, so we switch to two instructions for this case.
program = append(program, bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(p.arch.ID), SkipTrue: 1})
program = append(program, bpf.Jump{Skip: uint32(jumpN)})
}
program = append(program, bpf.LoadAbsolute{Off: syscallNumOffset, Size: sizeOfUint32})
program = append(program, x32Filter...)
program = append(program, instructions...)
return program, nil
}
// Dump writes a textual represenation of the BPF instructions to out.
func (p *Policy) Dump(out io.Writer) error {
assembled, err := p.Assemble()
if err != nil {
return err
}
for n, instruction := range assembled {
fmt.Fprintf(out, "%d: %v\n", n, instruction)
}
return nil
}
// SyscallWithConditions consists of a syscall number and optional conditions.
//
// The conditions are applied to the arguments of the syscall.
// So, conditions consist of a list of up to six argument conditions.
// This filter matches if all argument conditions match for any Conditions.
type SyscallWithConditions struct {
Num uint32
Conditions []ArgumentConditions
}
// getSyscall searches the syscall in the list.
// Do not use a map to keep the ordering, as specified by the user.
func getSyscall(syscalls []SyscallWithConditions, syscall uint32) *SyscallWithConditions {
for i := range syscalls {
// Use the reference directely from the slice rather than the iteration variable from range,
// as the iteration variable in a range loop is a copy and cannot be modified.
s := &syscalls[i]
if s.Num == syscall {
return s
}
}
return nil
}
// toSyscallsWithConditions transforms a syscall group to syscalls with conditions.
func (g *SyscallGroup) toSyscallsWithConditions() ([]SyscallWithConditions, error) {
var (
syscalls []SyscallWithConditions
problems []string
)
for _, name := range g.Names {
if num, found := g.arch.SyscallNames[name]; found {
syscall := uint32(num | g.arch.SeccompMask)
if getSyscall(syscalls, syscall) == nil {
syscalls = append(syscalls, SyscallWithConditions{Num: syscall})
} else {
problems = append(problems, fmt.Sprintf("found duplicate syscall %v", name))
}
} else {
problems = append(problems, fmt.Sprintf("found unknown syscalls for arch %v: %v", g.arch.Name, name))
}
}
for _, nc := range g.NamesWithCondtions {
if num, found := g.arch.SyscallNames[nc.Name]; found {
syscall := uint32(num | g.arch.SeccompMask)
check := getSyscall(syscalls, syscall)
invalidArguments := nc.Conditions.Validate()
if len(invalidArguments) > 0 {
problems = append(problems, invalidArguments...)
continue
}
if check == nil {
conditions := []ArgumentConditions{nc.Conditions}
syscalls = append(syscalls, SyscallWithConditions{Num: syscall, Conditions: conditions})
} else {
if len(check.Conditions) == 0 {
// Unconditional check found.
problems = append(problems, fmt.Sprintf("found conditional and unconditional check: %v", nc.Name))
} else {
check.Conditions = append(check.Conditions, nc.Conditions)
}
}
} else {
problems = append(problems, fmt.Sprintf("found unknown syscalls for arch %v: %v", g.arch.Name, nc.Name))
}
}
if len(problems) > 0 {
return nil, fmt.Errorf(strings.Join(problems, "\n"))
}
return syscalls, nil
}
func (g *SyscallGroup) Assemble(defaultAction Action) ([]bpf.Instruction, error) {
if len(g.Names) == 0 && len(g.NamesWithCondtions) == 0 {
return nil, nil
}
// Validate the syscalls.
syscalls, err := g.toSyscallsWithConditions()
if err != nil {
return nil, err
}
p := NewProgram()
action := p.NewLabel()
for _, syscall := range syscalls {
syscall.Assemble(&p, action)
}
p.Ret(defaultAction)
p.SetLabel(action)
p.Ret(g.Action)
return p.Assemble()
}
func (s SyscallWithConditions) Assemble(p *Program, action Label) {
if len(s.Conditions) == 0 {
// If no conditions are set, compare to the syscall number and jump to action if it matches.
p.JmpIfTrue(bpf.JumpEqual, s.Num, action)
return
}
nextSyscall := p.NewLabel()
p.JmpIfTrue(bpf.JumpNotEqual, s.Num, nextSyscall)
for _, conditions := range s.Conditions {
noMatch := p.NewLabel()
for i, c := range conditions {
nextArgument := p.NewLabel()
// All argument checks must match, so if this argument check matches, jump to the next argument
// or if it is the last check to the action.
match := nextArgument
isLast := i == len(conditions)-1
if isLast {
match = action
}
// Perform the 64-bit operation with multiple 32-bit operations.
if c.Operation == Equal {
// Arg_hi == Val_hi && Arg_lo == Val_lo
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpEqual, uint32(c.Value), match, noMatch)
} else if c.Operation == NotEqual {
// Arg_hi != Val_hi || Arg_lo != Val_lo
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), match)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpNotEqual, uint32(c.Value), match, noMatch)
} else if c.Operation == GreaterThan {
// Arg_hi > Val_hi || (Arg_hi == Val_hi && Arg_lo > Val_lo)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpGreaterThan, uint32(c.Value>>32), match)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpGreaterThan, uint32(c.Value), match, noMatch)
} else if c.Operation == GreaterOrEqual {
// Arg_hi >= Val_hi || (Arg_hi == Val_hi && Arg_lo >= Val_lo)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpGreaterThan, uint32(c.Value>>32), match)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpGreaterOrEqual, uint32(c.Value), match, noMatch)
} else if c.Operation == LessThan {
// Arg_hi < Val_hi || (Arg_hi == Val_hi && Arg_lo < Val_lo)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpLessThan, uint32(c.Value>>32), match)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpLessThan, uint32(c.Value), match, noMatch)
} else if c.Operation == LessOrEqual {
// Arg_hi <= Val_hi || (Arg_hi == Val_hi && Arg_lo <= Val_lo)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpLessThan, uint32(c.Value>>32), match)
p.JmpIfTrue(bpf.JumpNotEqual, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpLessOrEqual, uint32(c.Value), match, noMatch)
} else if c.Operation == BitsSet {
// (Arg_hi & Val_hi == 0) || (Arg_lo & Val_lo == 0)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpBitsSet, uint32(c.Value>>32), match)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpBitsSet, uint32(c.Value), match, noMatch)
} else if c.Operation == BitsNotSet {
// (Arg_hi & Val_hi != 0) && (Arg_lo & Val_lo != 0)
p.LdHi(c.Argument)
p.JmpIfTrue(bpf.JumpBitsSet, uint32(c.Value>>32), noMatch)
p.LdLo(c.Argument)
p.JmpIf(bpf.JumpBitsNotSet, uint32(c.Value), match, noMatch)
}
p.SetLabel(nextArgument)
}
p.SetLabel(noMatch)
}
p.SetLabel(nextSyscall)
}