mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
committed by
gVisor bot
parent
81b7b4aa14
commit
853c80007f
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
@@ -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"],
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user