diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go index 36b6fce9d..8239710ed 100644 --- a/pkg/seccomp/seccomp.go +++ b/pkg/seccomp/seccomp.go @@ -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 diff --git a/pkg/seccomp/seccomp_rules.go b/pkg/seccomp/seccomp_rules.go index 7ec721268..27fd208dc 100644 --- a/pkg/seccomp/seccomp_rules.go +++ b/pkg/seccomp/seccomp_rules.go @@ -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()) }