gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/seccomp/precompiledseccomp/precompiledseccomp.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 precompiledseccomp provides tooling to precompile seccomp-bpf 16 // programs that can be embedded inside Go source code. 17 package precompiledseccomp 18 19 import ( 20 "encoding/binary" 21 "fmt" 22 "sort" 23 "strings" 24 25 "gvisor.dev/gvisor/pkg/bpf" 26 "gvisor.dev/gvisor/pkg/log" 27 "gvisor.dev/gvisor/pkg/seccomp" 28 ) 29 30 // ProgramDesc describes a program to be compiled. 31 type ProgramDesc struct { 32 // Rules contains the seccomp-bpf rulesets to compile. 33 Rules []seccomp.RuleSet 34 35 // SeccompOptions is the seccomp-bpf program options used in compilation. 36 SeccompOptions seccomp.ProgramOptions 37 } 38 39 // Program is a precompiled seccomp-bpf program. 40 // To get actual BPF instructions, call the `RenderInstructions` function. 41 type Program struct { 42 // Name is the name of this program within a set of embedded programs. 43 Name string 44 45 // Bytecode32 is the raw BPF bytecode represented as a sequence of uint32s. 46 Bytecode32 []uint32 47 48 // VarOffsets maps variable names to the uint32-based offsets where these 49 // variables show up in `Bytecode32`. 50 VarOffsets map[string][]int 51 } 52 53 // Values is an assignment of variables to uint32 values. 54 // It is used when rendering seccomp-bpf program instructions. 55 type Values map[string]uint32 56 57 const ( 58 uint64VarSuffixHigh = "_high32bits" 59 uint64VarSuffixLow = "_low32bits" 60 ) 61 62 // SetUint64 sets the value of a 64-bit variable in `v`. 63 // Under the hood, this is stored as two 32-bit variables. 64 // Use `Values.GetUint64` to retrieve the 64-bit variable. 65 func (v Values) SetUint64(varName string, value uint64) { 66 v[varName+uint64VarSuffixHigh] = uint32(value >> 32) 67 v[varName+uint64VarSuffixLow] = uint32(value) 68 } 69 70 // GetUint64 retrieves the value of a 64-bit variable set using 71 // `Values.SetUint64(varName)`. 72 func (v Values) GetUint64(varName string) uint64 { 73 return uint64(v[varName+"_high32bits"])<<32 | uint64(v[varName+"_low32bits"]) 74 } 75 76 // Precompile compiles a `ProgramDesc` with the given values. 77 // It supports the notion of "variables", which are named in `vars`. 78 // Variables are uint32s which are only known at runtime, and whose value 79 // shows up in the BPF bytecode. 80 // 81 // `fn` takes in a mapping of variable names to their assigned values, 82 // and should return a `ProgramDesc` describing the seccomp-bpf program 83 // to be compiled. 84 // 85 // Precompile verifies that all variables in `vars` show up consistently in 86 // the bytecode by compiling the program twice, ensures that the offsets at 87 // which some stand-in values is consistent across these two compilation 88 // attempts, and that nothing else about the BPF bytecode is different. 89 func Precompile(name string, varNames []string, fn func(Values) ProgramDesc) (Program, error) { 90 vars := make(map[string]struct{}, len(varNames)) 91 for _, varName := range varNames { 92 vars[varName] = struct{}{} 93 } 94 if len(vars) != len(varNames) { 95 return Program{}, fmt.Errorf("non-unique variable names: %q", varNames) 96 } 97 98 // These constants are chosen to be recognizable and unique within 99 // seccomp-bpf programs. 100 // These could of course show up in seccomp-bpf programs for legitimate 101 // reasons other than being part the variable being matched against (e.g. a 102 // jump of this many instructions forward, or a static equality match that 103 // happens to check against this exact value), but it is very unlikely that 104 // integers this large actually occur. 105 // If it does happen, we'll catch it here because one compilation attempt 106 // will find its placeholder values show up less often than the other. 107 // Assuming that the reason this occurred is legitimate, update these 108 // constants to even-less-likely values in order to fix this issue. 109 const ( 110 varStart1 uint32 = 0x13371337 111 varStart2 uint32 = 0x42424243 112 ) 113 114 // Render the program with one set of values. 115 // Remember at which offsets we saw these values show up in the bytecode. 116 values1 := Values(make(map[string]uint32, len(vars))) 117 v := varStart1 118 for varName := range vars { 119 values1[varName] = v 120 v += 2 121 } 122 program1, err := precompile(name, values1, fn) 123 if err != nil { 124 return Program{}, err 125 } 126 127 // Do the same, but with a different set of values. 128 values2 := Values(make(map[string]uint32, len(vars))) 129 v = varStart2 130 for _, varName := range varNames { 131 values2[varName] = v 132 v += 2 133 } 134 program2, err := precompile(name, values2, fn) 135 if err != nil { 136 return Program{}, err 137 } 138 139 // Ensure that the offsets we got is consistent. 140 for _, varName := range varNames { 141 offsets1 := program1.VarOffsets[varName] 142 offsets2 := program2.VarOffsets[varName] 143 if len(offsets1) != len(offsets2) { 144 return Program{}, fmt.Errorf("var %q has different number of offsets depending on its value: with value 0x%08x it showed up %d times, but with value %d it showed up %d times", varName, values1[varName], len(offsets1), values2[varName], len(offsets2)) 145 } 146 for i := 0; i < len(offsets1); i++ { 147 if offsets1[i] != offsets2[i] { 148 return Program{}, fmt.Errorf("var %q has different offsets depending on its value: with value 0x%08x it showed up at offsets %v, but with value %d it showed up at offsets %v", varName, values1[varName], offsets1, values2[varName], offsets2) 149 } 150 } 151 } 152 153 // Ensure that the rest of the bytecode is exactly equal. 154 if len(program1.Bytecode32) != len(program2.Bytecode32) { 155 return Program{}, fmt.Errorf("compiled programs do not have the same bytecode size: %d vs %d", len(program1.Bytecode32), len(program2.Bytecode32)) 156 } 157 knownOffsets := map[int]struct{}{} 158 for _, varName := range varNames { 159 for _, offset := range program1.VarOffsets[varName] { 160 knownOffsets[offset] = struct{}{} 161 } 162 } 163 for i := 0; i < len(program1.Bytecode32); i++ { 164 if _, isVarOffset := knownOffsets[i]; isVarOffset { 165 continue 166 } 167 if program1.Bytecode32[i] != program2.Bytecode32[i] { 168 return Program{}, fmt.Errorf("compiled programs do not have the same bytecode at uint32 offset %d (which is not any of the offsets where a variable shows up: %v)", i, knownOffsets) 169 } 170 } 171 172 return program1, nil 173 } 174 175 // precompile compiles a `ProgramDesc` with the given values. 176 func precompile(name string, values Values, fn func(Values) ProgramDesc) (Program, error) { 177 precompileOpts := fn(values) 178 insns, _, err := seccomp.BuildProgram(precompileOpts.Rules, precompileOpts.SeccompOptions) 179 if err != nil { 180 return Program{}, err 181 } 182 if log.IsLogging(log.Debug) { 183 log.Debugf("Compiled program with values %v (%d instructions):", values, len(insns)) 184 for i, insn := range insns { 185 log.Debugf(" %04d: %s\n", i, insn.String()) 186 } 187 } 188 bytecode32 := instructionsToUint32Slice(insns) 189 varOffsets := getVarOffsets(bytecode32, values) 190 191 // nonOptimizedOffsets stores the offsets at which each variable shows up 192 // in the non-optimized version of the program. It is only computed when 193 // a variable doesn't show up in the optimized version of the program. 194 var nonOptimizedOffsets map[string][]int 195 computeNonOptimizedOffsets := func() error { 196 if nonOptimizedOffsets != nil { 197 return nil 198 } 199 if !precompileOpts.SeccompOptions.Optimize { 200 nonOptimizedOffsets = varOffsets 201 return nil 202 } 203 nonOptimizedOpts := precompileOpts.SeccompOptions 204 nonOptimizedOpts.Optimize = false 205 nonOptInsns, _, err := seccomp.BuildProgram(precompileOpts.Rules, nonOptimizedOpts) 206 if err != nil { 207 return fmt.Errorf("cannot build seccomp program with optimizations disabled: %w", err) 208 } 209 nonOptimizedOffsets = getVarOffsets(instructionsToUint32Slice(nonOptInsns), values) 210 return nil 211 } 212 213 for varName := range values { 214 if len(varOffsets[varName]) == 0 { 215 // If the variable doesn't show up in the optimized program but does 216 // show up in the non-optimized program, then it is not unused. 217 // It is being optimized away, e.g. as a result of being OR'd with a 218 // `MatchAll` rule. 219 // Only report an error if the variable shows up in neither optimized 220 // nor non-optimized bytecode. 221 if err := computeNonOptimizedOffsets(); err != nil { 222 return Program{}, fmt.Errorf("cannot compute variable offsets for the non-optimized version of the program: %v", err) 223 } 224 if len(nonOptimizedOffsets[varName]) == 0 { 225 return Program{}, fmt.Errorf("var %q does not show up in the BPF bytecode", varName) 226 } 227 // We set the offset slice for this variable to a nil slice, so that 228 // it gets properly serialized (as opposed to omitted entirely) in the 229 // generated Go code. 230 varOffsets[varName] = nil 231 } 232 } 233 return Program{ 234 Name: name, 235 Bytecode32: bytecode32, 236 VarOffsets: varOffsets, 237 }, nil 238 } 239 240 // getVarOffsets returns the uint32-based offsets at which the values of each 241 // variable in `values` shows up. 242 func getVarOffsets(bytecode32 []uint32, values Values) map[string][]int { 243 varOffsets := make(map[string][]int, len(values)) 244 for varName, value := range values { 245 for i, v := range bytecode32 { 246 if v == value { 247 varOffsets[varName] = append(varOffsets[varName], i) 248 } 249 } 250 } 251 return varOffsets 252 } 253 254 // Expr renders a Go expression encoding this `Program`. 255 // It is used when embedding a precompiled `Program` into a Go library file. 256 // `pkgName` is the package name under which the precompiledseccomp package is 257 // imported. 258 func (program Program) Expr(indentPrefix, pkgName string) string { 259 var sb strings.Builder 260 sb.WriteString(fmt.Sprintf("%s.Program{\n", pkgName)) 261 sb.WriteString(fmt.Sprintf("%s\tName: %q,\n", indentPrefix, program.Name)) 262 sb.WriteString(fmt.Sprintf("%s\tBytecode32: []uint32{\n", indentPrefix)) 263 for _, v := range program.Bytecode32 { 264 sb.WriteString(fmt.Sprintf("%s\t\t0x%08x,\n", indentPrefix, v)) 265 } 266 sb.WriteString(fmt.Sprintf("%s\t},\n", indentPrefix)) 267 sb.WriteString(fmt.Sprintf("%s\tVarOffsets: map[string][]int{\n", indentPrefix)) 268 varNames := make([]string, 0, len(program.VarOffsets)) 269 for varName := range program.VarOffsets { 270 varNames = append(varNames, varName) 271 } 272 sort.Strings(varNames) 273 for _, varName := range varNames { 274 if len(program.VarOffsets[varName]) == 0 { 275 sb.WriteString(fmt.Sprintf("%s\t\t%q: nil,\n", indentPrefix, varName)) 276 continue 277 } 278 sb.WriteString(fmt.Sprintf("%s\t\t%q: []int{\n", indentPrefix, varName)) 279 for _, v := range program.VarOffsets[varName] { 280 sb.WriteString(fmt.Sprintf("%s\t\t\t%d,\n", indentPrefix, v)) 281 } 282 sb.WriteString(fmt.Sprintf("%s\t\t},\n", indentPrefix)) 283 } 284 sb.WriteString(fmt.Sprintf("%s\t},\n", indentPrefix)) 285 sb.WriteString(fmt.Sprintf("%s}", indentPrefix)) 286 return sb.String() 287 } 288 289 // RenderInstructions builds the set of precompiled BPF instructions, 290 // replacing the variables with their values as given in `values`. 291 // This must be called with the exact same set of variable names as was used 292 // during `Precompile`. 293 func (program Program) RenderInstructions(values Values) ([]bpf.Instruction, error) { 294 if len(values) != len(program.VarOffsets) { 295 return nil, fmt.Errorf("called with inconsistent vars: got %v expected %v", values, program.VarOffsets) 296 } 297 for varName, value := range values { 298 offsets, found := program.VarOffsets[varName] 299 if !found { 300 return nil, fmt.Errorf("var %q was not defined in precompiled instructions (defined: %v)", varName, program.VarOffsets) 301 } 302 for _, offset := range offsets { 303 program.Bytecode32[offset] = value 304 } 305 } 306 return uint32SliceToInstructions(program.Bytecode32) 307 } 308 309 // instructionsToUint32Slice converts a slice of BPF instructions into a slice 310 // of uint32s containing the same binary data. 311 func instructionsToUint32Slice(insns []bpf.Instruction) []uint32 { 312 bytecode := bpf.ToBytecode(insns) 313 bytecode32 := make([]uint32, len(bytecode)/4) 314 for i := 0; i < len(bytecode); i += 4 { 315 bytecode32[i/4] = binary.NativeEndian.Uint32(bytecode[i : i+4]) 316 } 317 return bytecode32 318 } 319 320 // uint32SliceToInstructions converts a slice of uint32s into a slice of 321 // BPF instructions containing the same binary data. 322 func uint32SliceToInstructions(bytecode32 []uint32) ([]bpf.Instruction, error) { 323 bytecode := make([]byte, len(bytecode32)*4) 324 for i, v := range bytecode32 { 325 binary.NativeEndian.PutUint32(bytecode[i*4:], v) 326 } 327 return bpf.ParseBytecode(bytecode) 328 } 329 330 // Registration outputs Go code that registers this programs in a 331 // `map[string]Program` variable named `programsMapVarName` which maps 332 // programs names to their `Program` struct. 333 // It is used when embedding precompiled programs into a Go library file. 334 func (program Program) Registration(indentPrefix, pkgName, programsMapVarName string) string { 335 return fmt.Sprintf("%s%s[%q] = %s\n", indentPrefix, programsMapVarName, program.Name, program.Expr(indentPrefix, pkgName)) 336 }