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 }