Files
visor/pkg/seccomp/seccomp_rules.go
2026-02-25 11:07:15 -08:00

1023 lines
31 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
import (
"fmt"
"sort"
"strings"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/bpf"
)
// The offsets are based on the following struct in include/linux/seccomp.h.
//
// struct seccomp_data {
// int nr;
// __u32 arch;
// __u64 instruction_pointer;
// __u64 args[6];
// };
const (
seccompDataOffsetNR = 0
seccompDataOffsetArch = 4
seccompDataOffsetIPLow = 8
seccompDataOffsetIPHigh = 12
seccompDataOffsetArgs = 16
)
func seccompDataOffsetArgLow(i int) uint32 {
return uint32(seccompDataOffsetArgs + i*8)
}
func seccompDataOffsetArgHigh(i int) uint32 {
return seccompDataOffsetArgLow(i) + 4
}
// ValueMatcher verifies a numerical value, typically a syscall argument
// or RIP value.
type ValueMatcher interface {
// String returns a human-readable representation of the match rule.
// If the returned string contains "VAL", it will be replaced with
// the symbolic name of the value being matched against.
String() string
// Repr returns a string that will be used for asserting equality between
// two `ValueMatcher` instances. It must therefore be unique to the
// `ValueMatcher` implementation and to its parameters.
// It must not contain the character ";".
Repr() string
// Render should add rules to the given program that verify the value
// loadable from `value` matches this rule or not.
// The rules should indicate this by either jumping to `labelSet.Matched()`
// or `labelSet.Mismatched()`. They may not fall through.
Render(program *syscallProgram, labelSet *labelSet, value matchedValue)
// InterestingValues returns a list of values that may be interesting to
// test this `ValueMatcher` against.
InterestingValues() []uint64
}
// halfValueMatcher verifies a 32-bit value.
type halfValueMatcher interface {
// String returns a human-friendly representation of the check being done
// against the 32-bit value.
// The string "x.(high|low) {{halfValueMatcher.String()}}" should read well,
// e.g. "x.low == 0xffff".
String() string
// Repr returns a string that will be used for asserting equality between
// two `halfValueMatcher` instances. It must therefore be unique to the
// `halfValueMatcher` implementation and to its parameters.
// It must not contain the character ";".
Repr() string
// HalfRender should add rules to the given program that verify the value
// loaded into the "A" register matches this 32-bit value or not.
// The rules should indicate this by either jumping to `labelSet.Matched()`
// or `labelSet.Mismatched()`. They may not fall through.
HalfRender(program *syscallProgram, labelSet *labelSet)
// InterestingValues returns a list of values that may be interesting to
// test this `halfValueMatcher` against.
InterestingValues() []uint32
}
// halfAnyValue implements `halfValueMatcher` and matches any value.
type halfAnyValue struct{}
// String implements `halfValueMatcher.String`.
func (halfAnyValue) String() string {
return "== *"
}
// Repr implements `halfValueMatcher.Repr`.
func (halfAnyValue) Repr() string {
return "halfAnyValue"
}
// HalfRender implements `halfValueMatcher.HalfRender`.
func (halfAnyValue) HalfRender(program *syscallProgram, labelSet *labelSet) {
program.JumpTo(labelSet.Matched())
}
// halfEqualTo implements `halfValueMatcher` and matches a specific 32-bit value.
type halfEqualTo uint32
// String implements `halfValueMatcher.String`.
func (heq halfEqualTo) String() string {
if heq == 0 {
return "== 0"
}
return fmt.Sprintf("== %#x", uint32(heq))
}
// Repr implements `halfValueMatcher.Repr`.
func (heq halfEqualTo) Repr() string {
return fmt.Sprintf("halfEq(%#x)", uint32(heq))
}
// HalfRender implements `halfValueMatcher.HalfRender`.
func (heq halfEqualTo) HalfRender(program *syscallProgram, labelSet *labelSet) {
program.If(bpf.Jmp|bpf.Jeq|bpf.K, uint32(heq), labelSet.Matched())
program.JumpTo(labelSet.Mismatched())
}
// halfNotSet implements `halfValueMatcher` and matches using the "set"
// bitwise operation.
type halfNotSet uint32
// String implements `halfValueMatcher.String`.
func (hns halfNotSet) String() string {
return fmt.Sprintf("& %#x == 0", uint32(hns))
}
// Repr implements `halfValueMatcher.Repr`.
func (hns halfNotSet) Repr() string {
return fmt.Sprintf("halfNotSet(%#x)", uint32(hns))
}
// HalfRender implements `halfValueMatcher.HalfRender`.
func (hns halfNotSet) HalfRender(program *syscallProgram, labelSet *labelSet) {
program.If(bpf.Jmp|bpf.Jset|bpf.K, uint32(hns), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// halfMaskedEqual implements `halfValueMatcher` and verifies that the value
// is equal after applying a bit mask.
type halfMaskedEqual struct {
mask uint32
value uint32
}
// String implements `halfValueMatcher.String`.
func (hmeq halfMaskedEqual) String() string {
if hmeq.value == 0 {
return fmt.Sprintf("& %#x == 0", hmeq.mask)
}
return fmt.Sprintf("& %#x == %#x", hmeq.mask, hmeq.value)
}
// Repr implements `halfValueMatcher.Repr`.
func (hmeq halfMaskedEqual) Repr() string {
return fmt.Sprintf("halfMaskedEqual(%#x, %#x)", hmeq.mask, hmeq.value)
}
// HalfRender implements `halfValueMatcher.HalfRender`.
func (hmeq halfMaskedEqual) HalfRender(program *syscallProgram, labelSet *labelSet) {
program.Stmt(bpf.Alu|bpf.And|bpf.K, hmeq.mask)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, hmeq.value, labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// splitMatcher implements `ValueMatcher` and verifies each half of the 64-bit
// value independently (with AND semantics).
// It implements `ValueMatcher`, but is never used directly in seccomp filter
// rules. Rather, it acts as an intermediate representation for the rules that
// can be expressed as an AND of two 32-bit values.
type splitMatcher struct {
// repr is the `Repr()` of the original `ValueMatcher` (pre-split).
repr string
// highMatcher is the half-value matcher to verify the high 32 bits.
highMatcher halfValueMatcher
// lowMatcher is the half-value matcher to verify the low 32 bits.
lowMatcher halfValueMatcher
}
// String implements `ValueMatcher.String`.
func (sm splitMatcher) String() string {
if sm.repr == "" {
_, highIsAnyValue := sm.highMatcher.(halfAnyValue)
_, lowIsAnyValue := sm.lowMatcher.(halfAnyValue)
if highIsAnyValue && lowIsAnyValue {
return "== *"
}
if highIsAnyValue {
return fmt.Sprintf("VAL.low %s", sm.lowMatcher.String())
}
if lowIsAnyValue {
return fmt.Sprintf("VAL.high %s", sm.highMatcher.String())
}
return fmt.Sprintf("(VAL.high %s && VAL.low %s)", sm.highMatcher.String(), sm.lowMatcher.String())
}
return sm.repr
}
// Repr implements `ValueMatcher.Repr`.
func (sm splitMatcher) Repr() string {
if sm.repr == "" {
_, highIsAnyValue := sm.highMatcher.(halfAnyValue)
_, lowIsAnyValue := sm.lowMatcher.(halfAnyValue)
if highIsAnyValue && lowIsAnyValue {
return "split(*)"
}
if highIsAnyValue {
return fmt.Sprintf("low=%s", sm.lowMatcher.Repr())
}
if lowIsAnyValue {
return fmt.Sprintf("high=%s", sm.highMatcher.Repr())
}
return fmt.Sprintf("(high=%s && low=%s)", sm.highMatcher.Repr(), sm.lowMatcher.Repr())
}
return sm.repr
}
// Render implements `ValueMatcher.Render`.
func (sm splitMatcher) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
_, highIsAny := sm.highMatcher.(halfAnyValue)
_, lowIsAny := sm.lowMatcher.(halfAnyValue)
if highIsAny && lowIsAny {
program.JumpTo(labelSet.Matched())
return
}
if highIsAny {
value.LoadLow32Bits()
sm.lowMatcher.HalfRender(program, labelSet)
return
}
if lowIsAny {
value.LoadHigh32Bits()
sm.highMatcher.HalfRender(program, labelSet)
return
}
// We render the "low" bits first on the assumption that most syscall
// arguments fit within 32-bits, and those rules actually only care
// about the value of the low 32 bits. This way, we only check the
// high 32 bits if the low 32 bits have already matched.
lowLabels := labelSet.Push("low", labelSet.NewLabel(), labelSet.Mismatched())
lowFrag := program.Record()
value.LoadLow32Bits()
sm.lowMatcher.HalfRender(program, lowLabels)
lowFrag.MustHaveJumpedTo(lowLabels.Matched(), labelSet.Mismatched())
program.Label(lowLabels.Matched())
highFrag := program.Record()
value.LoadHigh32Bits()
sm.highMatcher.HalfRender(program, labelSet.Push("high", labelSet.Matched(), labelSet.Mismatched()))
highFrag.MustHaveJumpedTo(labelSet.Matched(), labelSet.Mismatched())
}
// high32BitsMatch returns a `splitMatcher` that only matches the high 32 bits
// of a 64-bit value.
func high32BitsMatch(hvm halfValueMatcher) splitMatcher {
return splitMatcher{
highMatcher: hvm,
lowMatcher: halfAnyValue{},
}
}
// low32BitsMatch returns a `splitMatcher` that only matches the low 32 bits
// of a 64-bit value.
func low32BitsMatch(hvm halfValueMatcher) splitMatcher {
return splitMatcher{
highMatcher: halfAnyValue{},
lowMatcher: hvm,
}
}
// splittableValueMatcher should be implemented by `ValueMatcher` that can
// be expressed as a `splitMatcher`.
type splittableValueMatcher interface {
// split converts this `ValueMatcher` into a `splitMatcher`.
split() splitMatcher
}
// renderSplittable is a helper function for the `ValueMatcher.Render`
// implementation of `splittableValueMatcher`s.
func renderSplittable(sm splittableValueMatcher, program *syscallProgram, labelSet *labelSet, value matchedValue) {
sm.split().Render(program, labelSet, value)
}
// high32Bits returns the higher 32-bits of the given value.
func high32Bits(val uintptr) uint32 {
return uint32(val >> 32)
}
// low32Bits returns the lower 32-bits of the given value.
func low32Bits(val uintptr) uint32 {
return uint32(val)
}
// AnyValue is marker to indicate any value will be accepted.
// It implements ValueMatcher.
type AnyValue struct{}
// String implements `ValueMatcher.String`.
func (AnyValue) String() string {
return "== *"
}
// Repr implements `ValueMatcher.Repr`.
func (av AnyValue) Repr() string {
return av.String()
}
// Render implements `ValueMatcher.Render`.
func (av AnyValue) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
program.JumpTo(labelSet.Matched())
}
// EqualTo specifies a value that needs to be strictly matched.
// It implements ValueMatcher.
type EqualTo uintptr
// String implements `ValueMatcher.String`.
func (eq EqualTo) String() string {
if eq == 0 {
return "== 0"
}
return fmt.Sprintf("== %#x", uintptr(eq))
}
// Repr implements `ValueMatcher.Repr`.
func (eq EqualTo) Repr() string {
return eq.String()
}
// Render implements `ValueMatcher.Render`.
func (eq EqualTo) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
renderSplittable(eq, program, labelSet, value)
}
// split implements `splittableValueMatcher.split`.
func (eq EqualTo) split() splitMatcher {
return splitMatcher{
repr: eq.Repr(),
highMatcher: halfEqualTo(high32Bits(uintptr(eq))),
lowMatcher: halfEqualTo(low32Bits(uintptr(eq))),
}
}
// NotEqual specifies a value that is strictly not equal.
type NotEqual uintptr
// String implements `ValueMatcher.String`.
func (ne NotEqual) String() string {
return fmt.Sprintf("!= %#x", uintptr(ne))
}
// Repr implements `ValueMatcher.Repr`.
func (ne NotEqual) Repr() string {
return ne.String()
}
// Render implements `ValueMatcher.Render`.
func (ne NotEqual) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
// Note that `NotEqual` is *not* a splittable rule by itself, because it is not the
// conjunction of two `halfValueMatchers` (it is the *disjunction* of them).
// However, it is also the exact inverse of `EqualTo`.
// Therefore, we can use `EqualTo` here, and simply invert the
// matched/mismatched labels.
EqualTo(ne).Render(program, labelSet.Push("inverted", labelSet.Mismatched(), labelSet.Matched()), value)
}
// GreaterThan specifies a value that needs to be strictly smaller.
type GreaterThan uintptr
// String implements `ValueMatcher.String`.
func (gt GreaterThan) String() string {
return fmt.Sprintf("> %#x", uintptr(gt))
}
// Repr implements `ValueMatcher.Repr`.
func (gt GreaterThan) Repr() string {
return gt.String()
}
// Render implements `ValueMatcher.Render`.
func (gt GreaterThan) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
high := high32Bits(uintptr(gt))
// Assert the higher 32bits are greater than or equal.
// arg_high >= high ? continue : violation (arg_high < high)
value.LoadHigh32Bits()
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, high, labelSet.Mismatched())
// arg_high == high ? continue : success (arg_high > high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, labelSet.Matched())
// Assert that the lower 32bits are greater.
// arg_low > low ? continue/success : violation (arg_high == high and arg_low <= low)
value.LoadLow32Bits()
program.IfNot(bpf.Jmp|bpf.Jgt|bpf.K, low32Bits(uintptr(gt)), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// GreaterThanOrEqual specifies a value that needs to be smaller or equal.
type GreaterThanOrEqual uintptr
// String implements `ValueMatcher.String`.
func (ge GreaterThanOrEqual) String() string {
return fmt.Sprintf(">= %#x", uintptr(ge))
}
// Repr implements `ValueMatcher.Repr`.
func (ge GreaterThanOrEqual) Repr() string {
return ge.String()
}
// Render implements `ValueMatcher.Render`.
func (ge GreaterThanOrEqual) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
high := high32Bits(uintptr(ge))
// Assert the higher 32bits are greater than or equal.
// arg_high >= high ? continue : violation (arg_high < high)
value.LoadHigh32Bits()
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, high, labelSet.Mismatched())
// arg_high == high ? continue : success (arg_high > high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, labelSet.Matched())
// Assert that the lower 32bits are greater or equal (assuming the
// higher bits are equal).
// arg_low >= low ? continue/success : violation (arg_high == high and arg_low < low)
value.LoadLow32Bits()
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, low32Bits(uintptr(ge)), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// LessThan specifies a value that needs to be strictly greater.
type LessThan uintptr
// String implements `ValueMatcher.String`.
func (lt LessThan) String() string {
return fmt.Sprintf("< %#x", uintptr(lt))
}
// Repr implements `ValueMatcher.Repr`.
func (lt LessThan) Repr() string {
return lt.String()
}
// Render implements `ValueMatcher.Render`.
func (lt LessThan) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
high := high32Bits(uintptr(lt))
// Assert the higher 32bits are less than or equal.
// arg_high > high ? violation : continue
value.LoadHigh32Bits()
program.If(bpf.Jmp|bpf.Jgt|bpf.K, high, labelSet.Mismatched())
// arg_high == high ? continue : success (arg_high < high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, labelSet.Matched())
// Assert that the lower 32bits are less (assuming the
// higher bits are equal).
// arg_low >= low ? violation : continue
value.LoadLow32Bits()
program.If(bpf.Jmp|bpf.Jge|bpf.K, low32Bits(uintptr(lt)), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// LessThanOrEqual specifies a value that needs to be greater or equal.
type LessThanOrEqual uintptr
// String implements `ValueMatcher.String`.
func (le LessThanOrEqual) String() string {
return fmt.Sprintf("<= %#x", uintptr(le))
}
// Repr implements `ValueMatcher.Repr`.
func (le LessThanOrEqual) Repr() string {
return le.String()
}
// Render implements `ValueMatcher.Render`.
func (le LessThanOrEqual) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
high := high32Bits(uintptr(le))
// Assert the higher 32bits are less than or equal.
// assert arg_high > high ? violation : continue
value.LoadHigh32Bits()
program.If(bpf.Jmp|bpf.Jgt|bpf.K, high, labelSet.Mismatched())
// arg_high == high ? continue : success
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, labelSet.Matched())
// Assert the lower bits are less than or equal (assuming
// the higher bits are equal).
// arg_low > low ? violation : success
value.LoadLow32Bits()
program.If(bpf.Jmp|bpf.Jgt|bpf.K, low32Bits(uintptr(le)), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// NonNegativeFD ensures that an FD argument is a non-negative int32.
type NonNegativeFD struct{}
// String implements `ValueMatcher.String`.
func (NonNegativeFD) String() string {
return "is non-negative FD"
}
// Repr implements `ValueMatcher.Repr`.
func (NonNegativeFD) Repr() string {
return "NonNegativeFD"
}
// Render implements `ValueMatcher.Render`.
func (nnfd NonNegativeFD) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
renderSplittable(nnfd, program, labelSet, value)
}
// split implements `splittableValueMatcher.split`.
func (nnfd NonNegativeFD) split() splitMatcher {
return splitMatcher{
repr: nnfd.Repr(),
// FDs are 32 bits, so the high 32 bits must all be zero.
// Negative int32 has the MSB (31st bit) set.
// So the low 32bits of the FD value must not have the 31st bit set.
highMatcher: halfEqualTo(0),
lowMatcher: halfNotSet(1 << 31),
}
}
// MaskedEqual specifies a value that matches the input after the input is
// masked (bitwise &) against the given mask. It implements `ValueMatcher`.
type maskedEqual struct {
mask uintptr
value uintptr
}
// String implements `ValueMatcher.String`.
func (me maskedEqual) String() string {
return fmt.Sprintf("& %#x == %#x", me.mask, me.value)
}
// Repr implements `ValueMatcher.Repr`.
func (me maskedEqual) Repr() string {
return me.String()
}
// Render implements `ValueMatcher.Render`.
func (me maskedEqual) Render(program *syscallProgram, labelSet *labelSet, value matchedValue) {
renderSplittable(me, program, labelSet, value)
}
// split implements `splittableValueMatcher.Split`.
func (me maskedEqual) split() splitMatcher {
return splitMatcher{
repr: me.Repr(),
highMatcher: halfMaskedEqual{high32Bits(me.mask), high32Bits(me.value)},
lowMatcher: halfMaskedEqual{low32Bits(me.mask), low32Bits(me.value)},
}
}
// MaskedEqual specifies a value that matches the input after the input is
// masked (bitwise &) against the given mask. Can be used to verify that input
// only includes certain approved flags.
func MaskedEqual(mask, value uintptr) ValueMatcher {
return maskedEqual{
mask: mask,
value: value,
}
}
// BitsAllowlist specifies that a value can only have non-zero bits within
// the mask specified in `allowlist`. It implements `ValueMatcher`.
func BitsAllowlist(allowlist uintptr) ValueMatcher {
return MaskedEqual(^allowlist, 0)
}
// SyscallRule expresses a set of rules to verify the arguments of a specific
// syscall.
type SyscallRule interface {
// Render renders the syscall rule in the given `program`.
// The emitted instructions **must** end up jumping to either
// `labelSet.Matched()` or `labelSet.Mismatched()`; they may
// not "fall through" to whatever instructions will be added
// next into the program.
Render(program *syscallProgram, labelSet *labelSet)
// Copy returns a copy of this `SyscallRule`.
Copy() SyscallRule
// Recurse should call the given function on all `SyscallRule`s that are
// part of this `SyscallRule`, and should replace them with the returned
// `SyscallRule`. For example, conjunctive rules should call the given
// function on each of the `SyscallRule`s that they are ANDing, replacing
// them with the rule returned by the function.
Recurse(func(SyscallRule) SyscallRule)
// String returns a human-readable string representing what the rule does.
String() string
}
// MatchAll implements `SyscallRule` and matches everything.
type MatchAll struct{}
// Render implements `SyscallRule.Render`.
func (MatchAll) Render(program *syscallProgram, labelSet *labelSet) {
program.JumpTo(labelSet.Matched())
}
// Copy implements `SyscallRule.Copy`.
func (MatchAll) Copy() SyscallRule {
return MatchAll{}
}
// Recurse implements `SyscallRule.Recurse`.
func (MatchAll) Recurse(func(SyscallRule) SyscallRule) {}
// String implements `SyscallRule.String`.
func (MatchAll) String() string { return "true" }
// Or expresses an "OR" (a disjunction) over a set of `SyscallRule`s.
// An `Or` may not be empty.
type Or []SyscallRule
// Render implements `SyscallRule.Render`.
func (or Or) Render(program *syscallProgram, labelSet *labelSet) {
if len(or) == 0 {
panic("Or expression cannot be empty")
}
// If `len(or) == 1`, this will be optimized away to be the same as
// rendering the single rule in the disjunction.
for i, rule := range or {
frag := program.Record()
nextRuleLabel := labelSet.NewLabel()
rule.Render(program, labelSet.Push(fmt.Sprintf("or[%d]", i), labelSet.Matched(), nextRuleLabel))
frag.MustHaveJumpedTo(labelSet.Matched(), nextRuleLabel)
program.Label(nextRuleLabel)
}
program.JumpTo(labelSet.Mismatched())
}
// Copy implements `SyscallRule.Copy`.
func (or Or) Copy() SyscallRule {
orCopy := make([]SyscallRule, len(or))
for i, rule := range or {
orCopy[i] = rule.Copy()
}
return Or(orCopy)
}
// Recurse implements `SyscallRule.Recurse`.
func (or Or) Recurse(fn func(SyscallRule) SyscallRule) {
for i, rule := range or {
or[i] = fn(rule)
}
}
// String implements `SyscallRule.String`.
func (or Or) String() string {
switch len(or) {
case 0:
return "invalid"
case 1:
return or[0].String()
default:
var sb strings.Builder
sb.WriteRune('(')
for i, rule := range or {
if i != 0 {
sb.WriteString(" || ")
}
sb.WriteString(rule.String())
}
sb.WriteRune(')')
return sb.String()
}
}
// And expresses an "AND" (a conjunction) over a set of `SyscallRule`s.
// An `And` may not be empty.
type And []SyscallRule
// Render implements `SyscallRule.Render`.
func (and And) Render(program *syscallProgram, labelSet *labelSet) {
if len(and) == 0 {
panic("And expression cannot be empty")
}
// If `len(and) == 1`, this will be optimized away to be the same as
// rendering the single rule in the conjunction.
for i, rule := range and {
frag := program.Record()
nextRuleLabel := labelSet.NewLabel()
rule.Render(program, labelSet.Push(fmt.Sprintf("and[%d]", i), nextRuleLabel, labelSet.Mismatched()))
frag.MustHaveJumpedTo(nextRuleLabel, labelSet.Mismatched())
program.Label(nextRuleLabel)
}
program.JumpTo(labelSet.Matched())
}
// Copy implements `SyscallRule.Copy`.
func (and And) Copy() SyscallRule {
andCopy := make([]SyscallRule, len(and))
for i, rule := range and {
andCopy[i] = rule.Copy()
}
return And(andCopy)
}
// Recurse implements `SyscallRule.Recurse`.
func (and And) Recurse(fn func(SyscallRule) SyscallRule) {
for i, rule := range and {
and[i] = fn(rule)
}
}
// String implements `SyscallRule.String`.
func (and And) String() string {
switch len(and) {
case 0:
return "invalid"
case 1:
return and[0].String()
default:
var sb strings.Builder
sb.WriteRune('(')
for i, rule := range and {
if i != 0 {
sb.WriteString(" && ")
}
sb.WriteString(rule.String())
}
sb.WriteRune(')')
return sb.String()
}
}
// PerArg implements SyscallRule and verifies the syscall arguments and RIP.
//
// For example:
//
// rule := PerArg{
// EqualTo(linux.ARCH_GET_FS | linux.ARCH_SET_FS), // arg0
// }
type PerArg [7]ValueMatcher // 6 arguments + RIP
// RuleIP indicates what rules in the Rule array have to be applied to
// instruction pointer.
const RuleIP = 6
// clone returns a copy of this `PerArg`.
// It is more efficient than `Copy` because it returns a `PerArg`
// directly, rather than a `SyscallRule` interface.
func (pa PerArg) clone() PerArg {
return PerArg{
pa[0],
pa[1],
pa[2],
pa[3],
pa[4],
pa[5],
pa[6],
}
}
// Copy implements `SyscallRule.Copy`.
func (pa PerArg) Copy() SyscallRule {
return pa.clone()
}
// Render implements `SyscallRule.Render`.
func (pa PerArg) Render(program *syscallProgram, labelSet *labelSet) {
for i, arg := range pa {
if arg == nil {
continue
}
frag := program.Record()
nextArgLabel := labelSet.NewLabel()
labelSuffix := fmt.Sprintf("arg[%d]", i)
// Determine the data offset for low and high bits of input.
dataOffsetLow := seccompDataOffsetArgLow(i)
dataOffsetHigh := seccompDataOffsetArgHigh(i)
if i == RuleIP {
dataOffsetLow = seccompDataOffsetIPLow
dataOffsetHigh = seccompDataOffsetIPHigh
labelSuffix = "rip"
}
ls := labelSet.Push(labelSuffix, nextArgLabel, labelSet.Mismatched())
arg.Render(program, ls, matchedValue{
program: program,
dataOffsetHigh: dataOffsetHigh,
dataOffsetLow: dataOffsetLow,
})
frag.MustHaveJumpedTo(ls.Matched(), ls.Mismatched())
program.Label(nextArgLabel)
}
// Matched all argument-wise rules, jump to the final rule matched label.
program.JumpTo(labelSet.Matched())
}
// Recurse implements `SyscallRule.Recurse`.
func (PerArg) Recurse(fn func(SyscallRule) SyscallRule) {}
// String implements `SyscallRule.String`.
func (pa PerArg) String() string {
var sb strings.Builder
writtenArgs := 0
for i, arg := range pa {
if arg == nil {
continue
}
if _, isAny := arg.(AnyValue); isAny {
continue
}
if writtenArgs != 0 {
sb.WriteString(" && ")
}
str := arg.String()
var varName string
if i == RuleIP {
varName = "rip"
} else {
varName = fmt.Sprintf("arg[%d]", i)
}
if strings.Contains(str, "VAL") {
sb.WriteString(strings.ReplaceAll(str, "VAL", varName))
} else {
sb.WriteString(varName)
sb.WriteRune(' ')
sb.WriteString(str)
}
writtenArgs++
}
if writtenArgs == 0 {
return "true"
}
if writtenArgs == 1 {
return sb.String()
}
return "(" + sb.String() + ")"
}
// SyscallRules maps syscall numbers to their corresponding rules.
//
// For example:
//
// rules := MakeSyscallRules(map[uintptr]SyscallRule{
// syscall.SYS_FUTEX: Or{
// PerArg{
// AnyValue{},
// EqualTo(linux.FUTEX_WAIT | linux.FUTEX_PRIVATE_FLAG),
// },
// PerArg{
// AnyValue{},
// EqualTo(linux.FUTEX_WAKE | linux.FUTEX_PRIVATE_FLAG),
// },
// },
// syscall.SYS_GETPID: MatchAll{},
// })
type SyscallRules struct {
rules map[uintptr]SyscallRule
}
// NewSyscallRules returns a new SyscallRules.
func NewSyscallRules() SyscallRules {
return MakeSyscallRules(nil)
}
// MakeSyscallRules returns a new SyscallRules with the given set of rules.
func MakeSyscallRules(rules map[uintptr]SyscallRule) SyscallRules {
if rules == nil {
rules = make(map[uintptr]SyscallRule)
}
return SyscallRules{rules: rules}
}
// String returns a string representation of the syscall rules, one syscall
// per line.
func (sr SyscallRules) String() string {
if len(sr.rules) == 0 {
return "(no rules)"
}
sysnums := make([]uintptr, 0, len(sr.rules))
for sysno := range sr.rules {
sysnums = append(sysnums, sysno)
}
sort.Slice(sysnums, func(i, j int) bool {
return sysnums[i] < sysnums[j]
})
var sb strings.Builder
for _, sysno := range sysnums {
fmt.Fprintf(&sb, "syscall %d: %v\n", sysno, sr.rules[sysno])
}
return strings.TrimSpace(sb.String())
}
// Size returns the number of syscall numbers for which a rule is defined.
func (sr SyscallRules) Size() int {
return len(sr.rules)
}
// Get returns the rule defined for the given syscall number.
func (sr SyscallRules) Get(sysno uintptr) SyscallRule {
return sr.rules[sysno]
}
// Has returns whether there is a rule defined for the given syscall number.
func (sr SyscallRules) Has(sysno uintptr) bool {
_, has := sr.rules[sysno]
return has
}
// Add adds the given rule. It will create a new entry for a new syscall, otherwise
// it will append to the existing rules.
// Returns itself for chainability.
func (sr SyscallRules) Add(sysno uintptr, r SyscallRule) SyscallRules {
if cur, ok := sr.rules[sysno]; ok {
sr.rules[sysno] = Or{cur, r}
} else {
sr.rules[sysno] = r
}
return sr
}
// Set sets the rule for the given syscall number.
// Panics if there is already a rule for this syscall number.
// This is useful for deterministic rules where the set of syscall rules is
// added in multiple chunks but is known to never overlap by syscall number.
// Returns itself for chainability.
func (sr SyscallRules) Set(sysno uintptr, r SyscallRule) SyscallRules {
if cur, ok := sr.rules[sysno]; ok {
panic(fmt.Sprintf("tried to set syscall rule for sysno=%d to %v but it is already set to %v", sysno, r, cur))
}
sr.rules[sysno] = r
return sr
}
// Remove clears the syscall rule for the given syscall number.
// It will panic if there is no syscall rule for this syscall number.
func (sr SyscallRules) Remove(sysno uintptr) {
if !sr.Has(sysno) {
panic(fmt.Sprintf("tried to remove syscall rule for sysno=%d but it is not set", sysno))
}
delete(sr.rules, sysno)
}
// Merge merges the given SyscallRules.
// Returns itself for chainability.
func (sr SyscallRules) Merge(other SyscallRules) SyscallRules {
for sysno, r := range other.rules {
sr.Add(sysno, r)
}
return sr
}
// Copy returns a deep copy of these SyscallRules.
func (sr SyscallRules) Copy() SyscallRules {
rulesCopy := make(map[uintptr]SyscallRule, len(sr.rules))
for sysno, r := range sr.rules {
rulesCopy[sysno] = r.Copy()
}
return MakeSyscallRules(rulesCopy)
}
// ForSingleArgument runs the given function on the `ValueMatcher` rules
// for a single specific syscall argument of the given syscall number.
// If the function returns an error, it will be propagated along with some
// details as to which rule caused the error to be returned.
// ForSingleArgument also returns an error if there are no rules defined for
// the given syscall number, or if at least one rule for this syscall number
// is not either a `PerArg` rule or a rule with children rules (as this would
// indicate that the `PerArg` rules alone may not be a good representation of
// the entire set of rules for this system call).
func (sr SyscallRules) ForSingleArgument(sysno uintptr, argNum int, fn func(ValueMatcher) error) error {
if argNum < 0 || argNum >= len(PerArg{}) {
return fmt.Errorf("invalid argument number %d", argNum)
}
if !sr.Has(sysno) {
return fmt.Errorf("syscall %d has no rules defined", sysno)
}
var err error
var process func(SyscallRule) SyscallRule
var callCount int
process = func(r SyscallRule) SyscallRule {
callCount++
pa, isPerArg := r.(PerArg)
if isPerArg {
if gotErr := fn(pa[argNum]); gotErr != nil && err == nil {
err = fmt.Errorf("PerArg rule %v: arg[%d] = %v (type %T): %v", pa, argNum, pa[argNum], pa[argNum], gotErr)
}
} else {
beforeRecurse := callCount
r.Recurse(process)
if callCount == beforeRecurse {
err = fmt.Errorf("rule %v (type: %T) is not a PerArg or a recursive rule", r, r)
}
}
return r
}
process(sr.rules[sysno])
return err
}
// DenyNewExecMappings is a set of rules that denies creating new executable
// mappings and converting existing ones.
var DenyNewExecMappings = MakeSyscallRules(map[uintptr]SyscallRule{
unix.SYS_MMAP: PerArg{
AnyValue{},
AnyValue{},
MaskedEqual(unix.PROT_EXEC, unix.PROT_EXEC),
},
unix.SYS_MPROTECT: PerArg{
AnyValue{},
AnyValue{},
MaskedEqual(unix.PROT_EXEC, unix.PROT_EXEC),
},
})