seccomp: Add ValueMatcher interface to represent argument matchers.

This replaces the argument-specific matching rules with a generic `interface`
which can be specialized for value matching.

`PerArg` is modified to become a 7-item array of `ValueMatcher` instead of
`any`s.

The interface is a bit complicated due to the limitation that BPF can only
do 32-bit arithmetic operations. As such, the rules for matching the high
and the low 32-bit of the 64-bit values are split up. It is up to the caller
to ensure that these values are loaded before rendering each half of the
rules.

This changes does not change any existing rules, but allows for the creation
of more complex but more efficient value matching rules (eg. conjunctions
and disjunctions over the same value).

PiperOrigin-RevId: 577316964
This commit is contained in:
Etienne Perot
2023-10-27 15:17:05 -07:00
committed by gVisor bot
parent ee3625a998
commit 0010b32f55
2 changed files with 243 additions and 160 deletions
+25 -1
View File
@@ -32,6 +32,10 @@ const (
// 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
)
// NonNegativeFDCheck ensures an FD argument is a non-negative int.
@@ -270,6 +274,26 @@ func (l *labelSet) Push(labelSuffix string, newRuleMatch, newRuleMismatch label)
}
}
// 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)
}
// BuildStats contains information about seccomp program generation.
type BuildStats struct {
// SizeBeforeOptimizations and SizeAfterOptimizations correspond to the
@@ -441,7 +465,7 @@ func buildBSTProgram(n *node, rules []RuleSet, program *syscallProgram) error {
// the vsyscall page will be mapped.
if rs.Vsyscall {
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, seccompDataOffsetIPHigh)
program.IfNot(bpf.Jmp|bpf.Jset|bpf.K, 0x80000000, ruleSetLabelSet.Mismatched())
program.IfNot(bpf.Jmp|bpf.Jset|bpf.K, vsyscallPageIPMask, ruleSetLabelSet.Mismatched())
}
// Add an argument check for these particular
+218 -159
View File
@@ -16,7 +16,6 @@ package seccomp
import (
"fmt"
"reflect"
"sort"
"strings"
@@ -48,68 +47,264 @@ 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.
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.
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)
}
// 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 "*"
return "== *"
}
// Repr implements `ValueMatcher.Repr`.
func (av AnyValue) Repr() string {
return av.String()
}
// Render implements `ValueMatcher.Render`.
func (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
func (a EqualTo) String() string {
return fmt.Sprintf("== %#x", uintptr(a))
// String implements `ValueMatcher.String`.
func (eq EqualTo) String() string {
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) {
// Assert that the higher 32bits are equal.
// arg_low == low ? continue : violation
value.LoadHigh32Bits()
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high32Bits(uintptr(eq)), labelSet.Mismatched())
// Assert that the lower 32bits are also equal.
// arg_high == high ? continue/success : violation
value.LoadLow32Bits()
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low32Bits(uintptr(eq)), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// NotEqual specifies a value that is strictly not equal.
type NotEqual uintptr
func (a NotEqual) String() string {
return fmt.Sprintf("!= %#x", uintptr(a))
// 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) {
// Check if the higher 32bits are (not) equal.
// arg_low != low ? success : continue
value.LoadHigh32Bits()
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high32Bits(uintptr(ne)), labelSet.Matched())
// Assert that the lower 32bits are not equal (assuming
// higher bits are equal).
// arg_high != high ? success : violation
value.LoadLow32Bits()
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low32Bits(uintptr(ne)), labelSet.Matched())
program.JumpTo(labelSet.Mismatched())
}
// GreaterThan specifies a value that needs to be strictly smaller.
type GreaterThan uintptr
func (a GreaterThan) String() string {
return fmt.Sprintf("> %#x", uintptr(a))
// 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
func (a GreaterThanOrEqual) String() string {
return fmt.Sprintf(">= %#x", uintptr(a))
// 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
func (a LessThan) String() string {
return fmt.Sprintf("< %#x", uintptr(a))
// 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
func (a LessThanOrEqual) String() string {
return fmt.Sprintf("<= %#x", uintptr(a))
// 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())
}
// 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
}
func (a maskedEqual) String() string {
return fmt.Sprintf("& %#x == %#x", a.mask, a.value)
// 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) {
// Assert that the higher 32bits are equal when masked.
// A <- arg_high & maskHigh
value.LoadHigh32Bits()
program.Stmt(bpf.Alu|bpf.And|bpf.K, high32Bits(me.mask))
// Assert that arg_high & maskHigh == high.
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high32Bits(me.value), labelSet.Mismatched())
// Assert that the lower 32bits are equal when masked.
// A <- arg_low & maskLow
value.LoadLow32Bits()
program.Stmt(bpf.Alu|bpf.And|bpf.K, low32Bits(me.mask))
// Assert that arg_low & maskLow == low.
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low32Bits(me.value), labelSet.Mismatched())
program.JumpTo(labelSet.Matched())
}
// 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) any {
func MaskedEqual(mask, value uintptr) ValueMatcher {
return maskedEqual{
mask: mask,
value: value,
@@ -208,7 +403,7 @@ func merge(rule1, rule2 SyscallRule) SyscallRule {
// rule := PerArg{
// EqualTo(linux.ARCH_GET_FS | linux.ARCH_SET_FS), // arg0
// }
type PerArg [7]any // 6 arguments + RIP
type PerArg [7]ValueMatcher // 6 arguments + RIP
// RuleIP indicates what rules in the Rule array have to be applied to
// instruction pointer.
@@ -220,7 +415,6 @@ func (pa PerArg) Render(program *syscallProgram, labelSet *labelSet) {
if arg == nil {
continue
}
frag := program.Record()
nextArgLabel := labelSet.NewLabel()
labelSuffix := fmt.Sprintf("arg[%d]", i)
@@ -233,149 +427,14 @@ func (pa PerArg) Render(program *syscallProgram, labelSet *labelSet) {
labelSuffix = "rip"
}
ls := labelSet.Push(labelSuffix, nextArgLabel, labelSet.Mismatched())
// Add the conditional operation. Input values to the BPF
// program are 64bit values. However, comparisons in BPF can
// only be done on 32bit values. This means that we need to
// operate on each 32bit half in order to do one logical 64bit
// comparison.
switch a := arg.(type) {
case AnyValue:
program.JumpTo(ls.Matched())
case EqualTo:
// EqualTo checks that both the higher and lower 32bits are equal.
high, low := uint32(a>>32), uint32(a)
// Assert that the lower 32bits are equal.
// arg_low == low ? continue : violation
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low, ls.Mismatched())
// Assert that the higher 32bits are also equal.
// arg_high == high ? continue/success : violation
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Mismatched())
program.JumpTo(ls.Matched())
case NotEqual:
// NotEqual checks that either the higher or lower 32bits
// are *not* equal.
high, low := uint32(a>>32), uint32(a)
// Check if the higher 32bits are (not) equal.
// arg_low != low ? success : continue
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low, ls.Matched())
// Assert that the lower 32bits are not equal (assuming
// higher bits are equal).
// arg_high != high ? success : violation
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Matched())
program.JumpTo(ls.Mismatched())
case GreaterThan:
// GreaterThan checks that the higher 32bits is greater
// *or* that the higher 32bits are equal and the lower
// 32bits are greater.
high, low := uint32(a>>32), uint32(a)
// Assert the higher 32bits are greater than or equal.
// arg_high >= high ? continue : violation (arg_high < high)
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, high, ls.Mismatched())
// Assert that the lower 32bits are greater.
// arg_high == high ? continue : success (arg_high > high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Matched())
// arg_low > low ? continue/success : violation (arg_high == high and arg_low <= low)
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.IfNot(bpf.Jmp|bpf.Jgt|bpf.K, low, ls.Mismatched())
program.JumpTo(ls.Matched())
case GreaterThanOrEqual:
// GreaterThanOrEqual checks that the higher 32bits is
// greater *or* that the higher 32bits are equal and the
// lower 32bits are greater than or equal.
high, low := uint32(a>>32), uint32(a)
// Assert the higher 32bits are greater than or equal.
// arg_high >= high ? continue : violation (arg_high < high)
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, high, ls.Mismatched())
// arg_high == high ? continue : success (arg_high > high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Matched())
// Assert that the lower 32bits are greater (assuming the
// higher bits are equal).
// arg_low >= low ? continue/success : violation (arg_high == high and arg_low < low)
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.IfNot(bpf.Jmp|bpf.Jge|bpf.K, low, ls.Mismatched())
program.JumpTo(ls.Matched())
case LessThan:
// LessThan checks that the higher 32bits is less *or* that
// the higher 32bits are equal and the lower 32bits are
// less.
high, low := uint32(a>>32), uint32(a)
// Assert the higher 32bits are less than or equal.
// arg_high > high ? violation : continue
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.If(bpf.Jmp|bpf.Jgt|bpf.K, high, ls.Mismatched())
// arg_high == high ? continue : success (arg_high < high)
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Matched())
// Assert that the lower 32bits are less (assuming the
// higher bits are equal).
// arg_low >= low ? violation : continue
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.If(bpf.Jmp|bpf.Jge|bpf.K, low, ls.Mismatched())
program.JumpTo(ls.Matched())
case LessThanOrEqual:
// LessThan checks that the higher 32bits is less *or* that
// the higher 32bits are equal and the lower 32bits are
// less than or equal.
high, low := uint32(a>>32), uint32(a)
// Assert the higher 32bits are less than or equal.
// assert arg_high > high ? violation : continue
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
program.If(bpf.Jmp|bpf.Jgt|bpf.K, high, ls.Mismatched())
// arg_high == high ? continue : success
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Matched())
// Assert the lower bits are less than or equal (assuming
// the higher bits are equal).
// arg_low > low ? violation : success
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
program.If(bpf.Jmp|bpf.Jgt|bpf.K, low, ls.Mismatched())
program.JumpTo(ls.Matched())
case maskedEqual:
// MaskedEqual checks that the bitwise AND of the value and
// mask are equal for both the higher and lower 32bits.
high, low := uint32(a.value>>32), uint32(a.value)
maskHigh, maskLow := uint32(a.mask>>32), uint32(a.mask)
// Assert that the lower 32bits are equal when masked.
// A <- arg_low.
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetLow)
// A <- arg_low & maskLow
program.Stmt(bpf.Alu|bpf.And|bpf.K, maskLow)
// Assert that arg_low & maskLow == low.
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, low, ls.Mismatched())
// Assert that the higher 32bits are equal when masked.
// A <- arg_high
program.Stmt(bpf.Ld|bpf.Abs|bpf.W, dataOffsetHigh)
// A <- arg_high & maskHigh
program.Stmt(bpf.Alu|bpf.And|bpf.K, maskHigh)
// Assert that arg_high & maskHigh == high.
program.IfNot(bpf.Jmp|bpf.Jeq|bpf.K, high, ls.Mismatched())
program.JumpTo(ls.Matched())
default:
panic(fmt.Sprintf("unknown syscall rule type: %v", reflect.TypeOf(a)))
}
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())
}