mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Example output: ``` I0807 10:27:23.158030 1 timing.go:866] ---- runsc boot timing information ---- [10:27:22.991938] ╭─ runsc boot [10:27:22.995285] +3 346µs ▋ ├─ Go code running: 3 346µs [10:27:22.996191] +906µs ▏ ├─ boot subcommand dispatched: 906µs [10:27:22.996643] +451µs ├─ CPUID initialized: 451µs [10:27:22.996645] +2µs ├─ ring0 initialized: 2µs [10:27:22.997012] +367µs ├─ host info read: 367µs [10:27:22.998024] +1 011µs ▏ ├─ spec read: 1 011µs [10:27:22.998555] +530µs ├─ chroot base mounted: 530µs [10:27:22.998822] +267µs ├─ chroot proc mounted: 267µs [10:27:23.000271] +1 449µs ▎ ├─ chroot procfs set up: 1 449µs [10:27:23.000369] +97µs ├─ chroot sealed read-only: 97µs [10:27:23.002635] +2 266µs ▍ ├─ chroot set up: 2 266µs [10:27:23.003523] +887µs ▏ ├─ sandbox capabilities computed: 887µs [10:27:23.003693] +170µs ├─ current capabilities read: 170µs [10:27:23.012905] +9 211µs ▉▊ ├─ bounding set trimmed on all threads: 9 211µs [10:27:23.013139] +233µs ├─ capset applied on all threads: 233µs [10:27:23.013449] +310µs ├─ capabilities applied: 310µs [10:27:23.024366] +10 916µs ▉▉ ├─ resolved mounts read: 10 916µs [10:27:23.029416] +5 049µs ▉ ├─ seccheck initialized: 5 049µs [10:27:23.029419] +3µs ├─ RNG initialized: 3µs [10:27:23.029571] +152µs ├─ memory usage initialized: 152µs [10:27:23.029619] +47µs ├─ GOMAXPROCS set: 47µs [10:27:23.057560] +27 941µs ▉▉▉▉▉▍ ├─ stdio FDs remapped: 27 941µs [10:27:23.057563] +2µs ├─ donated FDs ingested: 2µs [10:27:23.057889] +326µs ├─ systrap memory file created: 326µs [10:27:23.058714] +825µs ▏ ├─ systrap stub initialized: 825µs [10:27:23.063232] +4 517µs █ ├─ systrap source process created: 4 517µs [10:27:23.063244] +12µs ├─ waiting for membarrier: 12µs [10:27:23.063246] +1µs ├─ host membarrier probed: 1µs [10:27:23.063251] +4µs ├─ platform created: 4µs [10:27:23.063390] +138µs ├─ memory file created: 138µs [10:27:23.063438] +48µs ├─ VDSO prepared: 48µs [10:27:23.063473] +34µs ├─ timekeeper configured: 34µs [10:27:23.063675] +201µs ├─ network stack created: 201µs [10:27:23.063854] +179µs ├─ kernel initialized: 179µs [10:27:23.063859] +4µs ├─ filesystems registered: 4µs [10:27:23.063943] +84µs ├─ process args created: 84µs [10:27:23.063947] +3µs ├─ compat logs initialized: 3µs [10:27:23.063948] +0µs ├─ mount hints parsed: 0µs [10:27:23.063949] +1µs ├─ host FS mount created: 1µs [10:27:23.064142] +192µs ├─ control server started: 192µs [10:27:23.064142] +0µs ├─ loader created: 0µs [10:27:23.064152] +9µs ├─ metrics initialized: 9µs [10:27:23.064152] +0µs ├─ notifying parent: 0µs [10:27:23.126745] +62 592µs ▉▉▉▉▉▉▉▉▉▉▉▉ ├─ network args received: 62 592µs [10:27:23.126972] +227µs ├─ start RPC received: 227µs [10:27:23.126982] +10µs ├─ start signal received: 10µs [10:27:23.127852] +869µs ▏ ├─ network configured: 869µs [10:27:23.128646] +794µs ▏ ├─ open FDs validated: 794µs [10:27:23.131274] +2 628µs ▌ ├─ pre-seccomp callback done: 2 628µs [10:27:23.132513] +1 238µs ▎ ├─ seccomp rules computed: 1 238µs [10:27:23.146898] +14 384µs ▉▉▊ ├─ seccomp program compiled: 14 384µs [10:27:23.148596] +1 698µs ▎ ├─ seccomp program installed: 1 698µs [10:27:23.148655] +58µs ├─ seccomp filters installed: 58µs [10:27:23.148655] +0µs ├─ forked: 0µs ╭─ root container [10:27:23.148876] +220µs │ ... 220µs ... ├─ FD table created: 220µs [10:27:23.149125] +249µs │ ... 469µs ... ├─ cgroups mounted: 249µs [10:27:23.149798] +673µs ▏ │ ... 1 143µs ... ├─ rootfs mounted: 673µs [10:27:23.149804] +5µs │ ... 1 148µs ... ├─ submounts prepared: 5µs [10:27:23.150009] +205µs │ ... 1 353µs ... ├─ mounted "/dev": 205µs [10:27:23.150362] +352µs │ ... 1 706µs ... ├─ mounted "/sys": 352µs [10:27:23.150822] +459µs │ ... 2 166µs ... ├─ mounted "/proc": 459µs [10:27:23.150886] +63µs │ ... 2 230µs ... ├─ mounted "/dev/pts": 63µs [10:27:23.150915] +29µs │ ... 2 259µs ... ├─ mounted "/dev/shm": 29µs [10:27:23.151200] +285µs │ ... 2 544µs ... ├─ mounted "/etc/hosts": 285µs [10:27:23.151212] +12µs │ ... 2 557µs ... ├─ mounted "/dev/mqueue": 12µs [10:27:23.151539] +326µs │ ... 2 883µs ... ├─ mounted "/etc/hostname": 326µs [10:27:23.151660] +121µs │ ... 3 004µs ... ├─ mounted "/sys/fs/cgroup": 121µs [10:27:23.151846] +186µs │ ... 3 190µs ... ├─ mounted "/etc/resolv.conf": 186µs [10:27:23.151974] +128µs │ ... 3 319µs ... ├─ submounts mounted: 128µs [10:27:23.151981] +6µs │ ... 3 325µs ... ├─ device files created: 6µs [10:27:23.151981] +0µs │ ... 3 326µs ... ├─ executable path resolved: 0µs [10:27:23.152057] +75µs │ ... 3 401µs ... ├─ exec user home resolved: 75µs [10:27:23.152085] +28µs │ ... 3 429µs ... ├─ FS context created: 28µs [10:27:23.153742] +1 656µs ▎ │ ... 5 086µs ... ├─ memory manager created: 1 656µs [10:27:23.156409] +2 667µs ▌ │ ... 7 753µs ... ├─ executable loaded: 2 667µs [10:27:23.156419] +10µs │ ... 7 763µs ... ├─ VDSO mapped: 10µs [10:27:23.156425] +5µs │ ... 7 769µs ... ├─ stack allocated: 5µs [10:27:23.156491] +65µs │ ... 7 835µs ... ├─ stack contents loaded: 65µs [10:27:23.156497] +5µs │ ... 7 841µs ... ├─ task image loaded: 5µs [10:27:23.156543] +46µs │ ... 7 887µs ... ├─ init task created: 46µs [10:27:23.156599] +56µs ├─ root container created: 7 943µs │ ... 56µs ... [10:27:23.157146] +547µs ├─ signal forwarding started: 547µs │ ... 603µs ... [10:27:23.157575] +428µs ├─ kernel started: 428µs │ ... 1 031µs ... [10:27:23.157575] +0µs │ ... 0µs ... ╰─ END 1 032µs, total 8 919µs [10:27:23.157576] +1µs ╰─ END 1µs, total 165 638µs ---- End of runsc boot timing information ---- ```
1181 lines
39 KiB
Go
1181 lines
39 KiB
Go
// Copyright 2018 The gVisor Authors.
|
|
//
|
|
// Licensed 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 provides generation of basic seccomp filters. Currently,
|
|
// only little endian systems are supported.
|
|
package seccomp
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/bpf"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
"gvisor.dev/gvisor/pkg/timing"
|
|
)
|
|
|
|
const (
|
|
// skipOneInst is the offset to take for skipping one instruction.
|
|
skipOneInst = 1
|
|
|
|
// defaultLabel is the label for the default action.
|
|
defaultLabel = label("default_action")
|
|
|
|
// vsyscallPageIPMask is the bit we expect to see in the instruction
|
|
// pointer of a vsyscall call.
|
|
vsyscallPageIPMask = 1 << 31
|
|
)
|
|
|
|
// Install generates BPF code based on the set of syscalls provided. It only
|
|
// allows syscalls that conform to the specification. Syscalls that violate the
|
|
// specification will trigger RET_KILL_PROCESS. If RET_KILL_PROCESS is not
|
|
// supported, violations will trigger RET_TRAP instead. RET_KILL_THREAD is not
|
|
// used because it only kills the offending thread and often keeps the sentry
|
|
// hanging.
|
|
//
|
|
// denyRules describes forbidden syscalls. rules describes allowed syscalls.
|
|
// denyRules is executed before rules.
|
|
//
|
|
// Be aware that RET_TRAP sends SIGSYS to the process and it may be ignored,
|
|
// making it possible for the process to continue running after a violation.
|
|
// However, it will leave a SECCOMP audit event trail behind. In any case, the
|
|
// syscall is still blocked from executing.
|
|
func (p *Program) Install(timer *timing.Timer) error {
|
|
// *** DEBUG TIP ***
|
|
// If you suspect the Sentry is getting killed due to a seccomp violation,
|
|
// look for the `debugFilter` boolean in `//runsc/boot/filter/filter.go`.
|
|
|
|
totalSize := 0
|
|
for _, ruleSet := range p.RuleSets {
|
|
totalSize += ruleSet.Rules.Size()
|
|
}
|
|
log.Infof("Installing seccomp filters for %d syscalls (action=%v)", totalSize, p.Options.DefaultAction)
|
|
|
|
instrs, _, err := p.Build()
|
|
|
|
if log.IsLogging(log.Debug) {
|
|
programStr, errDecode := bpf.DecodeInstructions(instrs)
|
|
if errDecode != nil {
|
|
programStr = fmt.Sprintf("Error: %v\n%s", errDecode, programStr)
|
|
}
|
|
log.Debugf("Seccomp program dump:\n%s", programStr)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timer.Reached("seccomp program compiled")
|
|
|
|
// Perform the actual installation.
|
|
if err := SetFilter(instrs); err != nil {
|
|
return fmt.Errorf("failed to set filter: %v", err)
|
|
}
|
|
timer.Reached("seccomp program installed")
|
|
|
|
log.Infof("Seccomp filters installed.")
|
|
return nil
|
|
}
|
|
|
|
// Action is the action to take when a seccomp rule is matched.
|
|
// Unlike the ABI variant, it supports the notion of a "default",
|
|
// which is convenient to express in RuleSets.
|
|
type Action string
|
|
|
|
// Values for SeccompAction.
|
|
const (
|
|
// Default is the default action; see saneDefaultAction().
|
|
Default Action = ""
|
|
|
|
// Allow the syscall.
|
|
Allow Action = "allow"
|
|
|
|
// Trap the syscall.
|
|
Trap Action = "trap"
|
|
|
|
// KillThread kills the thread.
|
|
KillThread Action = "kill_thread"
|
|
|
|
// KillProcess kills the process.
|
|
KillProcess Action = "kill_process"
|
|
|
|
// UserNotify notifies userspace.
|
|
UserNotify Action = "user_notify"
|
|
|
|
// ReturnError returns the given error code to the application.
|
|
ReturnError Action = "return_error"
|
|
|
|
// Trace returns the given trace to the application.
|
|
Trace Action = "trace"
|
|
)
|
|
|
|
// Code returns an Action that holds the given code.
|
|
// Behavior is action-dependent.
|
|
func (a Action) Code(code uint16) Action {
|
|
if colonIndex := strings.Index(string(a), ":"); colonIndex != -1 {
|
|
return Action(fmt.Sprintf("%s:%04x", a[:colonIndex], code))
|
|
}
|
|
return Action(fmt.Sprintf("%s:%04x", a, code))
|
|
}
|
|
|
|
// String returns the string representation of the seccomp action.
|
|
func (a Action) String() string {
|
|
if a == Default {
|
|
return "[default]"
|
|
}
|
|
return string(a)
|
|
}
|
|
|
|
// bpf returns the corresponding BPF action for the given seccomp action.
|
|
// If the action is `Default`, `defaultAction` is called.
|
|
func (a Action) bpf(defaultAction func() (linux.BPFAction, error)) (linux.BPFAction, error) {
|
|
switch a {
|
|
case Default:
|
|
da, err := defaultAction()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to determine default seccomp action: %w", err)
|
|
}
|
|
return da, nil
|
|
case Allow:
|
|
return linux.SECCOMP_RET_ALLOW, nil
|
|
case UserNotify:
|
|
return linux.SECCOMP_RET_USER_NOTIF, nil
|
|
case Trap:
|
|
return linux.SECCOMP_RET_TRAP, nil
|
|
case Trace:
|
|
return linux.SECCOMP_RET_TRACE, nil
|
|
case ReturnError:
|
|
return linux.SECCOMP_RET_ERRNO, nil
|
|
case KillThread:
|
|
return linux.SECCOMP_RET_KILL_THREAD, nil
|
|
case KillProcess:
|
|
return linux.SECCOMP_RET_KILL_PROCESS, nil
|
|
default:
|
|
colonIndex := strings.Index(string(a), ":")
|
|
if colonIndex == -1 {
|
|
panic(fmt.Sprintf("unknown seccomp action: %v", a))
|
|
}
|
|
ins := Action(a[:colonIndex])
|
|
code, err := strconv.ParseUint(string(a[colonIndex+1:]), 16, 16)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse seccomp action: %v", err))
|
|
}
|
|
act, err := ins.bpf(defaultAction)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return act.WithReturnCode(uint16(code)), nil
|
|
}
|
|
}
|
|
|
|
var (
|
|
// killProcessAvailableOnce is used to ensure that isKillProcessAvailable is
|
|
// only called once.
|
|
killProcessAvailableOnce sync.Once
|
|
|
|
// killProcessAvailable is true if SECCOMP_RET_KILL_PROCESS is available.
|
|
killProcessAvailable bool
|
|
|
|
// killProcessAvailableErr is the error obtained when trying to determine if
|
|
// SECCOMP_RET_KILL_PROCESS is available.
|
|
killProcessAvailableErr error
|
|
)
|
|
|
|
// saneDefaultAction returns a sane default for a failure to match
|
|
// a seccomp-bpf filter. Either kill the process, or trap.
|
|
func saneDefaultAction() (linux.BPFAction, error) {
|
|
killProcessAvailableOnce.Do(func() {
|
|
killProcessAvailable, killProcessAvailableErr = isKillProcessAvailable()
|
|
})
|
|
if killProcessAvailableErr != nil {
|
|
return 0, killProcessAvailableErr
|
|
}
|
|
if killProcessAvailable {
|
|
return linux.SECCOMP_RET_KILL_PROCESS, nil
|
|
}
|
|
return linux.SECCOMP_RET_TRAP, nil
|
|
}
|
|
|
|
// RuleSet is a set of rules and associated action.
|
|
type RuleSet struct {
|
|
Rules SyscallRules
|
|
Action Action
|
|
|
|
// Vsyscall indicates that a check is made for a function being called
|
|
// from kernel mappings. This is where the vsyscall page is located
|
|
// (and typically) emulated, so this RuleSet will not match any
|
|
// functions not dispatched from the vsyscall page.
|
|
Vsyscall bool
|
|
}
|
|
|
|
// SyscallName gives names to system calls. It is used purely for debugging purposes.
|
|
//
|
|
// An alternate namer can be provided to the package at initialization time.
|
|
var SyscallName = func(sysno uintptr) string {
|
|
return fmt.Sprintf("syscall_%d", sysno)
|
|
}
|
|
|
|
// syscallProgram builds a BPF program for applying syscall rules.
|
|
// It is a stateful struct that is updated as the program is built.
|
|
type syscallProgram struct {
|
|
// program is the underlying BPF program being built.
|
|
program *bpf.ProgramBuilder
|
|
}
|
|
|
|
// Stmt adds a statement to the program.
|
|
func (s *syscallProgram) Stmt(code uint16, k uint32) {
|
|
s.program.AddStmt(code, k)
|
|
}
|
|
|
|
// label is a custom label type which is returned by `labelSet`.
|
|
type label string
|
|
|
|
// JumpTo adds a jump instruction to the program, jumping to the given label.
|
|
func (s *syscallProgram) JumpTo(label label) {
|
|
s.program.AddDirectJumpLabel(string(label))
|
|
}
|
|
|
|
// If checks a condition and jumps to a label if the condition is true.
|
|
// If the condition is false, the program continues executing (no jumping).
|
|
func (s *syscallProgram) If(code uint16, k uint32, jt label) {
|
|
s.program.AddJump(code, k, 0, skipOneInst)
|
|
s.JumpTo(jt)
|
|
}
|
|
|
|
// IfNot checks a condition and jumps to a label if the condition is false.
|
|
// If the condition is true, the program continues executing (no jumping).
|
|
func (s *syscallProgram) IfNot(code uint16, k uint32, jf label) {
|
|
s.program.AddJump(code, k, skipOneInst, 0)
|
|
s.JumpTo(jf)
|
|
}
|
|
|
|
// Ret adds a return instruction to the program.
|
|
func (s *syscallProgram) Ret(action linux.BPFAction) {
|
|
s.Stmt(bpf.Ret|bpf.K, uint32(action))
|
|
}
|
|
|
|
// Label adds a label to the program.
|
|
// It panics if this label has already been added to the program.
|
|
func (s *syscallProgram) Label(label label) {
|
|
if err := s.program.AddLabel(string(label)); err != nil {
|
|
panic(fmt.Sprintf("cannot add label %q to program: %v", label, err))
|
|
}
|
|
}
|
|
|
|
// Record starts recording the instructions added to the program from now on.
|
|
// It returns a syscallFragment which can be used to perform assertions on the
|
|
// possible set of outcomes of the set of instruction that has been added
|
|
// since `Record` was called.
|
|
func (s *syscallProgram) Record() syscallProgramFragment {
|
|
return syscallProgramFragment{s.program.Record()}
|
|
}
|
|
|
|
// syscallProgramFragment represents a fragment of the syscall program.
|
|
type syscallProgramFragment struct {
|
|
getFragment func() bpf.ProgramFragment
|
|
}
|
|
|
|
// MustHaveJumpedTo asserts that the fragment must jump to one of the
|
|
// given labels.
|
|
// The fragment may not jump to any other label, nor return, nor fall through.
|
|
func (f syscallProgramFragment) MustHaveJumpedTo(labels ...label) {
|
|
f.MustHaveJumpedToOrReturned(labels, nil)
|
|
}
|
|
|
|
// MustHaveJumpedToOrReturned asserts that the fragment must jump to one of
|
|
// the given labels, or have returned one of the given return values.
|
|
// The fragment may not jump to any other label, nor fall through,
|
|
// nor return a non-deterministic value.
|
|
func (f syscallProgramFragment) MustHaveJumpedToOrReturned(possibleLabels []label, possibleReturnValues map[linux.BPFAction]struct{}) {
|
|
fragment := f.getFragment()
|
|
outcomes := fragment.Outcomes()
|
|
if outcomes.MayFallThrough {
|
|
panic(fmt.Sprintf("fragment %v may fall through", fragment))
|
|
}
|
|
if len(possibleReturnValues) == 0 && outcomes.MayReturn() {
|
|
panic(fmt.Sprintf("fragment %v may return", fragment))
|
|
}
|
|
if outcomes.MayReturnRegisterA {
|
|
panic(fmt.Sprintf("fragment %v may return register A", fragment))
|
|
}
|
|
if outcomes.MayJumpToKnownOffsetBeyondFragment {
|
|
panic(fmt.Sprintf("fragment %v may jump to an offset beyond the fragment", fragment))
|
|
}
|
|
for jumpLabel := range outcomes.MayJumpToUnresolvedLabels {
|
|
found := false
|
|
for _, wantLabel := range possibleLabels {
|
|
if jumpLabel == string(wantLabel) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
panic(fmt.Sprintf("fragment %v may jump to a label %q which is not one of %v", fragment, jumpLabel, possibleLabels))
|
|
}
|
|
}
|
|
for returnValue := range outcomes.MayReturnImmediate {
|
|
if _, found := possibleReturnValues[returnValue]; !found {
|
|
panic(fmt.Sprintf("fragment %v may return a value %q which is not one of %v", fragment, returnValue, possibleReturnValues))
|
|
}
|
|
}
|
|
}
|
|
|
|
// labelSet keeps track of labels that individual rules may jump to if they
|
|
// either match or mismatch.
|
|
// It can generate unique label names, and can be used recursively within
|
|
// rules.
|
|
type labelSet struct {
|
|
// prefix is a label prefix used when generating label names.
|
|
prefix string
|
|
|
|
// labelCounter is used to generate unique label names.
|
|
labelCounter int
|
|
|
|
// ruleMatched is the label that a rule should jump to if it matches.
|
|
ruleMatched label
|
|
|
|
// ruleMismatched is the label that a rule should jump to if it doesn't
|
|
// match.
|
|
ruleMismatched label
|
|
}
|
|
|
|
// NewLabel returns a new unique label.
|
|
func (l *labelSet) NewLabel() label {
|
|
newLabel := label(fmt.Sprintf("%s#%d", l.prefix, l.labelCounter))
|
|
l.labelCounter++
|
|
return newLabel
|
|
}
|
|
|
|
// Matched returns the label to jump to if the rule matches.
|
|
func (l *labelSet) Matched() label {
|
|
return l.ruleMatched
|
|
}
|
|
|
|
// Mismatched returns the label to jump to if the rule does not match.
|
|
func (l *labelSet) Mismatched() label {
|
|
return l.ruleMismatched
|
|
}
|
|
|
|
// Push creates a new labelSet meant to be used in a recursive context of the
|
|
// rule currently being rendered.
|
|
// Labels generated by this new labelSet will have `labelSuffix` appended to
|
|
// this labelSet's current prefix, and will have its matched/mismatched labels
|
|
// point to the given labels.
|
|
func (l *labelSet) Push(labelSuffix string, newRuleMatch, newRuleMismatch label) *labelSet {
|
|
newPrefix := labelSuffix
|
|
if l.prefix != "" {
|
|
newPrefix = fmt.Sprintf("%s_%s", l.prefix, labelSuffix)
|
|
}
|
|
return &labelSet{
|
|
prefix: newPrefix,
|
|
ruleMatched: newRuleMatch,
|
|
ruleMismatched: newRuleMismatch,
|
|
}
|
|
}
|
|
|
|
// matchedValue keeps track of BPF instructions needed to load a 64-bit value
|
|
// being matched against. Since BPF can only do operations on 32-bit
|
|
// instructions, value-matching code needs to selectively load one or the
|
|
// other half of the 64-bit value.
|
|
type matchedValue struct {
|
|
program *syscallProgram
|
|
dataOffsetHigh uint32
|
|
dataOffsetLow uint32
|
|
}
|
|
|
|
// LoadHigh32Bits loads the high 32-bit of the 64-bit value into register A.
|
|
func (m matchedValue) LoadHigh32Bits() {
|
|
m.program.Stmt(bpf.Ld|bpf.Abs|bpf.W, m.dataOffsetHigh)
|
|
}
|
|
|
|
// LoadLow32Bits loads the low 32-bit of the 64-bit value into register A.
|
|
func (m matchedValue) LoadLow32Bits() {
|
|
m.program.Stmt(bpf.Ld|bpf.Abs|bpf.W, m.dataOffsetLow)
|
|
}
|
|
|
|
// Program represents a complete seccomp program.
|
|
type Program struct {
|
|
// RuleSets is the set of rules to be used for the seccomp program.
|
|
// The first matching RuleSet is used.
|
|
RuleSets []RuleSet
|
|
|
|
// Options is the set of options to be used for the seccomp program.
|
|
Options ProgramOptions
|
|
}
|
|
|
|
// ProgramOptions configure a seccomp program.
|
|
type ProgramOptions struct {
|
|
// DefaultAction is the action returned when none of the rules match.
|
|
DefaultAction Action
|
|
|
|
// BadArchAction is the action returned when the architecture of the
|
|
// syscall structure input doesn't match the one the program expects.
|
|
BadArchAction Action
|
|
|
|
// SkipOptimizations disables optimizations for the seccomp program
|
|
// and its BPF bytecode.
|
|
SkipOptimizations bool
|
|
|
|
// HotSyscalls is the set of syscall numbers that are the hottest,
|
|
// where "hotness" refers to frequency (regardless of the amount of
|
|
// computation that the kernel will do handling them, and regardless of
|
|
// the complexity of the syscall rule for this).
|
|
// It should only contain very hot syscalls (i.e. any syscall that is
|
|
// called >10% of the time out of all syscalls made).
|
|
// It is ordered from most frequent to least frequent.
|
|
HotSyscalls []uintptr
|
|
}
|
|
|
|
// BuildStats contains information about seccomp program generation.
|
|
type BuildStats struct {
|
|
// SizeBeforeOptimizations and SizeAfterOptimizations correspond to the
|
|
// number of instructions in the program before vs after optimization.
|
|
SizeBeforeOptimizations, SizeAfterOptimizations int
|
|
|
|
// BuildDuration is the amount of time it took to build the program (before
|
|
// BPF bytecode optimizations).
|
|
BuildDuration time.Duration
|
|
|
|
// RuleOptimizeDuration is the amount of time it took to run SyscallRule
|
|
// optimizations.
|
|
RuleOptimizeDuration time.Duration
|
|
|
|
// BPFOptimizeDuration is the amount of time it took to run BPF bytecode
|
|
// optimizations.
|
|
BPFOptimizeDuration time.Duration
|
|
}
|
|
|
|
// Build builds a BPF program from the given map of actions to matching
|
|
// SyscallRules. The single generated program covers all provided RuleSets.
|
|
func (p *Program) Build() ([]bpf.Instruction, BuildStats, error) {
|
|
start := time.Now()
|
|
// Make a copy of the syscall rules and maybe optimize them.
|
|
ors, ruleOptimizeDuration, err := orderRuleSets(p.RuleSets, p.Options)
|
|
if err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
|
|
defaultActionFn := func() (linux.BPFAction, error) {
|
|
return p.Options.DefaultAction.bpf(saneDefaultAction)
|
|
}
|
|
defaultAction, err := defaultActionFn()
|
|
if err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
possibleActions := make(map[linux.BPFAction]struct{})
|
|
for _, ruleSet := range p.RuleSets {
|
|
action, err := ruleSet.Action.bpf(defaultActionFn)
|
|
if err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
possibleActions[action] = struct{}{}
|
|
}
|
|
|
|
program := &syscallProgram{
|
|
program: bpf.NewProgramBuilder(),
|
|
}
|
|
|
|
// Be paranoid and check that syscall is done in the expected architecture.
|
|
//
|
|
// A = seccomp_data.arch
|
|
// if (A != AUDIT_ARCH) goto badArchLabel.
|
|
badArchLabel := label("badarch")
|
|
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, seccompDataOffsetArch)
|
|
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, LINUX_AUDIT_ARCH, badArchLabel)
|
|
orsFrag := program.Record()
|
|
if err := ors.render(program); err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
orsFrag.MustHaveJumpedToOrReturned([]label{defaultLabel}, possibleActions)
|
|
|
|
// Default label if none of the rules matched:
|
|
program.Label(defaultLabel)
|
|
program.Ret(defaultAction)
|
|
|
|
// Label if the architecture didn't match:
|
|
program.Label(badArchLabel)
|
|
badArchAction, err := p.Options.BadArchAction.bpf(defaultActionFn)
|
|
if err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
program.Ret(badArchAction)
|
|
|
|
insns, err := program.program.Instructions()
|
|
if err != nil {
|
|
return nil, BuildStats{}, err
|
|
}
|
|
beforeOpt := len(insns)
|
|
buildDuration := time.Since(start) - ruleOptimizeDuration
|
|
var bpfOptimizeDuration time.Duration
|
|
afterOpt := beforeOpt
|
|
if !p.Options.SkipOptimizations {
|
|
insns = bpf.Optimize(insns)
|
|
bpfOptimizeDuration = time.Since(start) - buildDuration - ruleOptimizeDuration
|
|
afterOpt = len(insns)
|
|
log.Debugf("Seccomp program optimized from %d to %d instructions; took %v to build and %v to optimize", beforeOpt, afterOpt, buildDuration, bpfOptimizeDuration)
|
|
}
|
|
return insns, BuildStats{
|
|
SizeBeforeOptimizations: beforeOpt,
|
|
SizeAfterOptimizations: afterOpt,
|
|
BuildDuration: buildDuration,
|
|
RuleOptimizeDuration: ruleOptimizeDuration,
|
|
BPFOptimizeDuration: bpfOptimizeDuration,
|
|
}, nil
|
|
}
|
|
|
|
// singleSyscallRuleSet represents what to do for a single syscall.
|
|
// It is used inside `orderedRules`.
|
|
type singleSyscallRuleSet struct {
|
|
sysno uintptr
|
|
rules []syscallRuleAction
|
|
vsyscall bool
|
|
}
|
|
|
|
// Render renders the ruleset for this syscall.
|
|
func (ssrs singleSyscallRuleSet) Render(program *syscallProgram, ls *labelSet, noMatch label) {
|
|
frag := program.Record()
|
|
if ssrs.vsyscall {
|
|
// Emit a vsyscall check.
|
|
// This rule ensures that the top bit is set in the
|
|
// instruction pointer, which is where the vsyscall page
|
|
// will be mapped.
|
|
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, seccompDataOffsetIPHigh)
|
|
program.IfNot(bpf.Jmp|bpf.Jset|bpf.K, vsyscallPageIPMask, noMatch)
|
|
}
|
|
var nextRule label
|
|
actions := make(map[linux.BPFAction]struct{})
|
|
for i, ra := range ssrs.rules {
|
|
actions[ra.action] = struct{}{}
|
|
|
|
// Render the rule.
|
|
nextRule = ls.NewLabel()
|
|
ruleLabels := ls.Push(fmt.Sprintf("sysno%d_rule%d", ssrs.sysno, i), ls.NewLabel(), nextRule)
|
|
ruleFrag := program.Record()
|
|
ra.rule.Render(program, ruleLabels)
|
|
program.Label(ruleLabels.Matched())
|
|
program.Ret(ra.action)
|
|
ruleFrag.MustHaveJumpedToOrReturned(
|
|
[]label{nextRule},
|
|
map[linux.BPFAction]struct{}{
|
|
ra.action: {},
|
|
})
|
|
program.Label(nextRule)
|
|
}
|
|
program.JumpTo(noMatch)
|
|
frag.MustHaveJumpedToOrReturned([]label{noMatch}, actions)
|
|
}
|
|
|
|
// String returns a human-friendly representation of the
|
|
// `singleSyscallRuleSet`.
|
|
func (ssrs singleSyscallRuleSet) String() string {
|
|
var sb strings.Builder
|
|
if ssrs.vsyscall {
|
|
sb.WriteString("Vsyscall ")
|
|
} else {
|
|
sb.WriteString("Syscall ")
|
|
}
|
|
fmt.Fprintf(&sb, "%3d: ", ssrs.sysno)
|
|
switch len(ssrs.rules) {
|
|
case 0:
|
|
sb.WriteString("(no rules)")
|
|
case 1:
|
|
sb.WriteString(ssrs.rules[0].String())
|
|
default:
|
|
sb.WriteRune('{')
|
|
for i, r := range ssrs.rules {
|
|
if i != 0 {
|
|
sb.WriteString("; ")
|
|
}
|
|
sb.WriteString(r.String())
|
|
}
|
|
sb.WriteRune('}')
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// syscallRuleAction groups a `SyscallRule` and an action that should be
|
|
// returned if the rule matches.
|
|
type syscallRuleAction struct {
|
|
rule SyscallRule
|
|
action linux.BPFAction
|
|
}
|
|
|
|
// String returns a human-friendly representation of the `syscallRuleAction`.
|
|
func (sra syscallRuleAction) String() string {
|
|
if _, isMatchAll := sra.rule.(MatchAll); isMatchAll {
|
|
return sra.action.String()
|
|
}
|
|
return fmt.Sprintf("(%v) => %v", sra.rule.String(), sra.action)
|
|
}
|
|
|
|
// orderedRules contains an ordering of syscall rules used to render a
|
|
// program. It is derived from a list of `RuleSet`s and `ProgramOptions`.
|
|
// Its fields represent the order in which rulesets are rendered.
|
|
// There are three categorization criteria:
|
|
// - "Hot" vs "cold": hot syscalls go first and are checked linearly, cold
|
|
// syscalls go later.
|
|
// - "Trivial" vs "non-trivial": A "trivial" syscall rule means one that
|
|
// does not require checking any argument or RIP data. This basically
|
|
// means a syscall mapped to `MatchAll{}`.
|
|
// If a syscall shows up in multiple RuleSets where any of them is
|
|
// non-trivial, the whole syscall is considered non-trivial.
|
|
// - "vsyscall" vs "non-vsyscall": A syscall that needs vsyscall checking
|
|
// checks that the function is dispatched from the vsyscall page by
|
|
// checking RIP. This inherently makes it non-trivial. All trivial
|
|
// rules are non-vsyscall, but not all non-vsyscall rules are trivial.
|
|
type orderedRuleSets struct {
|
|
// hotNonTrivial is the set of hot syscalls that are non-trivial
|
|
// and may or may not require vsyscall checking.
|
|
// They come first and are checked linearly using `hotNonTrivialOrder`.
|
|
hotNonTrivial map[uintptr]singleSyscallRuleSet
|
|
|
|
// hotNonTrivial is the set of hot syscalls that are non-trivial
|
|
// and may or may not require vsyscall checking.
|
|
// They come first and are checked linearly using `hotNonTrivialOrder`.
|
|
hotNonTrivialOrder []uintptr
|
|
|
|
// coldNonTrivial is the set of non-hot syscalls that are non-trivial.
|
|
// They may or may not require vsyscall checking.
|
|
// They come second.
|
|
coldNonTrivial map[uintptr]singleSyscallRuleSet
|
|
|
|
// trivial is the set of syscalls that are trivial. They may or may not be
|
|
// hot, but they may not require vsyscall checking (otherwise they would
|
|
// be non-trivial).
|
|
// They come last. This is because the host kernel will cache the results
|
|
// of these system calls, and will never execute them on the hot path.
|
|
trivial map[uintptr]singleSyscallRuleSet
|
|
}
|
|
|
|
// orderRuleSets converts a set of `RuleSet`s into an `orderedRuleSets`.
|
|
// It orders the rulesets, along with the time to optimize the
|
|
// rules (if any).
|
|
func orderRuleSets(rules []RuleSet, options ProgramOptions) (orderedRuleSets, time.Duration, error) {
|
|
// Do a pass to determine if vsyscall is consistent across syscall numbers.
|
|
vsyscallBySysno := make(map[uintptr]bool)
|
|
for _, rs := range rules {
|
|
for sysno := range rs.Rules.rules {
|
|
if prevVsyscall, ok := vsyscallBySysno[sysno]; ok {
|
|
if prevVsyscall != rs.Vsyscall {
|
|
return orderedRuleSets{}, 0, fmt.Errorf("syscall %d has conflicting vsyscall checking rules", sysno)
|
|
}
|
|
} else {
|
|
vsyscallBySysno[sysno] = rs.Vsyscall
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build a single map of per-syscall syscallRuleActions.
|
|
// We will split this map up later.
|
|
allSyscallRuleActions := make(map[uintptr][]syscallRuleAction)
|
|
defaultActionFn := func() (linux.BPFAction, error) {
|
|
return options.DefaultAction.bpf(saneDefaultAction)
|
|
}
|
|
for _, rs := range rules {
|
|
action, err := rs.Action.bpf(defaultActionFn)
|
|
if err != nil {
|
|
return orderedRuleSets{}, 0, err
|
|
}
|
|
for sysno, rule := range rs.Rules.rules {
|
|
existing, found := allSyscallRuleActions[sysno]
|
|
if !found {
|
|
allSyscallRuleActions[sysno] = []syscallRuleAction{{
|
|
rule: rule,
|
|
action: action,
|
|
}}
|
|
continue
|
|
}
|
|
if existing[len(existing)-1].action == action {
|
|
// If the last action for this syscall is the same, union the rules.
|
|
existing[len(existing)-1].rule = Or{existing[len(existing)-1].rule, rule}
|
|
} else {
|
|
// Otherwise, add it as a new ruleset.
|
|
existing = append(existing, syscallRuleAction{
|
|
rule: rule,
|
|
action: action,
|
|
})
|
|
}
|
|
allSyscallRuleActions[sysno] = existing
|
|
}
|
|
}
|
|
|
|
// Optimize all rules.
|
|
var optimizeDuration time.Duration
|
|
if !options.SkipOptimizations {
|
|
optimizeStart := time.Now()
|
|
for _, ruleActions := range allSyscallRuleActions {
|
|
for i, ra := range ruleActions {
|
|
ra.rule = optimizeSyscallRule(ra.rule)
|
|
ruleActions[i] = ra
|
|
}
|
|
}
|
|
optimizeDuration = time.Since(optimizeStart)
|
|
}
|
|
|
|
// Do a pass that checks which syscall numbers are trivial.
|
|
isTrivial := make(map[uintptr]bool)
|
|
for sysno, ruleActions := range allSyscallRuleActions {
|
|
for _, ra := range ruleActions {
|
|
_, isMatchAll := ra.rule.(MatchAll)
|
|
isVsyscall := vsyscallBySysno[sysno]
|
|
trivial := isMatchAll && !isVsyscall
|
|
if prevTrivial, ok := isTrivial[sysno]; ok {
|
|
isTrivial[sysno] = prevTrivial && trivial
|
|
} else {
|
|
isTrivial[sysno] = trivial
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute the set of non-trivial hot syscalls and their order.
|
|
hotNonTrivialSyscallsIndex := make(map[uintptr]int, len(options.HotSyscalls))
|
|
for i, sysno := range options.HotSyscalls {
|
|
if _, hasRule := allSyscallRuleActions[sysno]; !hasRule {
|
|
continue
|
|
}
|
|
if isTrivial[sysno] {
|
|
continue
|
|
}
|
|
if _, ok := hotNonTrivialSyscallsIndex[sysno]; ok {
|
|
continue
|
|
}
|
|
hotNonTrivialSyscallsIndex[sysno] = i
|
|
}
|
|
hotNonTrivialOrder := make([]uintptr, 0, len(hotNonTrivialSyscallsIndex))
|
|
for sysno := range hotNonTrivialSyscallsIndex {
|
|
hotNonTrivialOrder = append(hotNonTrivialOrder, sysno)
|
|
}
|
|
sort.Slice(hotNonTrivialOrder, func(i, j int) bool {
|
|
return hotNonTrivialSyscallsIndex[hotNonTrivialOrder[i]] < hotNonTrivialSyscallsIndex[hotNonTrivialOrder[j]]
|
|
})
|
|
|
|
// Now split up the map and build the `orderedRuleSets`.
|
|
ors := orderedRuleSets{
|
|
hotNonTrivial: make(map[uintptr]singleSyscallRuleSet),
|
|
hotNonTrivialOrder: hotNonTrivialOrder,
|
|
coldNonTrivial: make(map[uintptr]singleSyscallRuleSet),
|
|
trivial: make(map[uintptr]singleSyscallRuleSet),
|
|
}
|
|
for sysno, ruleActions := range allSyscallRuleActions {
|
|
_, hot := hotNonTrivialSyscallsIndex[sysno]
|
|
trivial := isTrivial[sysno]
|
|
var subMap map[uintptr]singleSyscallRuleSet
|
|
switch {
|
|
case trivial:
|
|
subMap = ors.trivial
|
|
case hot:
|
|
subMap = ors.hotNonTrivial
|
|
default:
|
|
subMap = ors.coldNonTrivial
|
|
}
|
|
subMap[sysno] = singleSyscallRuleSet{
|
|
sysno: sysno,
|
|
vsyscall: vsyscallBySysno[sysno],
|
|
rules: ruleActions,
|
|
}
|
|
}
|
|
|
|
// Log our findings.
|
|
if log.IsLogging(log.Debug) {
|
|
ors.log(log.Debugf)
|
|
}
|
|
|
|
return ors, optimizeDuration, nil
|
|
}
|
|
|
|
// log logs the set of seccomp rules to the given logger.
|
|
func (ors orderedRuleSets) log(logFn func(string, ...any)) {
|
|
logFn("Ordered seccomp rules:")
|
|
for _, sm := range []struct {
|
|
name string
|
|
m map[uintptr]singleSyscallRuleSet
|
|
}{
|
|
{"Hot non-trivial", ors.hotNonTrivial},
|
|
{"Cold non-trivial", ors.coldNonTrivial},
|
|
{"Trivial", ors.trivial},
|
|
} {
|
|
if len(sm.m) == 0 {
|
|
logFn(" %s syscalls: None.", sm.name)
|
|
continue
|
|
}
|
|
logFn(" %s syscalls:", sm.name)
|
|
orderedSysnos := make([]int, 0, len(sm.m))
|
|
for sysno := range sm.m {
|
|
orderedSysnos = append(orderedSysnos, int(sysno))
|
|
}
|
|
sort.Ints(orderedSysnos)
|
|
for _, sysno := range orderedSysnos {
|
|
logFn(" - %s", sm.m[uintptr(sysno)].String())
|
|
}
|
|
}
|
|
logFn("End of ordered seccomp rules.")
|
|
}
|
|
|
|
// render renders all rulesets in the given program.
|
|
func (ors orderedRuleSets) render(program *syscallProgram) error {
|
|
ls := &labelSet{prefix: string("ors")}
|
|
|
|
// totalFrag wraps the entire output of the `render` function.
|
|
totalFrag := program.Record()
|
|
|
|
// Load syscall number into register A.
|
|
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, seccompDataOffsetNR)
|
|
|
|
// Keep track of which syscalls we've already looked for.
|
|
sysnosChecked := make(map[uintptr]struct{})
|
|
|
|
// First render hot syscalls linearly.
|
|
if len(ors.hotNonTrivialOrder) > 0 {
|
|
notHotLabel := ls.NewLabel()
|
|
// hotFrag wraps the "hot syscalls" part of the program.
|
|
// It must either return one of `hotActions`, or jump to `defaultLabel` if
|
|
// the syscall number matched but the vsyscall match failed, or
|
|
// `notHotLabel` if none of the hot syscall numbers matched.
|
|
hotFrag := program.Record()
|
|
possibleActions := ors.renderLinear(program, ls, sysnosChecked, ors.hotNonTrivial, ors.hotNonTrivialOrder, notHotLabel)
|
|
hotFrag.MustHaveJumpedToOrReturned([]label{notHotLabel, defaultLabel}, possibleActions)
|
|
program.Label(notHotLabel)
|
|
}
|
|
|
|
// Now render the cold non-trivial rules as a binary search tree:
|
|
if len(ors.coldNonTrivial) > 0 {
|
|
frag := program.Record()
|
|
noSycallNumberMatch := ls.NewLabel()
|
|
possibleActions, err := ors.renderBST(program, ls, sysnosChecked, ors.coldNonTrivial, noSycallNumberMatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
frag.MustHaveJumpedToOrReturned([]label{noSycallNumberMatch, defaultLabel}, possibleActions)
|
|
program.Label(noSycallNumberMatch)
|
|
}
|
|
|
|
// Finally render the trivial rules as a binary search tree:
|
|
if len(ors.trivial) > 0 {
|
|
frag := program.Record()
|
|
noSycallNumberMatch := ls.NewLabel()
|
|
possibleActions, err := ors.renderBST(program, ls, sysnosChecked, ors.trivial, noSycallNumberMatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
frag.MustHaveJumpedToOrReturned([]label{noSycallNumberMatch, defaultLabel}, possibleActions)
|
|
program.Label(noSycallNumberMatch)
|
|
}
|
|
program.JumpTo(defaultLabel)
|
|
|
|
// Reached the end of the program.
|
|
// Independently verify the set of all possible actions.
|
|
allPossibleActions := make(map[linux.BPFAction]struct{})
|
|
for _, mapping := range []map[uintptr]singleSyscallRuleSet{
|
|
ors.hotNonTrivial,
|
|
ors.coldNonTrivial,
|
|
ors.trivial,
|
|
} {
|
|
for _, ssrs := range mapping {
|
|
for _, ra := range ssrs.rules {
|
|
allPossibleActions[ra.action] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
totalFrag.MustHaveJumpedToOrReturned([]label{defaultLabel}, allPossibleActions)
|
|
return nil
|
|
}
|
|
|
|
// renderLinear renders linear search code that searches for syscall matches
|
|
// in the given order. It assumes the syscall number is loaded into register
|
|
// A. Rulesets for all syscall numbers in `order` must exist in `syscallMap`.
|
|
// It returns the list of possible actions the generated code may return.
|
|
// `alreadyChecked` will be updated with the syscalls that have been checked.
|
|
func (ors orderedRuleSets) renderLinear(program *syscallProgram, ls *labelSet, alreadyChecked map[uintptr]struct{}, syscallMap map[uintptr]singleSyscallRuleSet, order []uintptr, noSycallNumberMatch label) map[linux.BPFAction]struct{} {
|
|
allActions := make(map[linux.BPFAction]struct{})
|
|
for _, sysno := range order {
|
|
ssrs, found := syscallMap[sysno]
|
|
if !found {
|
|
panic(fmt.Sprintf("syscall %d found in linear order but not map", sysno))
|
|
}
|
|
nextSyscall := ls.NewLabel()
|
|
// sysnoFrag wraps the "statements about this syscall number" part of
|
|
// the program. It must either return one of the actions specified in
|
|
// that syscall number's rules (`sysnoActions`), or jump to
|
|
// `nextSyscall`.
|
|
sysnoFrag := program.Record()
|
|
sysnoActions := make(map[linux.BPFAction]struct{})
|
|
for _, ra := range ssrs.rules {
|
|
sysnoActions[ra.action] = struct{}{}
|
|
allActions[ra.action] = struct{}{}
|
|
}
|
|
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, uint32(ssrs.sysno), nextSyscall)
|
|
ssrs.Render(program, ls, defaultLabel)
|
|
sysnoFrag.MustHaveJumpedToOrReturned([]label{nextSyscall, defaultLabel}, sysnoActions)
|
|
program.Label(nextSyscall)
|
|
}
|
|
program.JumpTo(noSycallNumberMatch)
|
|
for _, sysno := range order {
|
|
alreadyChecked[sysno] = struct{}{}
|
|
}
|
|
return allActions
|
|
}
|
|
|
|
// renderBST renders a binary search tree that searches the given map of
|
|
// syscalls. It assumes the syscall number is loaded into register A.
|
|
// It returns the list of possible actions the generated code may return.
|
|
// `alreadyChecked` will be updated with the syscalls that the BST has
|
|
// searched.
|
|
func (ors orderedRuleSets) renderBST(program *syscallProgram, ls *labelSet, alreadyChecked map[uintptr]struct{}, syscallMap map[uintptr]singleSyscallRuleSet, noSycallNumberMatch label) (map[linux.BPFAction]struct{}, error) {
|
|
possibleActions := make(map[linux.BPFAction]struct{})
|
|
orderedSysnos := make([]uintptr, 0, len(syscallMap))
|
|
for sysno, ruleActions := range syscallMap {
|
|
orderedSysnos = append(orderedSysnos, sysno)
|
|
for _, ra := range ruleActions.rules {
|
|
possibleActions[ra.action] = struct{}{}
|
|
}
|
|
}
|
|
sort.Slice(orderedSysnos, func(i, j int) bool {
|
|
return orderedSysnos[i] < orderedSysnos[j]
|
|
})
|
|
frag := program.Record()
|
|
root := createBST(orderedSysnos)
|
|
root.root = true
|
|
knownRng := knownRange{
|
|
lowerBoundExclusive: -1,
|
|
// sysno fits in 32 bits, so this is definitely out of bounds:
|
|
upperBoundExclusive: 1 << 32,
|
|
previouslyChecked: alreadyChecked,
|
|
}
|
|
if err := root.traverse(renderBSTTraversal, knownRng, syscallMap, program, noSycallNumberMatch); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := root.traverse(renderBSTRules, knownRng, syscallMap, program, noSycallNumberMatch); err != nil {
|
|
return nil, err
|
|
}
|
|
frag.MustHaveJumpedToOrReturned([]label{noSycallNumberMatch, defaultLabel}, possibleActions)
|
|
for sysno := range syscallMap {
|
|
alreadyChecked[sysno] = struct{}{}
|
|
}
|
|
return possibleActions, nil
|
|
}
|
|
|
|
// createBST converts sorted syscall slice into a balanced BST.
|
|
// Panics if syscalls is empty.
|
|
func createBST(syscalls []uintptr) *node {
|
|
i := len(syscalls) / 2
|
|
parent := node{value: syscalls[i]}
|
|
if i > 0 {
|
|
parent.left = createBST(syscalls[:i])
|
|
}
|
|
if i+1 < len(syscalls) {
|
|
parent.right = createBST(syscalls[i+1:])
|
|
}
|
|
return &parent
|
|
}
|
|
|
|
// renderBSTTraversal renders the traversal bytecode for a binary search tree.
|
|
// The outline of the code is as follows, given a BST with:
|
|
//
|
|
// 22
|
|
// / \
|
|
// 9 24
|
|
// / / \
|
|
// 8 23 50
|
|
//
|
|
// index_22: // SYS_PIPE(22), root
|
|
// (A < 22) ? goto index_9 : continue
|
|
// (A > 22) ? goto index_24 : continue
|
|
// goto checkArgs_22
|
|
//
|
|
// index_9: // SYS_MMAP(9), single child
|
|
// (A < 9) ? goto index_8 : continue
|
|
// (A == 9) ? continue : goto defaultLabel
|
|
// goto checkArgs_9
|
|
//
|
|
// index_8: // SYS_LSEEK(8), leaf
|
|
// (A == 8) ? continue : goto defaultLabel
|
|
// goto checkArgs_8
|
|
//
|
|
// index_24: // SYS_SCHED_YIELD(24)
|
|
// (A < 24) ? goto index_23 : continue
|
|
// (A > 22) ? goto index_50 : continue
|
|
// goto checkArgs_24
|
|
//
|
|
// index_23: // SYS_SELECT(23), leaf with parent nodes adjacent in value
|
|
// # Notice that we do not check for equality at all here, since we've
|
|
// # already established that the syscall number is 23 from the
|
|
// # two parent nodes that we've already traversed.
|
|
// # This is tracked in the `rng knownRange` argument during traversal.
|
|
// goto rules_23
|
|
//
|
|
// index_50: // SYS_LISTEN(50), leaf
|
|
// (A == 50) ? continue : goto defaultLabel
|
|
// goto checkArgs_50
|
|
//
|
|
// All of the "checkArgs_XYZ" labels are not defined in this function; they
|
|
// are created using the `renderBSTRules` function, which is expected to be
|
|
// called after this one on the entire BST.
|
|
func renderBSTTraversal(n *node, rng knownRange, syscallMap map[uintptr]singleSyscallRuleSet, program *syscallProgram, searchFailed label) error {
|
|
// Root node is never referenced by label, skip it.
|
|
if !n.root {
|
|
program.Label(n.label())
|
|
}
|
|
sysno := n.value
|
|
nodeFrag := program.Record()
|
|
checkArgsLabel := label(fmt.Sprintf("checkArgs_%d", sysno))
|
|
if n.left != nil {
|
|
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, uint32(sysno), n.left.label())
|
|
rng.lowerBoundExclusive = int(sysno - 1)
|
|
}
|
|
if n.right != nil {
|
|
program.If(bpf.Jmp|bpf.Jgt|bpf.K, uint32(sysno), n.right.label())
|
|
rng.upperBoundExclusive = int(sysno + 1)
|
|
}
|
|
if rng.lowerBoundExclusive != int(sysno-1) || rng.upperBoundExclusive != int(sysno+1) {
|
|
// If the previous BST nodes we traversed haven't fully established
|
|
// that the current node's syscall value is exactly `sysno`, we still
|
|
// need to verify it.
|
|
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, uint32(sysno), searchFailed)
|
|
}
|
|
program.JumpTo(checkArgsLabel)
|
|
nodeFrag.MustHaveJumpedTo(n.left.label(), n.right.label(), checkArgsLabel, searchFailed)
|
|
return nil
|
|
}
|
|
|
|
// renderBSTRules renders the `checkArgs_XYZ` labels that `renderBSTTraversal`
|
|
// jumps to as part of the BST traversal code. It contains all the
|
|
// argument-specific syscall rules for each syscall number.
|
|
func renderBSTRules(n *node, rng knownRange, syscallMap map[uintptr]singleSyscallRuleSet, program *syscallProgram, searchFailed label) error {
|
|
sysno := n.value
|
|
checkArgsLabel := label(fmt.Sprintf("checkArgs_%d", sysno))
|
|
program.Label(checkArgsLabel)
|
|
ruleSetsFrag := program.Record()
|
|
possibleActions := make(map[linux.BPFAction]struct{})
|
|
for _, ra := range syscallMap[sysno].rules {
|
|
possibleActions[ra.action] = struct{}{}
|
|
}
|
|
nodeLabelSet := &labelSet{prefix: string(n.label())}
|
|
syscallMap[sysno].Render(program, nodeLabelSet, defaultLabel)
|
|
ruleSetsFrag.MustHaveJumpedToOrReturned(
|
|
[]label{
|
|
defaultLabel, // Either we jumped to the default label (if the rules didn't match)...
|
|
},
|
|
possibleActions, // ... or we returned one of the actions of the rulesets.
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// node represents a tree node.
|
|
type node struct {
|
|
value uintptr
|
|
left *node
|
|
right *node
|
|
root bool
|
|
}
|
|
|
|
// label returns the label corresponding to this node.
|
|
//
|
|
// If n is nil, then the defaultLabel is returned.
|
|
func (n *node) label() label {
|
|
if n == nil {
|
|
return defaultLabel
|
|
}
|
|
return label(fmt.Sprintf("node_%d", n.value))
|
|
}
|
|
|
|
// knownRange represents the known set of node numbers that we've
|
|
// already checked. This is used as part of BST traversal.
|
|
type knownRange struct {
|
|
lowerBoundExclusive int
|
|
upperBoundExclusive int
|
|
|
|
// alreadyChecked is a set of node values that were already checked
|
|
// earlier in the program (prior to the BST being built).
|
|
// It is *not* updated during BST traversal.
|
|
previouslyChecked map[uintptr]struct{}
|
|
}
|
|
|
|
// withLowerBoundExclusive returns an updated `knownRange` with the given
|
|
// new exclusive lower bound. The actual exclusive lower bound of the
|
|
// returned `knownRange` may be higher, in case `previouslyChecked` covers
|
|
// more numbers.
|
|
func (kr knownRange) withLowerBoundExclusive(newLowerBoundExclusive int) knownRange {
|
|
nkr := knownRange{
|
|
lowerBoundExclusive: newLowerBoundExclusive,
|
|
upperBoundExclusive: kr.upperBoundExclusive,
|
|
previouslyChecked: kr.previouslyChecked,
|
|
}
|
|
for ; nkr.lowerBoundExclusive < nkr.upperBoundExclusive; nkr.lowerBoundExclusive++ {
|
|
if _, ok := nkr.previouslyChecked[uintptr(nkr.lowerBoundExclusive+1)]; !ok {
|
|
break
|
|
}
|
|
}
|
|
return nkr
|
|
}
|
|
|
|
// withUpperBoundExclusive returns an updated `knownRange` with the given
|
|
// new exclusive upper bound. The actual exclusive upper bound of the
|
|
// returned `knownRange` may be lower, in case `previouslyChecked` covers
|
|
// more numbers.
|
|
func (kr knownRange) withUpperBoundExclusive(newUpperBoundExclusive int) knownRange {
|
|
nkr := knownRange{
|
|
lowerBoundExclusive: kr.lowerBoundExclusive,
|
|
upperBoundExclusive: newUpperBoundExclusive,
|
|
previouslyChecked: kr.previouslyChecked,
|
|
}
|
|
for ; nkr.lowerBoundExclusive < nkr.upperBoundExclusive; nkr.upperBoundExclusive-- {
|
|
if _, ok := nkr.previouslyChecked[uintptr(nkr.upperBoundExclusive-1)]; !ok {
|
|
break
|
|
}
|
|
}
|
|
return nkr
|
|
}
|
|
|
|
// traverseFunc is called as the BST is traversed.
|
|
type traverseFunc func(*node, knownRange, map[uintptr]singleSyscallRuleSet, *syscallProgram, label) error
|
|
|
|
func (n *node) traverse(fn traverseFunc, kr knownRange, syscallMap map[uintptr]singleSyscallRuleSet, program *syscallProgram, searchFailed label) error {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
if err := fn(n, kr, syscallMap, program, searchFailed); err != nil {
|
|
return err
|
|
}
|
|
if err := n.left.traverse(
|
|
fn,
|
|
kr.withUpperBoundExclusive(int(n.value)),
|
|
syscallMap,
|
|
program,
|
|
searchFailed,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return n.right.traverse(
|
|
fn,
|
|
kr.withLowerBoundExclusive(int(n.value)),
|
|
syscallMap,
|
|
program,
|
|
searchFailed,
|
|
)
|
|
}
|
|
|
|
// DataAsBPFInput converts a linux.SeccompData to a bpf.Input.
|
|
// It uses `buf` as scratch buffer; this buffer must be wide enough
|
|
// to accommodate a mashaled version of `d`.
|
|
func DataAsBPFInput(d *linux.SeccompData, buf []byte) bpf.Input {
|
|
if len(buf) < d.SizeBytes() {
|
|
panic(fmt.Sprintf("buffer must be at least %d bytes long", d.SizeBytes()))
|
|
}
|
|
d.MarshalUnsafe(buf)
|
|
return buf[:d.SizeBytes()]
|
|
}
|