diff --git a/runsc/boot/filter/BUILD b/runsc/boot/filter/BUILD index bd99ceb35..760fd10aa 100644 --- a/runsc/boot/filter/BUILD +++ b/runsc/boot/filter/BUILD @@ -1,3 +1,4 @@ +load("//test/secbench:defs.bzl", "secbench_test") load("//tools:defs.bzl", "go_library") package( @@ -36,3 +37,17 @@ go_library( "@org_golang_x_sys//unix:go_default_library", ], ) + +secbench_test( + name = "filter_bench_test", + srcs = ["filter_bench_test.go"], + deps = [ + ":filter", + "//pkg/abi/linux", + "//pkg/sentry/platform/kvm", + "//pkg/sentry/platform/systrap", + "//test/secbench", + "//test/secbench/secbenchdef", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/runsc/boot/filter/filter.go b/runsc/boot/filter/filter.go index 1a10fd811..6b9a91568 100644 --- a/runsc/boot/filter/filter.go +++ b/runsc/boot/filter/filter.go @@ -37,8 +37,8 @@ type Options struct { ControllerFD int } -// Install seccomp filters based on the given platform. -func Install(opt Options) error { +// Rules returns the seccomp (rules, denyRules) to use for the Sentry. +func Rules(opt Options) (seccomp.SyscallRules, seccomp.SyscallRules) { s := allowedSyscalls s.Merge(controlServerFilters(opt.ControllerFD)) @@ -73,7 +73,13 @@ func Install(opt Options) error { s.Merge(opt.Platform.SyscallFilters()) - return seccomp.Install(s, seccomp.DenyNewExecMappings) + return s, seccomp.DenyNewExecMappings +} + +// Install seccomp filters based on the given platform. +func Install(opt Options) error { + rules, denyRules := Rules(opt) + return seccomp.Install(rules, denyRules) } // Report writes a warning message to the log. diff --git a/runsc/boot/filter/filter_bench_test.go b/runsc/boot/filter/filter_bench_test.go new file mode 100644 index 000000000..a04a0db74 --- /dev/null +++ b/runsc/boot/filter/filter_bench_test.go @@ -0,0 +1,95 @@ +// 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 filter_bench_test benchmarks the speed of the seccomp-bpf filters. +package filter_bench_test + +import ( + "testing" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/sentry/platform/kvm" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap" + "gvisor.dev/gvisor/runsc/boot/filter" + "gvisor.dev/gvisor/test/secbench" + "gvisor.dev/gvisor/test/secbench/secbenchdef" +) + +type Options struct { + Name string + Options filter.Options +} + +// BenchmarkSentrySystrap benchmarks the seccomp filters used by the Sentry +// using the Systrap platform. +func BenchmarkSentrySystrap(b *testing.B) { + rules, denyRules := filter.Rules(filter.Options{ + Platform: &systrap.Systrap{}, + }) + secbench.Run(b, secbench.BenchFromSyscallRules( + b, + "Postgres", + secbenchdef.Profile{ + Arch: linux.AUDIT_ARCH_X86_64, + Sequences: []secbenchdef.Sequence{ + // Top 10 syscalls captured by running Postgres in a runsc container + // and running `pgbench` against it. Weights are the number of times + // each syscall was called. + {"futex", 870063, secbenchdef.Single(unix.SYS_FUTEX, 0, linux.FUTEX_WAKE)}, + {"nanosleep", 275649, secbenchdef.NanosleepZero.Seq()}, + {"sendmmsg", 160201, secbenchdef.Single(unix.SYS_SENDMMSG, secbenchdef.NonExistentFD, 0, 0, unix.MSG_DONTWAIT)}, + {"fstat", 115769, secbenchdef.Single(unix.SYS_FSTAT, secbenchdef.NonExistentFD)}, + {"ppoll", 69749, secbenchdef.PPollNonExistent.Seq()}, + {"fsync", 23131, secbenchdef.Single(unix.SYS_FSYNC, secbenchdef.NonExistentFD)}, + {"pwrite64", 14096, secbenchdef.Single(unix.SYS_PWRITE64, secbenchdef.NonExistentFD)}, + {"epoll_pwait", 12266, secbenchdef.Single(unix.SYS_EPOLL_PWAIT, secbenchdef.NonExistentFD)}, + {"close", 1991, secbenchdef.Single(unix.SYS_CLOSE, secbenchdef.NonExistentFD)}, + {"getpid", 1413, secbenchdef.Single(unix.SYS_GETPID)}, + }, + }, + rules, + denyRules, + )) +} + +// BenchmarkSentryKVM benchmarks the seccomp filters used by the Sentry +// using the KVM platform. +func BenchmarkSentryKVM(b *testing.B) { + rules, denyRules := filter.Rules(filter.Options{ + Platform: &kvm.KVM{}, + }) + secbench.Run(b, secbench.BenchFromSyscallRules( + b, + "Postgres", + secbenchdef.Profile{ + Arch: linux.AUDIT_ARCH_X86_64, + Sequences: []secbenchdef.Sequence{ + // Same procedure, but using the KVM platform instead. + {"futex", 3180352, secbenchdef.Single(unix.SYS_FUTEX, 0, linux.FUTEX_WAKE)}, + {"ioctl", 2501786, secbenchdef.Single(unix.SYS_IOCTL, secbenchdef.NonExistentFD, kvm.KVM_RUN)}, + {"rt_sigreturn", 2501695, secbenchdef.RTSigreturn.Seq()}, + {"sendmmsg", 1490395, secbenchdef.Single(unix.SYS_SENDMMSG, secbenchdef.NonExistentFD, 0, 0, unix.MSG_DONTWAIT)}, + {"nanosleep", 1217019, secbenchdef.NanosleepZero.Seq()}, + {"fstat", 1068477, secbenchdef.Single(unix.SYS_FSTAT, secbenchdef.NonExistentFD)}, + {"ppoll", 653137, secbenchdef.PPollNonExistent.Seq()}, + {"fsync", 213320, secbenchdef.Single(unix.SYS_FSYNC, secbenchdef.NonExistentFD)}, + {"pwrite64", 107603, secbenchdef.Single(unix.SYS_PWRITE64, secbenchdef.NonExistentFD)}, + {"epoll_pwait", 29909, secbenchdef.Single(unix.SYS_EPOLL_PWAIT, secbenchdef.NonExistentFD)}, + }, + }, + rules, + denyRules, + )) +} diff --git a/test/secbench/BUILD b/test/secbench/BUILD new file mode 100644 index 000000000..44dbdd6fa --- /dev/null +++ b/test/secbench/BUILD @@ -0,0 +1,47 @@ +load("//tools:defs.bzl", "bzl_library", "go_binary", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "secbench", + testonly = 1, + srcs = ["secbench.go"], + data = [ + ":runner", + ], + visibility = [ + "//:sandbox", + ], + deps = [ + "//pkg/abi/linux", + "//pkg/bpf", + "//pkg/hostarch", + "//pkg/marshal", + "//pkg/seccomp", + "//pkg/test/testutil", + "//test/secbench/secbenchdef", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +go_binary( + name = "runner", + testonly = 1, + srcs = ["runner.go"], + deps = [ + "//pkg/abi/linux", + "//pkg/bpf", + "//pkg/gohacks", + "//pkg/seccomp", + "//test/secbench/secbenchdef", + ], +) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + deps = ["//tools:defs_bzl"], +) diff --git a/test/secbench/defs.bzl b/test/secbench/defs.bzl new file mode 100644 index 000000000..1781ff7c9 --- /dev/null +++ b/test/secbench/defs.bzl @@ -0,0 +1,18 @@ +"""Defines secbench_test, a wrapper over go_test for secbench benchmarks.""" + +load("//tools:defs.bzl", "go_test") + +def secbench_test(**kwargs): + """Wrapper over go_test useful for secbench benchmarks. + + Args: + **kwargs: Same as go_test arguments. + """ + kwargs["tags"] = kwargs.get("tags", []) + [ + "local", + "manual", + "secbench", + ] + kwargs["static"] = True + kwargs["timeout"] = "long" + go_test(**kwargs) diff --git a/test/secbench/runner.go b/test/secbench/runner.go new file mode 100644 index 000000000..97e33ac51 --- /dev/null +++ b/test/secbench/runner.go @@ -0,0 +1,156 @@ +// 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. + +// The runner binary executes a single benchmark run and prints out results. +// Because seccomp-bpf filters cannot be removed from a process, this runs as +// a subprocess of the secbench library. +// This requires the ability to write(2) to stdout even after installing the +// seccomp-bpf filter. +package main + +import ( + "encoding/json" + "fmt" + "io" + "math/rand" + "os" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/gohacks" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/test/secbench/secbenchdef" +) + +// install installs the given program on the runner. +func install(program []linux.BPFInstruction) error { + // Rewrite the program so that all return actions are either ALLOW or + // RET_ERRNO. This allows us to benchmark the program without worrying + // that we'll crash if we call a bad system call. + rewritten := make([]linux.BPFInstruction, len(program)) + copy(rewritten, program) + for pc, ins := range rewritten { + switch ins.OpCode { + case bpf.Ret | bpf.A: + // Override the return action value to RET_ERRNO. + ins.K = uint32(linux.SECCOMP_RET_ERRNO) + case bpf.Ret | bpf.K: + switch linux.BPFAction(ins.K) { + case linux.SECCOMP_RET_ALLOW, linux.SECCOMP_RET_ERRNO: + // Do nothing. + default: + // Override the return action value to RET_ERRNO. + ins.K = uint32(linux.SECCOMP_RET_ERRNO) + } + default: + // Do nothing. + } + rewritten[pc] = ins + } + return seccomp.SetFilter(rewritten) +} + +// run runs a Bench request. +func run(req secbenchdef.BenchRunRequest) (secbenchdef.BenchRunResponse, error) { + bn := req.Bench + rng := rand.New(rand.NewSource(req.RandomSeed)) + + sequenceMetrics := make([]secbenchdef.SequenceMetrics, len(bn.Profile.Sequences)) + var totalWeight int + for _, seq := range bn.Profile.Sequences { + if seq.Weight < 0 { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("weight of sequence %v cannot be zero or negative: %d", seq, seq.Weight) + } + totalWeight += seq.Weight + } + + // We're ready. Install the BPF program. + if req.InstallFilter { + if err := install(bn.Program); err != nil { + panic(fmt.Sprintf("cannot install BPF program: %v", err)) + } + } + + var ( + before, after int64 + si, seqIndex, randWeight int + seq secbenchdef.Sequence + seqSyscalls []secbenchdef.Syscall + sc secbenchdef.Syscall + duration, totalNanos uint64 + ) + for i := uint64(0); i < req.Iterations; i++ { + randWeight = rng.Intn(totalWeight) + seqIndex = -1 + for si, seq = range bn.Profile.Sequences { + if randWeight -= seq.Weight; randWeight < 0 { + seqIndex = si + break + } + } + if seqIndex == -1 { + panic("logic error in weight randomization") + } + if !req.ActiveSequences[seqIndex] { + continue + } + seqSyscalls = bn.Profile.Sequences[seqIndex].Syscalls + if len(seqSyscalls) == 1 { + // If we have only one syscall to call (common), measure this directly to + // avoid measuring the loop overhead. + sc = seqSyscalls[0] + before = gohacks.Nanotime() + sc.Call() + after = gohacks.Nanotime() + } else { + before = gohacks.Nanotime() + for _, sc = range seqSyscalls { + sc.Call() + } + after = gohacks.Nanotime() + } + duration = uint64(after - before) + sequenceMetrics[seqIndex].Iterations++ + sequenceMetrics[seqIndex].TotalNanos += duration + totalNanos += duration + } + + return secbenchdef.BenchRunResponse{ + TotalNanos: totalNanos, + SequenceMetrics: sequenceMetrics, + }, nil +} + +func main() { + data, err := io.ReadAll(os.Stdin) + if err != nil { + panic(fmt.Sprintf("cannot read from stdin: %v", err)) + } + var runReq secbenchdef.BenchRunRequest + if err = json.Unmarshal(data, &runReq); err != nil { + panic(fmt.Sprintf("cannot deserialize bench data: %v", err)) + } + resp, err := run(runReq) + if err != nil { + panic(fmt.Sprintf("cannot run bench: %v", err)) + } + respData, err := json.Marshal(&resp) + if err != nil { + panic(fmt.Sprintf("cannot serialize bench response: %v", err)) + } + if _, err := os.Stdout.Write(respData); err != nil { + panic(fmt.Sprintf("cannot write response to stdout: %v", err)) + } + os.Stdout.Close() +} diff --git a/test/secbench/secbench.go b/test/secbench/secbench.go new file mode 100644 index 000000000..54235deb6 --- /dev/null +++ b/test/secbench/secbench.go @@ -0,0 +1,293 @@ +// 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 secbench provides utilities for benchmarking seccomp-bpf filters. +package secbench + +import ( + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "sync" + "testing" + "time" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/marshal" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/test/testutil" + "gvisor.dev/gvisor/test/secbench/secbenchdef" +) + +// BenchFromSyscallRules returns a new Bench creates from SyscallRules. +func BenchFromSyscallRules(b *testing.B, name string, profile secbenchdef.Profile, rules seccomp.SyscallRules, denyRules seccomp.SyscallRules) secbenchdef.Bench { + // If there is a rule allowing rt_sigreturn to be called, + // also add a rule for the stand-in syscall number instead. + if sigreturnRule, found := rules[unix.SYS_RT_SIGRETURN]; found { + rules[uintptr(secbenchdef.RTSigreturn.Data(profile.Arch).Nr)] = sigreturnRule + } + instrs, err := seccomp.BuildProgram([]seccomp.RuleSet{ + { + Rules: denyRules, + Action: linux.SECCOMP_RET_ERRNO, + }, + { + Rules: rules, + Action: linux.SECCOMP_RET_ALLOW, + }, + }, linux.SECCOMP_RET_ERRNO, linux.SECCOMP_RET_ERRNO) + if err != nil { + b.Fatalf("BuildProgram() failed: %v", err) + } + return secbenchdef.Bench{ + Name: name, + Profile: secbenchdef.Profile(profile), + Program: instrs, + } +} + +func runRequest(runReq secbenchdef.BenchRunRequest) (secbenchdef.BenchRunResponse, error) { + runReqData, err := json.Marshal(&runReq) + if err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot serialize benchmark run request: %v", err) + } + runnerPath, err := testutil.FindFile("test/secbench/runner") + if err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot find runner binary: %v", err) + } + cmd := exec.Command(runnerPath) + cmd.Stderr = os.Stderr + stdin, err := cmd.StdinPipe() + if err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot attach pipe to stdin: %v", err) + } + defer stdin.Close() + stdout, err := cmd.StdoutPipe() + if err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot attach pipe to stdout: %v", err) + } + defer stdout.Close() + var stdoutData []byte + var stdoutErr error + var stdoutWait sync.WaitGroup + stdoutWait.Add(1) + go func() { + defer stdoutWait.Done() + stdoutData, stdoutErr = io.ReadAll(stdout) + }() + if err := cmd.Start(); err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot start runner: %v", err) + } + if _, err := stdin.Write(runReqData); err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot write benchmark instructions to runner: %v", err) + } + if err := stdin.Close(); err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot close runner stdin pipe: %v", err) + } + if err := cmd.Wait(); err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("runner failed: %v", err) + } + stdoutWait.Wait() + if stdoutErr != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("failed to read from runner stdout: %v", stdoutErr) + } + var runResp secbenchdef.BenchRunResponse + if err := json.Unmarshal(stdoutData, &runResp); err != nil { + return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot unmarshal response: %v", err) + } + return runResp, nil +} + +func evalSyscall(program bpf.Program, arch uint32, sc secbenchdef.Syscall) (uint32, error) { + scData := &linux.SeccompData{ + Nr: int32(sc.Sysno), + Arch: arch, + Args: [6]uint64{ + uint64(sc.Args[0]), + uint64(sc.Args[1]), + uint64(sc.Args[2]), + uint64(sc.Args[3]), + uint64(sc.Args[4]), + uint64(sc.Args[5]), + }, + } + return bpf.Exec(program, bpf.InputBytes{ + Data: marshal.Marshal(scData), + Order: hostarch.ByteOrder, + }) +} + +// Number of times we scale b.N by. +// Without this, a single iteration would be meaningless. +// Since the benchmark always runs with a single iteration first, +// we scale it so that even a single iteration means something. +const iterationScaleFactor = 128 + +// RunBench runs a single Bench. +func RunBench(b *testing.B, bn secbenchdef.Bench) { + b.Helper() + b.Run(bn.Name, func(b *testing.B) { + randSeed := time.Now().UnixNano() + b.Logf("Running with %d iterations (scaled by %dx), random seed %d...", b.N, iterationScaleFactor, randSeed) + iterations := uint64(b.N * iterationScaleFactor) + + // Check if there are any sequences where the syscall will be approved. + // If there is any, we will need to run the runner twice: Once with the + // filter, once without. Then we will compute the difference between the + // two runs. + // If there are no syscall sequences that will be approved, then we can + // skip running the runner the second time altogether. + program, err := bpf.Compile(bn.Program) + if err != nil { + b.Fatalf("program does not compile: %v", err) + } + activeSequences := make([]bool, len(bn.Profile.Sequences)) + positiveSequenceIndexes := make(map[int]struct{}, len(bn.Profile.Sequences)) + for i, seq := range bn.Profile.Sequences { + result := int64(-1) + for _, sc := range seq.Syscalls { + scResult, err := evalSyscall(program, bn.Profile.Arch, sc) + if err != nil { + b.Fatalf("cannot eval program with syscall %v: %v", sc, err) + } + if result == -1 { + result = int64(scResult) + } else if result != int64(scResult) { + b.Fatalf("sequence %v has incoherent syscall return results: %v vs %v", seq, result, scResult) + } + } + if result == -1 { + b.Fatalf("sequence %v is empty", seq) + } + if linux.BPFAction(result) == linux.SECCOMP_RET_ALLOW { + positiveSequenceIndexes[i] = struct{}{} + } else if !bn.AllowRejected { + b.Fatalf("sequence %v is disallowed (%v), but AllowRejected is false", seq, result) + } + activeSequences[i] = true + } + + // Run the runner with the seccomp filter. + runReq := secbenchdef.BenchRunRequest{ + Bench: bn, + Iterations: iterations, + ActiveSequences: activeSequences, + RandomSeed: randSeed, + InstallFilter: true, + } + runResp, err := runRequest(runReq) + if err != nil { + b.Fatalf("cannot run benchmark with the filter: %v", err) + } + + // Now run the runner without the seccomp filter, if necessary. + coherent := true + if len(positiveSequenceIndexes) > 0 { + onlyPositiveSequences := make([]bool, len(activeSequences)) + copy(onlyPositiveSequences, activeSequences) + for i := range bn.Profile.Sequences { + if _, found := positiveSequenceIndexes[i]; !found { + onlyPositiveSequences[i] = false + } + } + noFilterReq := runReq + noFilterReq.ActiveSequences = onlyPositiveSequences + noFilterReq.InstallFilter = false + b.Logf("Running allowed sequences only (%v), without the filter...", onlyPositiveSequences) + noFilterResp, err := runRequest(noFilterReq) + if err != nil { + b.Fatalf("cannot run benchmark without the filter: %v", err) + } + if noFilterResp.TotalNanos >= runResp.TotalNanos { + // This can happen for low iteration numbers where noise is high, so + // don't treat this as fatal. + b.Logf( + "It took us %v to run with filter, but %v without filter => run is incoherent", + time.Duration(runResp.TotalNanos)*time.Nanosecond, + time.Duration(noFilterResp.TotalNanos)*time.Nanosecond, + ) + coherent = false + } else { + b.Logf( + "Reducing total runtime (%v with filter) by %v without filter => %v for filter evaluation time", + time.Duration(runResp.TotalNanos)*time.Nanosecond, + time.Duration(noFilterResp.TotalNanos)*time.Nanosecond, + time.Duration(runResp.TotalNanos-noFilterResp.TotalNanos)*time.Nanosecond, + ) + runResp.TotalNanos -= noFilterResp.TotalNanos + for i := range onlyPositiveSequences { + // Same. + if noFilterResp.SequenceMetrics[i].TotalNanos >= runResp.SequenceMetrics[i].TotalNanos { + b.Logf( + "Sequence %v took %v to run with filter, but %v without filter => sequence is incoherent", + bn.Profile.Sequences[i], + time.Duration(runResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond, + time.Duration(noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond, + ) + // Invalidate the data by setting it to zero. + runResp.SequenceMetrics[i].TotalNanos = 0 + runResp.SequenceMetrics[i].Iterations = 0 + } else { + b.Logf( + "Reducing sequence %v runtime (%v with filter) by %v without filter => %v for filter evaluation time", + bn.Profile.Sequences[i], + time.Duration(runResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond, + time.Duration(noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond, + time.Duration(runResp.SequenceMetrics[i].TotalNanos-noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond, + ) + runResp.SequenceMetrics[i].TotalNanos -= noFilterResp.SequenceMetrics[i].TotalNanos + } + } + } + } + + if coherent { + // Report results. + if !bn.AllowRejected { + b.ReportMetric(float64(runResp.TotalNanos)/float64(iterations), "ns/op") + } else { + // Suppress default metric. + b.ReportMetric(0, "ns/op") + } + for i, seq := range bn.Profile.Sequences { + seqData := runResp.SequenceMetrics[i] + if seqData.Iterations < 100 { + // Too small number of attempts for this number to be precise, or + // invalidated earlier due to incoherence. Skip. + continue + } + // We don't use b.ReportMetric here because the number of iterations + // would be incorrect. + fmt.Fprintf(os.Stdout, "%s/%s %d %v ns/op\n", b.Name(), seq.Name, seqData.Iterations, float64(seqData.TotalNanos)/float64(seqData.Iterations)) + } + } else { + // Suppress default metric, which is useless for us here. + b.ReportMetric(0, "ns/op") + } + }) +} + +// Run runs a set of Benches. +func Run(b *testing.B, bns ...secbenchdef.Bench) { + b.Helper() + for _, bn := range bns { + RunBench(b, bn) + } + b.ReportMetric(0, "ns/op") +} diff --git a/test/secbench/secbenchdef/BUILD b/test/secbench/secbenchdef/BUILD new file mode 100644 index 000000000..7ed60abc6 --- /dev/null +++ b/test/secbench/secbenchdef/BUILD @@ -0,0 +1,21 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "secbenchdef", + srcs = [ + "secbenchdef.go", + "special_unsafe.go", + ], + visibility = [ + "//:sandbox", + ], + deps = [ + "//pkg/abi/linux", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/test/secbench/secbenchdef/secbenchdef.go b/test/secbench/secbenchdef/secbenchdef.go new file mode 100644 index 000000000..070451d14 --- /dev/null +++ b/test/secbench/secbenchdef/secbenchdef.go @@ -0,0 +1,152 @@ +// 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 secbenchdef contains struct definitions for secbench benchmarks. +// All structs in this package need to be JSON-serializable. +package secbenchdef + +import ( + "fmt" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +// Bench represents a benchmark to run. +type Bench struct { + // Name is the name of the benchmark. + Name string `json:"name"` + // Profile represents the syscall pattern profile being benchmarked. + Profile Profile `json:"profile"` + // Program is the seccomp-bpf program to run the benchmark with. + Program []linux.BPFInstruction `json:"program"` + // AllowRejected can be set to true if some sequences in the application + // profile are expected to not be allowed. + // If this is the case, the program's overall performance will not be + // reported. + AllowRejected bool `json:"allowRejected"` +} + +// Profile represents an application's syscall profile. +type Profile struct { + // Arch is the architecture of the application. + // Should be an AUDIT_ARCH_* value. + Arch uint32 `json:"arch"` + // Sequences is a set of weighted syscall sequences. + // A benchmark with a given Profile will run these sequences + // picked by weighted random choice. + Sequences []Sequence `json:"sequences"` +} + +// Sequence is a syscall sequence that the benchmark will make. +type Sequence struct { + // Name is the name of the sequence. + Name string `json:"name"` + // Weight is the weight of the sequence relative to all others within the + // same Profile. + Weight int `json:"weight"` + // Syscalls is the set of syscalls of the sequence. + Syscalls []Syscall `json:"syscalls"` +} + +// String returns the name of the Sequence. +func (s Sequence) String() string { + return s.Name +} + +const ( + // NonExistentFD is an FD that is overwhelmingly likely to not exist, + // because it would mean that the application has opened 2^31-1 FDs. + // Useful to make sure syscalls involving FDs don't actually + // do anything serious. + NonExistentFD = uintptr(0x7fffffff) + + // BadFD can be used as an invalid FD in syscall arguments. + BadFD = uintptr(0x80000000) +) + +// Syscall is a single syscall within a Sequence. +type Syscall struct { + // Special may be set for syscalls with special handling. + // If set, this takes precedence over the other fields. + Special SpecialSyscall `json:"special,omitempty"` + // Sysno is the syscall number. + Sysno uintptr `json:"sysno"` + // Args is the syscall arguments. + Args [6]uintptr `json:"args"` +} + +// Sys is a helper function to create a Syscall struct. +func Sys(sysno uintptr, args ...uintptr) Syscall { + if len(args) > 6 { + panic(fmt.Sprintf("cannot pass more than 6 syscall arguments, got: %v", args)) + } + var sixArgs [6]uintptr + for i := 0; i < len(args); i++ { + sixArgs[i] = args[i] + } + return Syscall{ + Sysno: sysno, + Args: sixArgs, + } +} + +// Single takes in a single syscall data and returns a one-item Syscall slice. +func Single(sysno uintptr, args ...uintptr) []Syscall { + return []Syscall{Sys(sysno, args...)} +} + +// Call calls the system call. +// +//go:nosplit +func (s *Syscall) Call() (r1 uintptr, r2 uintptr, err error) { + if s.Special != "" { + return s.Special.Call() + } + return unix.Syscall6(s.Sysno, s.Args[0], s.Args[1], s.Args[2], s.Args[3], s.Args[4], s.Args[5]) +} + +// BenchRunRequest encodes a request sent to the benchmark runner binary. +type BenchRunRequest struct { + // Bench is the benchmark being run. + Bench Bench `json:"bench"` + // Iterations is the number of iterations to do (b.N). + Iterations uint64 `json:"iterations"` + // RandomSeed is the random seed to use to pick sequences. + RandomSeed int64 `json:"randomSeed"` + // ActiveSequences[i] is true if Bench.Profile.Sequences[i] should be + // run. + ActiveSequences []bool `json:"activeSequences"` + // InstallFilter is true if the seccomp-bpf filter should be actually + // installed. Setting this to false allows measuring the filter-less + // performance, so that it can be subtracted from performance with the + // filter. + InstallFilter bool `json:"installFilter"` +} + +// SequenceMetrics is the per-sequence part of BenchRunResponse. +type SequenceMetrics struct { + Iterations uint64 `json:"iterations"` + TotalNanos uint64 `json:"totalNanos"` +} + +// BenchRunResponse encodes a response from the runner binary. +type BenchRunResponse struct { + // TotalNanos is the number of nanoseconds that the whole run took. + TotalNanos uint64 `json:"totalNanos"` + + // SequenceMetrics is the per-sequence metrics, mapped by index against + // the sequences in the Profile. + SequenceMetrics []SequenceMetrics `json:"sequenceMetrics"` +} diff --git a/test/secbench/secbenchdef/special_unsafe.go b/test/secbench/secbenchdef/special_unsafe.go new file mode 100644 index 000000000..a72bd17d2 --- /dev/null +++ b/test/secbench/secbenchdef/special_unsafe.go @@ -0,0 +1,105 @@ +// 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 secbenchdef + +import ( + "runtime" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +// SpecialSyscall are syscalls which need special handling. +// This can be syscalls where the arguments must be valid references to user +// memory. +type SpecialSyscall string + +const ( + // NanosleepZero calls nanosleep(2) to sleep for zero nanoseconds. + NanosleepZero = SpecialSyscall("NanosleepZero") + // PPollNonExistent calls ppoll(2) with a non-existent FD and a tiny timeout. + PPollNonExistent = SpecialSyscall("PPollNonExistent") + // RTSigreturn calls a system call that stands in the place of `rt_sigreturn(2)`. + RTSigreturn = SpecialSyscall("RTSigreturn") +) + +// Sys returns the Syscall struct for this special syscall. +func (s SpecialSyscall) Sys() Syscall { + return Syscall{Special: s} +} + +// Seq returns a one-item slice of the Syscall struct for this special syscall. +func (s SpecialSyscall) Seq() []Syscall { + return []Syscall{s.Sys()} +} + +// zeroNanoseconds is a timespec that represents zero nanoseconds. +var zeroNanosecond = &linux.Timespec{} + +// oneNanosecond is a timespec that represents a single nanosecond. +var oneNanosecond = &linux.Timespec{Nsec: 1} + +// ppollNonExistent is a PollFD struct with a non-existent FD and no events. +var ppollNonExistent = &linux.PollFD{FD: int32(NonExistentFD)} + +// args returns the syscall number and arguments to call, +// along with an array of references that must be kept alive if the syscall +// arguments should refer to valid user memory. +func (s SpecialSyscall) args() (sysno uintptr, args [6]uintptr, refs [6]any) { + switch s { + case NanosleepZero: + refs[0] = zeroNanosecond + args[0] = uintptr(unsafe.Pointer(zeroNanosecond)) + return unix.SYS_NANOSLEEP, args, refs + case PPollNonExistent: + refs[0] = ppollNonExistent + args[0] = uintptr(unsafe.Pointer(ppollNonExistent)) + args[1] = 1 + refs[2] = oneNanosecond + args[2] = uintptr(unsafe.Pointer(oneNanosecond)) + return unix.SYS_PPOLL, args, refs + case RTSigreturn: + // We use `request_key(2)` as a stand-in for `rt_sigreturn(2)`. + return unix.SYS_REQUEST_KEY, args, refs + default: + panic("invalid special syscall") + } +} + +// Data returns the seccomp data for this syscall. +func (s SpecialSyscall) Data(arch uint32) *linux.SeccompData { + sysno, args, _ := s.args() + return &linux.SeccompData{ + Nr: int32(sysno), + Arch: arch, + Args: [6]uint64{ + uint64(args[0]), + uint64(args[1]), + uint64(args[2]), + uint64(args[3]), + uint64(args[4]), + uint64(args[5]), + }, + } +} + +// Call calls this syscall. +func (s SpecialSyscall) Call() (r1 uintptr, r2 uintptr, err error) { + sysno, args, refs := s.args() + r1, r2, err = unix.Syscall6(sysno, args[0], args[1], args[2], args[3], args[4], args[5]) + runtime.KeepAlive(refs) + return r1, r2, err +}