gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/bpf/program_builder.go (about)

     1  // Copyright 2018 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 bpf
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"sort"
    21  	"strings"
    22  
    23  	"gvisor.dev/gvisor/pkg/abi/linux"
    24  )
    25  
    26  const (
    27  	labelTarget       = math.MaxUint8
    28  	labelDirectTarget = math.MaxUint32
    29  )
    30  
    31  // ProgramBuilder assists with building a BPF program with jump
    32  // labels that are resolved to their proper offsets.
    33  type ProgramBuilder struct {
    34  	// Maps label names to label objects.
    35  	labels map[string]*label
    36  
    37  	// Maps label sources to the label name it references.
    38  	jumpSourceToLabel map[source]string
    39  
    40  	// unusableLabels are labels that are added before being referenced in a
    41  	// jump. Any labels added this way cannot be referenced later in order to
    42  	// avoid backwards references.
    43  	unusableLabels map[string]bool
    44  
    45  	// Array of BPF instructions that makes up the program.
    46  	instructions []Instruction
    47  }
    48  
    49  // NewProgramBuilder creates a new ProgramBuilder instance.
    50  func NewProgramBuilder() *ProgramBuilder {
    51  	return &ProgramBuilder{
    52  		labels:            map[string]*label{},
    53  		jumpSourceToLabel: map[source]string{},
    54  		unusableLabels:    map[string]bool{},
    55  	}
    56  }
    57  
    58  // label contains information to resolve a label to an offset.
    59  type label struct {
    60  	// List of locations that reference the label in the program.
    61  	sources []source
    62  
    63  	// Program line when the label is located.
    64  	target int
    65  }
    66  
    67  // JumpType is the type of jump target that an instruction may use.
    68  type JumpType int
    69  
    70  // Types of jump that an instruction may use.
    71  const (
    72  	JumpDirect JumpType = iota
    73  	JumpTrue
    74  	JumpFalse
    75  )
    76  
    77  // source contains information about a single reference to a label.
    78  type source struct {
    79  	// Program line where the label reference is present.
    80  	line int
    81  
    82  	// Which type of jump is referencing this label.
    83  	jt JumpType
    84  }
    85  
    86  // AddStmt adds a new statement to the program.
    87  func (b *ProgramBuilder) AddStmt(code uint16, k uint32) {
    88  	b.instructions = append(b.instructions, Stmt(code, k))
    89  }
    90  
    91  // AddJump adds a new jump to the program.
    92  func (b *ProgramBuilder) AddJump(code uint16, k uint32, jt, jf uint8) {
    93  	b.instructions = append(b.instructions, Jump(code, k, jt, jf))
    94  }
    95  
    96  // AddDirectJumpLabel adds a new jump to the program where is labelled.
    97  func (b *ProgramBuilder) AddDirectJumpLabel(labelName string) {
    98  	b.addLabelSource(labelName, JumpDirect)
    99  	b.AddJump(Jmp|Ja, labelDirectTarget, 0, 0)
   100  }
   101  
   102  // AddJumpTrueLabel adds a new jump to the program where 'jump if true' is a label.
   103  func (b *ProgramBuilder) AddJumpTrueLabel(code uint16, k uint32, jtLabel string, jf uint8) {
   104  	b.addLabelSource(jtLabel, JumpTrue)
   105  	b.AddJump(code, k, labelTarget, jf)
   106  }
   107  
   108  // AddJumpFalseLabel adds a new jump to the program where 'jump if false' is a label.
   109  func (b *ProgramBuilder) AddJumpFalseLabel(code uint16, k uint32, jt uint8, jfLabel string) {
   110  	b.addLabelSource(jfLabel, JumpFalse)
   111  	b.AddJump(code, k, jt, labelTarget)
   112  }
   113  
   114  // AddJumpLabels adds a new jump to the program where both jump targets are labels.
   115  func (b *ProgramBuilder) AddJumpLabels(code uint16, k uint32, jtLabel, jfLabel string) {
   116  	b.addLabelSource(jtLabel, JumpTrue)
   117  	b.addLabelSource(jfLabel, JumpFalse)
   118  	b.AddJump(code, k, labelTarget, labelTarget)
   119  }
   120  
   121  // AddLabel sets the given label name at the current location. The next instruction is executed
   122  // when the any code jumps to this label. More than one label can be added to the same location.
   123  func (b *ProgramBuilder) AddLabel(name string) error {
   124  	l, ok := b.labels[name]
   125  	if !ok {
   126  		if _, ok = b.unusableLabels[name]; ok {
   127  			return fmt.Errorf("label %q already set", name)
   128  		}
   129  		// Mark the label as unusable. This is done to catch backwards jumps.
   130  		b.unusableLabels[name] = true
   131  		return nil
   132  	}
   133  	if l.target != -1 {
   134  		return fmt.Errorf("label %q target already set: %v", name, l.target)
   135  	}
   136  	l.target = len(b.instructions)
   137  	return nil
   138  }
   139  
   140  // Instructions returns an array of BPF instructions representing the program with all labels
   141  // resolved. Return error in case label resolution failed due to an invalid program.
   142  //
   143  // N.B. Partial results will be returned in the error case, which is useful for debugging.
   144  func (b *ProgramBuilder) Instructions() ([]Instruction, error) {
   145  	if err := b.resolveLabels(); err != nil {
   146  		return b.instructions, err
   147  	}
   148  	return b.instructions, nil
   149  }
   150  
   151  func (b *ProgramBuilder) addLabelSource(labelName string, t JumpType) {
   152  	l, ok := b.labels[labelName]
   153  	if !ok {
   154  		l = &label{sources: make([]source, 0), target: -1}
   155  		b.labels[labelName] = l
   156  	}
   157  	src := source{line: len(b.instructions), jt: t}
   158  	l.sources = append(l.sources, src)
   159  	if existingLabel, found := b.jumpSourceToLabel[src]; found {
   160  		panic(fmt.Sprintf("label %q already present at source %v; one source may only have one label", existingLabel, src))
   161  	}
   162  	b.jumpSourceToLabel[src] = labelName
   163  }
   164  
   165  func (b *ProgramBuilder) resolveLabels() error {
   166  	for key, v := range b.labels {
   167  		if _, ok := b.unusableLabels[key]; ok {
   168  			return fmt.Errorf("backwards reference detected for label: %q", key)
   169  		}
   170  
   171  		if v.target == -1 {
   172  			return fmt.Errorf("label target not set: %v", key)
   173  		}
   174  		if v.target >= len(b.instructions) {
   175  			return fmt.Errorf("target is beyond end of ProgramBuilder")
   176  		}
   177  		for _, s := range v.sources {
   178  			// Finds jump instruction that references the label.
   179  			inst := b.instructions[s.line]
   180  			if s.line >= v.target {
   181  				return fmt.Errorf("cannot jump backwards")
   182  			}
   183  			// Calculates the jump offset from current line.
   184  			offset := v.target - s.line - 1
   185  			// Sets offset into jump instruction.
   186  			switch s.jt {
   187  			case JumpDirect:
   188  				if offset > labelDirectTarget {
   189  					return fmt.Errorf("jump offset to label '%v' is too large: %v, inst: %v, lineno: %v", key, offset, inst, s.line)
   190  				}
   191  				if inst.K != labelDirectTarget {
   192  					return fmt.Errorf("jump target is not a label")
   193  				}
   194  				inst.K = uint32(offset)
   195  			case JumpTrue:
   196  				if offset > labelTarget {
   197  					return fmt.Errorf("jump offset to label '%v' is too large: %v, inst: %v, lineno: %v", key, offset, inst, s.line)
   198  				}
   199  				if inst.JumpIfTrue != labelTarget {
   200  					return fmt.Errorf("jump target is not a label")
   201  				}
   202  				inst.JumpIfTrue = uint8(offset)
   203  			case JumpFalse:
   204  				if offset > labelTarget {
   205  					return fmt.Errorf("jump offset to label '%v' is too large: %v, inst: %v, lineno: %v", key, offset, inst, s.line)
   206  				}
   207  				if inst.JumpIfFalse != labelTarget {
   208  					return fmt.Errorf("jump target is not a label")
   209  				}
   210  				inst.JumpIfFalse = uint8(offset)
   211  			}
   212  
   213  			b.instructions[s.line] = inst
   214  		}
   215  	}
   216  	clear(b.labels)
   217  	return nil
   218  }
   219  
   220  // ProgramFragment is a set of not-compiled instructions that were added to
   221  // a ProgramBuilder from the moment the `Record` function was called on it.
   222  type ProgramFragment struct {
   223  	// b is a reference to the ProgramBuilder that this is a fragment from.
   224  	b *ProgramBuilder
   225  
   226  	// fromPC is the index of the first instruction that was recorded.
   227  	// If no instruction was recorded, this index will be equal to `toPC`.
   228  	fromPC int
   229  
   230  	// toPC is the index *after* the last instruction that was recorded.
   231  	// This means that right after recording, the program will not have
   232  	// any instruction at index `toPC`.
   233  	toPC int
   234  }
   235  
   236  // Record starts recording the instructions being added to the ProgramBuilder
   237  // until the returned function is called.
   238  // The returned function returns a ProgramFragment which represents the
   239  // recorded instructions. It may be called repeatedly.
   240  func (b *ProgramBuilder) Record() func() ProgramFragment {
   241  	currentPC := len(b.instructions)
   242  	return func() ProgramFragment {
   243  		return ProgramFragment{
   244  			b:      b,
   245  			fromPC: currentPC,
   246  			toPC:   len(b.instructions),
   247  		}
   248  	}
   249  }
   250  
   251  // String returns a string version of the fragment.
   252  func (f ProgramFragment) String() string {
   253  	return fmt.Sprintf("fromPC=%d toPC=%d", f.fromPC, f.toPC)
   254  }
   255  
   256  // FragmentOutcomes represents the set of outcomes that a ProgramFragment
   257  // execution may result into.
   258  type FragmentOutcomes struct {
   259  	// MayFallThrough is true if executing the fragment may cause it to start
   260  	// executing the program instruction that comes right after the last
   261  	// instruction in this fragment (i.e. at `Fragment.toPC`).
   262  	MayFallThrough bool
   263  
   264  	// MayJumpToKnownOffsetBeyondFragment is true if executing the fragment may
   265  	// jump to a fixed offset (or resolved label) that is not within the range
   266  	// of the fragment itself, nor does it point to the instruction that would
   267  	// come right after this fragment.
   268  	// If the fragment jumps to an unresolved label, this will instead be
   269  	// indicated in `MayJumpToUnresolvedLabels`.
   270  	MayJumpToKnownOffsetBeyondFragment bool
   271  
   272  	// MayJumpToUnresolvedLabels is the set of named labels that have not yet
   273  	// been added to the program (the labels are not resolvable) but that the
   274  	// fragment may jump to.
   275  	MayJumpToUnresolvedLabels map[string]struct{}
   276  
   277  	// MayReturnImmediate contains the set of possible immediate return values
   278  	// that the fragment may return.
   279  	MayReturnImmediate map[linux.BPFAction]struct{}
   280  
   281  	// MayReturnRegisterA is true if the fragment may return the value of
   282  	// register A.
   283  	MayReturnRegisterA bool
   284  }
   285  
   286  // String returns a list of possible human-readable outcomes.
   287  func (o FragmentOutcomes) String() string {
   288  	var s []string
   289  	if o.MayJumpToKnownOffsetBeyondFragment {
   290  		s = append(s, "may jump to known offset beyond fragment")
   291  	}
   292  	sortedLabels := make([]string, 0, len(o.MayJumpToUnresolvedLabels))
   293  	for lbl := range o.MayJumpToUnresolvedLabels {
   294  		sortedLabels = append(sortedLabels, lbl)
   295  	}
   296  	sort.Strings(sortedLabels)
   297  	for _, lbl := range sortedLabels {
   298  		s = append(s, fmt.Sprintf("may jump to unresolved label %q", lbl))
   299  	}
   300  	if o.MayFallThrough {
   301  		s = append(s, "may fall through")
   302  	}
   303  	sortedReturnValues := make([]uint32, 0, len(o.MayReturnImmediate))
   304  	for v := range o.MayReturnImmediate {
   305  		sortedReturnValues = append(sortedReturnValues, uint32(v))
   306  	}
   307  	sort.Slice(sortedReturnValues, func(i, j int) bool {
   308  		return sortedReturnValues[i] < sortedReturnValues[j]
   309  	})
   310  	for _, v := range sortedReturnValues {
   311  		s = append(s, fmt.Sprintf("may return '0x%x'", v))
   312  	}
   313  	if o.MayReturnRegisterA {
   314  		s = append(s, "may return register A")
   315  	}
   316  	if len(s) == 0 {
   317  		return "no outcomes (this should never happen)"
   318  	}
   319  	return strings.Join(s, ", ")
   320  }
   321  
   322  // MayReturn returns whether the fragment may return for any reason.
   323  func (o FragmentOutcomes) MayReturn() bool {
   324  	return len(o.MayReturnImmediate) > 0 || o.MayReturnRegisterA
   325  }
   326  
   327  // Outcomes returns the set of possible outcomes that executing this fragment
   328  // may result into.
   329  func (f ProgramFragment) Outcomes() FragmentOutcomes {
   330  	if f.fromPC == f.toPC {
   331  		// No instructions, this just falls through.
   332  		return FragmentOutcomes{
   333  			MayFallThrough: true,
   334  		}
   335  	}
   336  	outcomes := FragmentOutcomes{
   337  		MayJumpToUnresolvedLabels: make(map[string]struct{}),
   338  		MayReturnImmediate:        make(map[linux.BPFAction]struct{}),
   339  	}
   340  	for pc := f.fromPC; pc < f.toPC; pc++ {
   341  		ins := f.b.instructions[pc]
   342  		isLastInstruction := pc == f.toPC-1
   343  		switch ins.OpCode & instructionClassMask {
   344  		case Ret:
   345  			switch ins.OpCode {
   346  			case Ret | K:
   347  				outcomes.MayReturnImmediate[linux.BPFAction(ins.K)] = struct{}{}
   348  			case Ret | A:
   349  				outcomes.MayReturnRegisterA = true
   350  			}
   351  		case Jmp:
   352  			for _, offset := range ins.JumpOffsets() {
   353  				var foundLabel *label
   354  				foundLabelName, found := f.b.jumpSourceToLabel[source{line: pc, jt: offset.Type}]
   355  				if found {
   356  					foundLabel = f.b.labels[foundLabelName]
   357  					if foundLabel.target == -1 {
   358  						outcomes.MayJumpToUnresolvedLabels[foundLabelName] = struct{}{}
   359  						continue
   360  					}
   361  				}
   362  				var target int
   363  				if foundLabel != nil {
   364  					target = foundLabel.target
   365  				} else {
   366  					target = pc + int(offset.Offset) + 1
   367  				}
   368  				if target == f.toPC {
   369  					outcomes.MayFallThrough = true
   370  				} else if target > f.toPC {
   371  					outcomes.MayJumpToKnownOffsetBeyondFragment = true
   372  				}
   373  			}
   374  		default:
   375  			if isLastInstruction {
   376  				outcomes.MayFallThrough = true
   377  			}
   378  		}
   379  	}
   380  	return outcomes
   381  }
   382  
   383  // MayModifyRegisterA returns whether this fragment may modify register A.
   384  // A value of "true" does not necessarily mean that A *will* be modified,
   385  // as the control flow of this fragment may skip over instructions that
   386  // modify the A register.
   387  func (f ProgramFragment) MayModifyRegisterA() bool {
   388  	for pc := f.fromPC; pc < f.toPC; pc++ {
   389  		if f.b.instructions[pc].ModifiesRegisterA() {
   390  			return true
   391  		}
   392  	}
   393  	return false
   394  }