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  }