From 0010b32f558b4bf1eaf8207af12e5f5f1cc3138c Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Fri, 27 Oct 2023 15:13:13 -0700 Subject: [PATCH] `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 --- pkg/seccomp/seccomp.go | 26 ++- pkg/seccomp/seccomp_rules.go | 377 ++++++++++++++++++++--------------- 2 files changed, 243 insertions(+), 160 deletions(-) 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()) }