From 201a046299b7857bab8f85de578a3e58200d9eff Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Wed, 15 Nov 2023 22:35:30 -0800 Subject: [PATCH] `seccomp`: Enforce that Sentry filters match against reference program. This change adds a `filter_fuzz_golden.bpf` BPF program that was generated manually prior to my recent set of changes to seccomp bytecode and rule optimization changes. It represents the "reference logic"; the new test verifies that the current seccomp-bpf library produces BPF bytecode that has the same behavior, using fuzz testing with full line-based coverage. PiperOrigin-RevId: 582914572 --- Makefile | 5 + pkg/bpf/BUILD | 1 + pkg/bpf/bpf_unsafe.go | 34 +++++++ pkg/seccomp/seccomp.go | 6 +- runsc/boot/filter/BUILD | 9 +- runsc/boot/filter/dumpfilter/dumpfilter.go | 50 ++++++++-- runsc/boot/filter/filter_fuzz_golden.bpf | Bin 0 -> 6040 bytes runsc/boot/filter/filter_fuzz_golden_test.go | 98 +++++++++++++++++++ 8 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 pkg/bpf/bpf_unsafe.go create mode 100644 runsc/boot/filter/filter_fuzz_golden.bpf create mode 100644 runsc/boot/filter/filter_fuzz_golden_test.go diff --git a/Makefile b/Makefile index cceee7275..5deddabbc 100644 --- a/Makefile +++ b/Makefile @@ -458,6 +458,11 @@ run-benchmark: load-benchmarks ## Runs single benchmark and optionally sends dat @$(call run_benchmark,$(RUNTIME)) .PHONY: run-benchmark +## Seccomp targets. +seccomp-sentry-filters: # Dumps seccomp-bpf program for the Sentry binary. + @$(call run,//runsc/boot/filter/dumpfilter,$(ARGS)) +.PHONY: seccomp-sentry-filters + ## ## Website & documentation helpers. ## diff --git a/pkg/bpf/BUILD b/pkg/bpf/BUILD index 51b52752a..53862b80f 100644 --- a/pkg/bpf/BUILD +++ b/pkg/bpf/BUILD @@ -9,6 +9,7 @@ go_library( name = "bpf", srcs = [ "bpf.go", + "bpf_unsafe.go", "decoder.go", "input_bytes.go", "interpreter.go", diff --git a/pkg/bpf/bpf_unsafe.go b/pkg/bpf/bpf_unsafe.go new file mode 100644 index 000000000..418fe218c --- /dev/null +++ b/pkg/bpf/bpf_unsafe.go @@ -0,0 +1,34 @@ +// Copyright 2023 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 bpf + +import ( + "fmt" + "unsafe" +) + +// ParseBytecode converts raw BPF bytecode into BPF instructions. +// It verifies that the resulting set of instructions is a valid program. +func ParseBytecode(bytecode []byte) ([]Instruction, error) { + sizeOfInstruction := int(unsafe.Sizeof(Instruction{})) + if len(bytecode)%sizeOfInstruction != 0 { + return nil, fmt.Errorf("bytecode size (%d bytes) is not a multiple of BPF instruction size of %d bytes", len(bytecode), sizeOfInstruction) + } + insns := ([]Instruction)(unsafe.Slice((*Instruction)(unsafe.Pointer(&bytecode[0])), len(bytecode)/sizeOfInstruction)) + if _, err := Compile(insns, false); err != nil { + return nil, fmt.Errorf("not a valid BPF program: %v", err) + } + return insns, nil +} diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go index 8b337f8ee..70d2571a0 100644 --- a/pkg/seccomp/seccomp.go +++ b/pkg/seccomp/seccomp.go @@ -92,7 +92,9 @@ func Install(rules SyscallRules, denyRules SyscallRules, options ProgramOptions) return nil } -func defaultAction() (linux.BPFAction, error) { +// DefaultAction returns a sane default for a failure to match +// a seccomp-bpf filter. Either kill the process, or trap. +func DefaultAction() (linux.BPFAction, error) { available, err := isKillProcessAvailable() if err != nil { return 0, err @@ -325,7 +327,7 @@ type ProgramOptions struct { // DefaultProgramOptions returns the default program options. func DefaultProgramOptions() ProgramOptions { - action, err := defaultAction() + action, err := DefaultAction() if err != nil { panic(fmt.Sprintf("cannot determine default seccomp action: %v", err)) } diff --git a/runsc/boot/filter/BUILD b/runsc/boot/filter/BUILD index 194e20493..98cefc9e0 100644 --- a/runsc/boot/filter/BUILD +++ b/runsc/boot/filter/BUILD @@ -67,12 +67,19 @@ secbench_test( go_test( name = "filter_fuzz_test", - srcs = ["filter_fuzz_test.go"], + srcs = [ + "filter_fuzz_golden_test.go", + "filter_fuzz_test.go", + ], + data = ["filter_fuzz_golden.bpf"], deps = [ ":filter", "//pkg/abi/linux", + "//pkg/bpf", "//pkg/seccomp", "//pkg/sentry/platform/systrap", + "//pkg/test/testutil", "//test/secfuzz", + "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/runsc/boot/filter/dumpfilter/dumpfilter.go b/runsc/boot/filter/dumpfilter/dumpfilter.go index 3c1459ee0..d15693dba 100644 --- a/runsc/boot/filter/dumpfilter/dumpfilter.go +++ b/runsc/boot/filter/dumpfilter/dumpfilter.go @@ -30,10 +30,36 @@ import ( // Flags. var ( - output = flag.String("output", "fancy", "Output type: 'fancy' (human-readable with line numbers resolved), 'plain' (diffable but still human-readable output), 'bytecode' (dump raw bytecode)") - nvproxy = flag.Bool("nvproxy", false, "Enable nvproxy in filter configuration") + output = flag.String("output", "fancy", "Output type: 'fancy' (human-readable with line numbers resolved), 'plain' (diffable but still human-readable output), 'bytecode' (dump raw bytecode)") + nvproxy = flag.Bool("nvproxy", false, "Enable nvproxy in filter configuration") + denyAction = flag.String("deny-action", "default", "What to do if the syscall matches the 'deny' ruleset (one of: errno, kill_process, kill_thread)") + defaultAction = flag.String("default-action", "default", "What to do if all the syscall rules fail to match (one of: errno, kill_process, kill_thread)") + badArchAction = flag.String("bad-arch-action", "default", "What to do if all the architecture field mismatches (one of: errno, kill_process, kill_thread)") + out = flag.String("out", "/dev/stdout", "Where to write the filter output (defaults to standard output)") ) +func action(s string) linux.BPFAction { + switch s { + case "default": + def, err := seccomp.DefaultAction() + if err != nil { + log.Warningf("cannot determine default seccomp action: %v", err) + os.Exit(1) + } + return def + case "errno": + return linux.SECCOMP_RET_ERRNO + case "kill_process": + return linux.SECCOMP_RET_KILL_PROCESS + case "kill_thread": + return linux.SECCOMP_RET_KILL_THREAD + default: + log.Warningf("invalid action %q (want one of: errno, kill_process, kill_thread)", s) + os.Exit(1) + panic("unreachable") + } +} + func main() { flag.Parse() opt := filter.Options{ @@ -41,16 +67,20 @@ func main() { NVProxy: *nvproxy, } rules, denyRules := filter.Rules(opt) + + seccompOpts := filter.SeccompOptions(opt) + seccompOpts.DefaultAction = action(*defaultAction) + seccompOpts.BadArchAction = action(*badArchAction) insns, stats, err := seccomp.BuildProgram([]seccomp.RuleSet{ { Rules: denyRules, - Action: linux.SECCOMP_RET_ERRNO, + Action: action(*denyAction), }, { Rules: rules, Action: linux.SECCOMP_RET_ALLOW, }, - }, filter.SeccompOptions(opt)) + }, seccompOpts) if err != nil { log.Warningf("%v", err) os.Exit(1) @@ -61,6 +91,12 @@ func main() { log.Infof("Rule optimization passes duration: %v", stats.RuleOptimizeDuration) log.Infof("BPF optimization passes duration: %v", stats.BPFOptimizeDuration) log.Infof("Total duration: %v", stats.BuildDuration+stats.RuleOptimizeDuration+stats.BPFOptimizeDuration) + outFile, err := os.Create(*out) + if err != nil { + log.Warningf("cannot open output file %q: %v", *out, err) + os.Exit(1) + } + defer outFile.Close() switch *output { case "fancy": dump, err := bpf.DecodeInstructions(insns) @@ -68,13 +104,13 @@ func main() { log.Warningf("%v", err) os.Exit(1) } - fmt.Print(dump) + fmt.Fprint(outFile, dump) case "plain": for _, ins := range insns { - fmt.Println(ins.String()) + fmt.Fprint(outFile, ins.String()) } case "bytecode": - if _, err := os.Stdout.WriteString(InstructionsToBytecode(insns)); err != nil { + if _, err := outFile.WriteString(InstructionsToBytecode(insns)); err != nil { log.Warningf("cannot write bytecode to stdout: %v", err) os.Exit(1) } diff --git a/runsc/boot/filter/filter_fuzz_golden.bpf b/runsc/boot/filter/filter_fuzz_golden.bpf new file mode 100644 index 0000000000000000000000000000000000000000..488198f8339f42c6273669b443cbcd0071b90b52 GIT binary patch literal 6040 zcmY#jU|?WjU|paS%(Cftewdfq_Alfq@Z3vobI+JYr;E0MX$L z3=AN30$}|N%pe-X7lH5@LHZ%){jUd!gXBdS7`WM?`k?AKLFz#AAS}wj0Fnlqs{%0x zWEGgN0pT;l)ayX_xa?tObb`1KY7Z`T>@2A2;Pye?!wk|0Vnf{n;xb_I7cIk?nu zGoY#?+5e6V0^sl<;D2^jRC~zv7dtlrb2y;!Ng#Z<<)P}J@jQ2c_#LGd8UzyP)w5>Bir_JSf7WNsQ0B>#c*g7ksR5oI9G zJdhq(eg|0u;^Rv1Fmo!;cgC2-;500(0DaPi+@nM1jRpy2B}Yissn{HBtHMwgVH^yyauTQ z(I9nEP<4S&`$73wl>wA$K>3LQ6s%x(g4C-rurmDr4=ML0Amuc(bO^-%p!_C^l71lS zNHvd<%H}aq**q31o5xIL^F*j@o-mcobLzyMUO?&b79%L#F*EQ(%V!V`62A^@pP;5^ zMg~4;`n}AET26w>4UoPIDB{cvpmYmL{~#J956e&J=7Q2KNd5#P#J!+=YYJ&rF^DoC z@&zbQgUkb!pRjxe(svN-4@L%QsQdP#xx*alKM)O4Uj|jD2hE2d_kqd{kh?&3K=T(! zDL6lZ#6%ewnAM@`LGHtr|A;jQt$YGm3orjcb`fh1s6GIxr_?>51|v1h0i^?6=70jT z2OLk#44`la#UqFYiHk$y0~Dtq{U8hqZwF|3kIjD|eN9mF)S&STqCw)YdKnb3ATt>l zAmy<#R2_&0sjI?~&O!ErK{-!4l)ZGpP*<5 zrzdFo0yz&`yu;hegw%m543PQQ)PdX)2M!-b27Rb|Vxaj8qz4qf2?WwJOdY6x1F><% zLm<>1P`wJW*B@#R$p4^x121*8rnkINsfVDp(7yrAI>qCxh+(hn#g zk>lAGst!bh)WOmhx;h)EIuH#~XMrOM4E(&{_=Nfk)NBLuL4HB@H#FQp zR)FPU?GRYFU~A8S6sv*#&B)*ZwO<8lKP=op2?}Hvwr~T9!_>jb2}0^X;RDi#O&!QR zB4GD0Gekqf4Mc;)VHl(z5f6+EzEF4bqU0Aw22i>K>ES{VXJ(LKg!Gd^G)TS+svne} zLGFV32UJ{v!x8FEkWw(8*l_&~Zci~Y+=u!DM1u^0xtH4Zy@lGh3u+&T2H6LzuTaB{ znPDna9f$_06NZKZy1HJdIuH#~_W-I6=6+DVLQaR!bP7@m4lhucg319{I)tWEkTvjf z38eTc*!_$Q%~10%GoaT0)1cxPP{f%TZbRJ%qCw_JLe+sRKu#y8pyDT>e34R4TfaCd|B zuLkR9W_Se+9}o=^2l)p?gAzE%ewcY7{&wK_!e$;U96|aPK+Sy)H5WvK#6jr+M1vdx zQxEE&Jb|hM(IEL5Q2(H+V`O*)6`z6s$x^~^wQsQcjMJJfyb4$ydqx({R&*!`e(0CK)E0lO2qzyH4;q&N#K z&di_;bq|OJiK{~00}3C6f0!8*pz1(0NF69WfM}2!kUL@N1(wc1^62eUka|$L4T^tR zsCgh7Bp!j)UPcBpwrR{}>sRp!$8F;RSUkD4B!93+hgg6To~>K0ppX zT=~!mY(6uCI@BK^8e|A4oq=f7@MB~E)t?}7Yp6Rweu3D-0k#(!&me6m`PCF`4B$9ZKZpjI4>AWtgWLhK1Lj|l!5}`l{sox}iZ@fJeIOcS53GKG*#`|jc=$rY z52O_APmmiR{zmk-u%#0*sK1S&{sy@lq!*h0nbFce$a^T^2Fg#MaWp%qejc=N^MLvT jM1vd$!ytn|X&TaQXJl}P>Ib!Bki~tV;>hYD@(c_Ba-&^s literal 0 HcmV?d00001 diff --git a/runsc/boot/filter/filter_fuzz_golden_test.go b/runsc/boot/filter/filter_fuzz_golden_test.go new file mode 100644 index 000000000..e9eab5c63 --- /dev/null +++ b/runsc/boot/filter/filter_fuzz_golden_test.go @@ -0,0 +1,98 @@ +// Copyright 2023 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. + +//go:build !false +// +build !false + +package filter_fuzz_test + +import ( + "os" + "testing" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap" + "gvisor.dev/gvisor/pkg/test/testutil" + "gvisor.dev/gvisor/runsc/boot/filter" + "gvisor.dev/gvisor/test/secfuzz" +) + +// FuzzFilterAgainstGolden tests that the behavior of the generated +// seccomp-bpf program has not changed. +// This is useful when modifying the way that the seccomp-bpf program +// is built, not when modifying what rules the program is meant to enforce. +// If you are modifying the seccomp-bpf rules in such a way that you +// are expecting the set of allowed/disallowed syscalls to change, +// you can update the reference program using: +// +// $ make seccomp-sentry-filters ARGS='--deny-action=errno --default-action=kill_thread --bad-arch-action=kill_process --output=bytecode --out=runsc/boot/filter/filter_fuzz_golden.bpf' +func FuzzFilterAgainstGolden(f *testing.F) { + goldenProgPath, err := testutil.FindFile("runsc/boot/filter/filter_fuzz_golden.bpf") + if err != nil { + f.Fatalf("failed to find golden program: %v", err) + } + goldenProg, err := os.ReadFile(goldenProgPath) + if err != nil { + f.Fatalf("failed to read golden program: %v", err) + } + goldenInstructions, err := bpf.ParseBytecode(goldenProg) + if err != nil { + f.Fatalf("failed to parse golden program bytecode: %v", err) + } + goldenFuzzee := secfuzz.Fuzzee{ + Name: "golden", + Instructions: goldenInstructions, + // TODO(b/298726675): Enforce full coverage with the optimized program + // once confident that it works well. + // This will ensure that the generated fuzz corpus is sufficient to + // fully exhaust the golden program. + EnforceFullCoverage: false, + } + + filterOpts := filter.Options{ + Platform: &systrap.Systrap{}, + } + rules, denyRules := filter.Rules(filterOpts) + ruleSets := []seccomp.RuleSet{ + { + Rules: denyRules, + Action: linux.SECCOMP_RET_ERRNO, + }, + { + Rules: rules, + Action: linux.SECCOMP_RET_ALLOW, + }, + } + opts := filter.SeccompOptions(filterOpts) + // We use unique actions here to be able to tell them apart. + opts.DefaultAction = linux.SECCOMP_RET_KILL_THREAD + opts.BadArchAction = linux.SECCOMP_RET_KILL_PROCESS + current, _, err := seccomp.BuildProgram(ruleSets, opts) + if err != nil { + f.Fatalf("failed to build seccomp-bpf program: %v", err) + } + currentFuzzee := secfuzz.Fuzzee{ + Name: "current", + Instructions: current, + EnforceFullCoverage: true, + } + df, err := secfuzz.NewDiffFuzzer(f, &goldenFuzzee, ¤tFuzzee) + if err != nil { + f.Fatalf("failed to create diff fuzzer: %v", err) + } + df.DeriveCorpusFromRuleSets(ruleSets) + df.Fuzz() +}