Add library to benchmark gVisor's seccomp-bpf filters.

The first step to optimizing something is to measure it.

This creates a `secbench` library which can benchmark the time it takes to
run an application "Profile", which is a weighted-random set of syscall
sequences, under a given `seccomp-bpf` filter.

The `secbench` library runs two subprocesses which run the same set of system
calls using this application profile. One of the subprocesses runs with the
seccomp-bpf filter, and the other one runs without it. The library computes
the difference, and reports this duration as the time it takes to run the
seccomp-bpf filter for that system call. It also reports the overall time it
takes to run the weighted-random set of system calls, useful as a general
measure of the overhead that the application will see.

PiperOrigin-RevId: 567704927
This commit is contained in:
Etienne Perot
2023-09-22 13:38:04 -07:00
committed by gVisor bot
parent 81b7b4aa14
commit 853c80007f
10 changed files with 911 additions and 3 deletions
+15
View File
@@ -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",
],
)
+9 -3
View File
@@ -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.
+95
View File
@@ -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,
))
}
+47
View File
@@ -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"],
)
+18
View File
@@ -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)
+156
View File
@@ -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()
}
+293
View File
@@ -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")
}
+21
View File
@@ -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",
],
)
+152
View File
@@ -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"`
}
+105
View File
@@ -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
}