gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/secbench/secbench.go (about)

     1  // Copyright 2023 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package secbench provides utilities for benchmarking seccomp-bpf filters.
    16  package secbench
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"testing"
    25  	"time"
    26  
    27  	"golang.org/x/sys/unix"
    28  	"gvisor.dev/gvisor/pkg/abi/linux"
    29  	"gvisor.dev/gvisor/pkg/bpf"
    30  	"gvisor.dev/gvisor/pkg/seccomp"
    31  	"gvisor.dev/gvisor/pkg/test/testutil"
    32  	"gvisor.dev/gvisor/test/secbench/secbenchdef"
    33  )
    34  
    35  // BenchFromSyscallRules returns a new Bench created from SyscallRules.
    36  func BenchFromSyscallRules(b *testing.B, name string, profile secbenchdef.Profile, rules seccomp.SyscallRules, denyRules seccomp.SyscallRules, options seccomp.ProgramOptions) secbenchdef.Bench {
    37  	// If there is a rule allowing rt_sigreturn to be called,
    38  	// also add a rule for the stand-in syscall number instead.
    39  	if rules.Has(unix.SYS_RT_SIGRETURN) {
    40  		rules = rules.Copy()
    41  		rules.Set(uintptr(secbenchdef.RTSigreturn.Data(profile.Arch).Nr), rules.Get(unix.SYS_RT_SIGRETURN))
    42  	}
    43  	// Also replace it in the list of hottest syscalls.
    44  	for i, sysno := range options.HotSyscalls {
    45  		if sysno == unix.SYS_RT_SIGRETURN {
    46  			options.HotSyscalls[i] = uintptr(secbenchdef.RTSigreturn.Data(profile.Arch).Nr)
    47  		}
    48  	}
    49  
    50  	options.DefaultAction = linux.SECCOMP_RET_ERRNO
    51  	options.BadArchAction = linux.SECCOMP_RET_ERRNO
    52  	insns, buildStats, err := seccomp.BuildProgram([]seccomp.RuleSet{
    53  		{
    54  			Rules:  denyRules,
    55  			Action: linux.SECCOMP_RET_ERRNO,
    56  		},
    57  		{
    58  			Rules:  rules,
    59  			Action: linux.SECCOMP_RET_ALLOW,
    60  		},
    61  	}, options)
    62  	if err != nil {
    63  		b.Fatalf("BuildProgram() failed: %v", err)
    64  	}
    65  	return secbenchdef.Bench{
    66  		Name:         name,
    67  		Profile:      secbenchdef.Profile(profile),
    68  		Instructions: insns,
    69  		BuildStats:   buildStats,
    70  	}
    71  }
    72  
    73  func runRequest(runReq secbenchdef.BenchRunRequest) (secbenchdef.BenchRunResponse, error) {
    74  	runReqData, err := json.Marshal(&runReq)
    75  	if err != nil {
    76  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot serialize benchmark run request: %v", err)
    77  	}
    78  	runnerPath, err := testutil.FindFile("test/secbench/runner")
    79  	if err != nil {
    80  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot find runner binary: %v", err)
    81  	}
    82  	cmd := exec.Command(runnerPath)
    83  	cmd.Stderr = os.Stderr
    84  	stdin, err := cmd.StdinPipe()
    85  	if err != nil {
    86  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot attach pipe to stdin: %v", err)
    87  	}
    88  	defer stdin.Close()
    89  	stdout, err := cmd.StdoutPipe()
    90  	if err != nil {
    91  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot attach pipe to stdout: %v", err)
    92  	}
    93  	defer stdout.Close()
    94  	if err := cmd.Start(); err != nil {
    95  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot start runner: %v", err)
    96  	}
    97  	if _, err := stdin.Write(runReqData); err != nil {
    98  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot write benchmark instructions to runner: %v", err)
    99  	}
   100  	if err := stdin.Close(); err != nil {
   101  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot close runner stdin pipe: %v", err)
   102  	}
   103  	stdoutData, err := io.ReadAll(stdout)
   104  	if err != nil {
   105  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("failed to read from runner stdout: %v", err)
   106  	}
   107  	if err := cmd.Wait(); err != nil {
   108  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("runner failed: %v", err)
   109  	}
   110  	var runResp secbenchdef.BenchRunResponse
   111  	if err := json.Unmarshal(stdoutData, &runResp); err != nil {
   112  		return secbenchdef.BenchRunResponse{}, fmt.Errorf("cannot unmarshal response: %v", err)
   113  	}
   114  	return runResp, nil
   115  }
   116  
   117  func evalSyscall(program bpf.Program, arch uint32, sc secbenchdef.Syscall, buf []byte) (uint32, error) {
   118  	return bpf.Exec[bpf.NativeEndian](program, seccomp.DataAsBPFInput(&linux.SeccompData{
   119  		Nr:   int32(sc.Sysno),
   120  		Arch: arch,
   121  		Args: [6]uint64{
   122  			uint64(sc.Args[0]),
   123  			uint64(sc.Args[1]),
   124  			uint64(sc.Args[2]),
   125  			uint64(sc.Args[3]),
   126  			uint64(sc.Args[4]),
   127  			uint64(sc.Args[5]),
   128  		},
   129  	}, buf))
   130  }
   131  
   132  // Number of times we scale b.N by.
   133  // Without this, a single iteration would be meaningless.
   134  // Since the benchmark always runs with a single iteration first,
   135  // we scale it so that even a single iteration means something.
   136  const iterationScaleFactor = 128
   137  
   138  // RunBench runs a single Bench.
   139  func RunBench(b *testing.B, bn secbenchdef.Bench) {
   140  	b.Helper()
   141  	b.Run(bn.Name, func(b *testing.B) {
   142  		randSeed := time.Now().UnixNano()
   143  		b.Logf("Running with %d iterations (scaled by %dx), random seed %d...", b.N, iterationScaleFactor, randSeed)
   144  		iterations := uint64(b.N * iterationScaleFactor)
   145  		buf := make([]byte, (&linux.SeccompData{}).SizeBytes())
   146  
   147  		// Check if there are any sequences where the syscall will be approved.
   148  		// If there is any, we will need to run the runner twice: Once with the
   149  		// filter, once without. Then we will compute the difference between the
   150  		// two runs.
   151  		// If there are no syscall sequences that will be approved, then we can
   152  		// skip running the runner the second time altogether.
   153  		program, err := bpf.Compile(bn.Instructions, true /* optimize */)
   154  		if err != nil {
   155  			b.Fatalf("program does not compile: %v", err)
   156  		}
   157  		b.ReportMetric(float64(bn.BuildStats.BuildDuration.Nanoseconds()), "build-ns")
   158  		b.ReportMetric(float64(bn.BuildStats.RuleOptimizeDuration.Nanoseconds()), "ruleopt-ns")
   159  		b.ReportMetric(float64(bn.BuildStats.BPFOptimizeDuration.Nanoseconds()), "bpfopt-ns")
   160  		b.ReportMetric(float64((bn.BuildStats.RuleOptimizeDuration + bn.BuildStats.BPFOptimizeDuration).Nanoseconds()), "opt-ns")
   161  		b.ReportMetric(float64(bn.BuildStats.SizeBeforeOptimizations), "gen-instr")
   162  		b.ReportMetric(float64(bn.BuildStats.SizeAfterOptimizations), "opt-instr")
   163  		b.ReportMetric(float64(bn.BuildStats.SizeBeforeOptimizations)/float64(bn.BuildStats.SizeAfterOptimizations), "compression-ratio")
   164  		activeSequences := make([]bool, len(bn.Profile.Sequences))
   165  		positiveSequenceIndexes := make(map[int]struct{}, len(bn.Profile.Sequences))
   166  		for i, seq := range bn.Profile.Sequences {
   167  			result := int64(-1)
   168  			for _, sc := range seq.Syscalls {
   169  				scResult, err := evalSyscall(program, bn.Profile.Arch, sc, buf)
   170  				if err != nil {
   171  					b.Fatalf("cannot eval program with syscall %v: %v", sc, err)
   172  				}
   173  				if result == -1 {
   174  					result = int64(scResult)
   175  				} else if result != int64(scResult) {
   176  					b.Fatalf("sequence %v has incoherent syscall return results: %v vs %v", seq, result, scResult)
   177  				}
   178  			}
   179  			if result == -1 {
   180  				b.Fatalf("sequence %v is empty", seq)
   181  			}
   182  			if linux.BPFAction(result) == linux.SECCOMP_RET_ALLOW {
   183  				positiveSequenceIndexes[i] = struct{}{}
   184  			} else if !bn.AllowRejected {
   185  				b.Fatalf("sequence %v is disallowed (%v), but AllowRejected is false", seq, result)
   186  			}
   187  			activeSequences[i] = true
   188  		}
   189  
   190  		// Run the runner with the seccomp filter.
   191  		runReq := secbenchdef.BenchRunRequest{
   192  			Bench:           bn,
   193  			Iterations:      iterations,
   194  			ActiveSequences: activeSequences,
   195  			RandomSeed:      randSeed,
   196  			InstallFilter:   true,
   197  		}
   198  		runResp, err := runRequest(runReq)
   199  		if err != nil {
   200  			b.Fatalf("cannot run benchmark with the filter: %v", err)
   201  		}
   202  
   203  		// Now run the runner without the seccomp filter, if necessary.
   204  		coherent := true
   205  		if len(positiveSequenceIndexes) > 0 {
   206  			onlyPositiveSequences := make([]bool, len(activeSequences))
   207  			copy(onlyPositiveSequences, activeSequences)
   208  			for i := range bn.Profile.Sequences {
   209  				if _, found := positiveSequenceIndexes[i]; !found {
   210  					onlyPositiveSequences[i] = false
   211  				}
   212  			}
   213  			noFilterReq := runReq
   214  			noFilterReq.ActiveSequences = onlyPositiveSequences
   215  			noFilterReq.InstallFilter = false
   216  			b.Logf("Running allowed sequences only (%v), without the filter...", onlyPositiveSequences)
   217  			noFilterResp, err := runRequest(noFilterReq)
   218  			if err != nil {
   219  				b.Fatalf("cannot run benchmark without the filter: %v", err)
   220  			}
   221  			if noFilterResp.TotalNanos >= runResp.TotalNanos {
   222  				// This can happen for low iteration numbers where noise is high, so
   223  				// don't treat this as fatal.
   224  				b.Logf(
   225  					"It took us %v to run with filter, but %v without filter => run is incoherent",
   226  					time.Duration(runResp.TotalNanos)*time.Nanosecond,
   227  					time.Duration(noFilterResp.TotalNanos)*time.Nanosecond,
   228  				)
   229  				coherent = false
   230  			} else {
   231  				b.Logf(
   232  					"Reducing total runtime (%v with filter) by %v without filter => %v for filter evaluation time",
   233  					time.Duration(runResp.TotalNanos)*time.Nanosecond,
   234  					time.Duration(noFilterResp.TotalNanos)*time.Nanosecond,
   235  					time.Duration(runResp.TotalNanos-noFilterResp.TotalNanos)*time.Nanosecond,
   236  				)
   237  				runResp.TotalNanos -= noFilterResp.TotalNanos
   238  				for i := range onlyPositiveSequences {
   239  					// Same.
   240  					if noFilterResp.SequenceMetrics[i].TotalNanos >= runResp.SequenceMetrics[i].TotalNanos {
   241  						b.Logf(
   242  							"Sequence %v took %v to run with filter, but %v without filter => sequence is incoherent",
   243  							bn.Profile.Sequences[i],
   244  							time.Duration(runResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond,
   245  							time.Duration(noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond,
   246  						)
   247  						// Invalidate the data by setting it to zero.
   248  						runResp.SequenceMetrics[i].TotalNanos = 0
   249  						runResp.SequenceMetrics[i].Iterations = 0
   250  					} else {
   251  						b.Logf(
   252  							"Reducing sequence %v runtime (%v with filter) by %v without filter => %v for filter evaluation time",
   253  							bn.Profile.Sequences[i],
   254  							time.Duration(runResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond,
   255  							time.Duration(noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond,
   256  							time.Duration(runResp.SequenceMetrics[i].TotalNanos-noFilterResp.SequenceMetrics[i].TotalNanos)*time.Nanosecond,
   257  						)
   258  						runResp.SequenceMetrics[i].TotalNanos -= noFilterResp.SequenceMetrics[i].TotalNanos
   259  					}
   260  				}
   261  			}
   262  		}
   263  
   264  		if coherent {
   265  			// Report results.
   266  			if !bn.AllowRejected {
   267  				b.ReportMetric(float64(runResp.TotalNanos)/float64(iterations), "ns/op")
   268  			} else {
   269  				// Suppress default metric.
   270  				b.ReportMetric(0, "ns/op")
   271  			}
   272  			for i, seq := range bn.Profile.Sequences {
   273  				seqData := runResp.SequenceMetrics[i]
   274  				if seqData.Iterations < 100 {
   275  					// Too small number of attempts for this number to be precise, or
   276  					// invalidated earlier due to incoherence. Skip.
   277  					continue
   278  				}
   279  				// We don't use b.ReportMetric here because the number of iterations
   280  				// would be incorrect.
   281  				fmt.Fprintf(os.Stdout, "%s/%s %d %v ns/op\n", b.Name(), seq.Name, seqData.Iterations, float64(seqData.TotalNanos)/float64(seqData.Iterations))
   282  			}
   283  		} else {
   284  			// Suppress default metric, which is useless for us here.
   285  			b.ReportMetric(0, "ns/op")
   286  		}
   287  	})
   288  }
   289  
   290  // Run runs a set of Benches.
   291  func Run(b *testing.B, bns ...secbenchdef.Bench) {
   292  	b.Helper()
   293  	for _, bn := range bns {
   294  		RunBench(b, bn)
   295  	}
   296  	b.ReportMetric(0, "ns/op")
   297  }