github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/prog/hints.go (about)

     1  // Copyright 2017 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package prog
     5  
     6  // A hint is basically a tuple consisting of a pointer to an argument
     7  // in one of the syscalls of a program and a value, which should be
     8  // assigned to that argument (we call it a replacer).
     9  
    10  // A simplified version of hints workflow looks like this:
    11  //		1. Fuzzer launches a program (we call it a hint seed) and collects all
    12  // the comparisons' data for every syscall in the program.
    13  //		2. Next it tries to match the obtained comparison operands' values
    14  // vs. the input arguments' values.
    15  //		3. For every such match the fuzzer mutates the program by
    16  // replacing the pointed argument with the saved value.
    17  //		4. If a valid program is obtained, then fuzzer launches it and
    18  // checks if new coverage is obtained.
    19  // For more insights on particular mutations please see prog/hints_test.go.
    20  
    21  import (
    22  	"bytes"
    23  	"encoding/binary"
    24  	"fmt"
    25  	"sort"
    26  
    27  	"github.com/google/syzkaller/pkg/image"
    28  )
    29  
    30  // Example: for comparisons {(op1, op2), (op1, op3), (op1, op4), (op2, op1)}
    31  // this map will store the following:
    32  //
    33  //	m = {
    34  //			op1: {map[op2]: true, map[op3]: true, map[op4]: true},
    35  //			op2: {map[op1]: true}
    36  //	}.
    37  type CompMap map[uint64]map[uint64]bool
    38  
    39  const (
    40  	maxDataLength = 100
    41  )
    42  
    43  var specialIntsSet map[uint64]bool
    44  
    45  func (m CompMap) AddComp(arg1, arg2 uint64) {
    46  	if _, ok := m[arg1]; !ok {
    47  		m[arg1] = make(map[uint64]bool)
    48  	}
    49  	m[arg1][arg2] = true
    50  }
    51  
    52  func (m CompMap) String() string {
    53  	buf := new(bytes.Buffer)
    54  	for v, comps := range m {
    55  		if len(buf.Bytes()) != 0 {
    56  			fmt.Fprintf(buf, ", ")
    57  		}
    58  		fmt.Fprintf(buf, "0x%x:", v)
    59  		for c := range comps {
    60  			fmt.Fprintf(buf, " 0x%x", c)
    61  		}
    62  	}
    63  	return buf.String()
    64  }
    65  
    66  // InplaceIntersect() only leaves the value pairs that are also present in other.
    67  func (m CompMap) InplaceIntersect(other CompMap) {
    68  	for val1, nested := range m {
    69  		for val2 := range nested {
    70  			if !other[val1][val2] {
    71  				delete(nested, val2)
    72  			}
    73  		}
    74  		if len(nested) == 0 {
    75  			delete(m, val1)
    76  		}
    77  	}
    78  }
    79  
    80  // Mutates the program using the comparison operands stored in compMaps.
    81  // For each of the mutants executes the exec callback.
    82  // The callback must return whether we should continue substitution (true)
    83  // or abort the process (false).
    84  func (p *Prog) MutateWithHints(callIndex int, comps CompMap, exec func(p *Prog) bool) {
    85  	p = p.Clone()
    86  	c := p.Calls[callIndex]
    87  	doMore := true
    88  	execValidate := func() bool {
    89  		// Don't try to fix the candidate program.
    90  		// Assuming the original call was sanitized, we've got a bad call
    91  		// as the result of hint substitution, so just throw it away.
    92  		if p.Target.sanitize(c, false) != nil {
    93  			return true
    94  		}
    95  		if p.checkConditions() != nil {
    96  			// Patching unions that no longer satisfy conditions would
    97  			// require much deeped changes to prog arguments than
    98  			// generateHints() expects.
    99  			// Let's just ignore such mutations.
   100  			return true
   101  		}
   102  		p.debugValidate()
   103  		doMore = exec(p)
   104  		return doMore
   105  	}
   106  	ForeachArg(c, func(arg Arg, ctx *ArgCtx) {
   107  		if !doMore {
   108  			ctx.Stop = true
   109  			return
   110  		}
   111  		generateHints(comps, arg, execValidate)
   112  	})
   113  }
   114  
   115  func generateHints(compMap CompMap, arg Arg, exec func() bool) {
   116  	typ := arg.Type()
   117  	if typ == nil || arg.Dir() == DirOut {
   118  		return
   119  	}
   120  	switch t := typ.(type) {
   121  	case *ProcType:
   122  		// Random proc will not pass validation.
   123  		// We can mutate it, but only if the resulting value is within the legal range.
   124  		return
   125  	case *ConstType:
   126  		if IsPad(typ) {
   127  			return
   128  		}
   129  	case *CsumType:
   130  		// Csum will not pass validation and is always computed.
   131  		return
   132  	case *BufferType:
   133  		switch t.Kind {
   134  		case BufferFilename:
   135  			// This can generate escaping paths and is probably not too useful anyway.
   136  			return
   137  		case BufferString, BufferGlob:
   138  			if len(t.Values) != 0 {
   139  				// These are frequently file names or complete enumerations.
   140  				// Mutating these may be useful iff we intercept strcmp
   141  				// (and filter out file names).
   142  				return
   143  			}
   144  		}
   145  	}
   146  
   147  	switch a := arg.(type) {
   148  	case *ConstArg:
   149  		checkConstArg(a, compMap, exec)
   150  	case *DataArg:
   151  		if typ.(*BufferType).Kind == BufferCompressed {
   152  			checkCompressedArg(a, compMap, exec)
   153  		} else {
   154  			checkDataArg(a, compMap, exec)
   155  		}
   156  	}
   157  }
   158  
   159  func checkConstArg(arg *ConstArg, compMap CompMap, exec func() bool) {
   160  	original := arg.Val
   161  	// Note: because shrinkExpand returns a map, order of programs is non-deterministic.
   162  	// This can affect test coverage reports.
   163  	for _, replacer := range shrinkExpand(original, compMap, arg.Type().TypeBitSize(), false) {
   164  		arg.Val = replacer
   165  		if !exec() {
   166  			break
   167  		}
   168  	}
   169  	arg.Val = original
   170  }
   171  
   172  func checkDataArg(arg *DataArg, compMap CompMap, exec func() bool) {
   173  	bytes := make([]byte, 8)
   174  	data := arg.Data()
   175  	size := len(data)
   176  	if size > maxDataLength {
   177  		size = maxDataLength
   178  	}
   179  	for i := 0; i < size; i++ {
   180  		original := make([]byte, 8)
   181  		copy(original, data[i:])
   182  		val := binary.LittleEndian.Uint64(original)
   183  		for _, replacer := range shrinkExpand(val, compMap, 64, false) {
   184  			binary.LittleEndian.PutUint64(bytes, replacer)
   185  			copy(data[i:], bytes)
   186  			if !exec() {
   187  				break
   188  			}
   189  		}
   190  		copy(data[i:], original)
   191  	}
   192  }
   193  
   194  func checkCompressedArg(arg *DataArg, compMap CompMap, exec func() bool) {
   195  	data0 := arg.Data()
   196  	data, dtor := image.MustDecompress(data0)
   197  	defer dtor()
   198  	// Images are very large so the generic algorithm for data arguments
   199  	// can produce too many mutants. For images we consider only
   200  	// 4/8-byte aligned ints. This is enough to handle all magic
   201  	// numbers and checksums. We also ignore 0 and ^uint64(0) source bytes,
   202  	// because there are too many of these in lots of images.
   203  	bytes := make([]byte, 8)
   204  	for i := 0; i < len(data); i += 4 {
   205  		original := make([]byte, 8)
   206  		copy(original, data[i:])
   207  		val := binary.LittleEndian.Uint64(original)
   208  		for _, replacer := range shrinkExpand(val, compMap, 64, true) {
   209  			binary.LittleEndian.PutUint64(bytes, replacer)
   210  			copy(data[i:], bytes)
   211  			arg.SetData(image.Compress(data))
   212  			if !exec() {
   213  				break
   214  			}
   215  		}
   216  		copy(data[i:], original)
   217  	}
   218  	arg.SetData(data0)
   219  }
   220  
   221  // Shrink and expand mutations model the cases when the syscall arguments
   222  // are casted to narrower (and wider) integer types.
   223  //
   224  // Motivation for shrink:
   225  //
   226  //	void f(u16 x) {
   227  //			u8 y = (u8)x;
   228  //			if (y == 0xab) {...}
   229  //	}
   230  //
   231  // If we call f(0x1234), then we'll see a comparison 0x34 vs 0xab and we'll
   232  // be unable to match the argument 0x1234 with any of the comparison operands.
   233  // Thus we shrink 0x1234 to 0x34 and try to match 0x34.
   234  // If there's a match for the shrank value, then we replace the corresponding
   235  // bytes of the input (in the given example we'll get 0x12ab).
   236  // Sometimes the other comparison operand will be wider than the shrank value
   237  // (in the example above consider comparison if (y == 0xdeadbeef) {...}).
   238  // In this case we ignore such comparison because we couldn't come up with
   239  // any valid code example that does similar things. To avoid such comparisons
   240  // we check the sizes with leastSize().
   241  //
   242  // Motivation for expand:
   243  //
   244  //	void f(i8 x) {
   245  //			i16 y = (i16)x;
   246  //			if (y == -2) {...}
   247  //	}
   248  //
   249  // Suppose we call f(-1), then we'll see a comparison 0xffff vs 0xfffe and be
   250  // unable to match input vs any operands. Thus we sign extend the input and
   251  // check the extension.
   252  // As with shrink we ignore cases when the other operand is wider.
   253  // Note that executor sign extends all the comparison operands to int64.
   254  func shrinkExpand(v uint64, compMap CompMap, bitsize uint64, image bool) []uint64 {
   255  	v = truncateToBitSize(v, bitsize)
   256  	limit := uint64(1<<bitsize - 1)
   257  	var replacers map[uint64]bool
   258  	for _, iwidth := range []int{8, 4, 2, 1, -4, -2, -1} {
   259  		var width int
   260  		var size, mutant uint64
   261  		if iwidth > 0 {
   262  			width = iwidth
   263  			size = uint64(width) * 8
   264  			mutant = v & ((1 << size) - 1)
   265  		} else {
   266  			width = -iwidth
   267  			size = uint64(width) * 8
   268  			if size > bitsize {
   269  				size = bitsize
   270  			}
   271  			if v&(1<<(size-1)) == 0 {
   272  				continue
   273  			}
   274  			mutant = v | ^((1 << size) - 1)
   275  		}
   276  		if image {
   277  			// For images we can produce too many mutants for small integers.
   278  			if width < 4 {
   279  				continue
   280  			}
   281  			if mutant == 0 || (mutant|^((1<<size)-1)) == ^uint64(0) {
   282  				continue
   283  			}
   284  		}
   285  		// Use big-endian match/replace for both blobs and ints.
   286  		// Sometimes we have unmarked blobs (no little/big-endian info);
   287  		// for ANYBLOBs we intentionally lose all marking;
   288  		// but even for marked ints we may need this too.
   289  		// Consider that kernel code does not convert the data
   290  		// (i.e. not ntohs(pkt->proto) == ETH_P_BATMAN),
   291  		// but instead converts the constant (i.e. pkt->proto == htons(ETH_P_BATMAN)).
   292  		// In such case we will see dynamic operand that does not match what we have in the program.
   293  		for _, bigendian := range []bool{false, true} {
   294  			if bigendian {
   295  				if width == 1 {
   296  					continue
   297  				}
   298  				mutant = swapInt(mutant, width)
   299  			}
   300  			for newV := range compMap[mutant] {
   301  				// Check the limit for negative numbers.
   302  				if newV > limit && ((^(limit >> 1) & newV) != ^(limit >> 1)) {
   303  					continue
   304  				}
   305  				mask := uint64(1<<size - 1)
   306  				newHi := newV & ^mask
   307  				newV = newV & mask
   308  				if newHi != 0 && newHi^^mask != 0 {
   309  					continue
   310  				}
   311  				if bigendian {
   312  					newV = swapInt(newV, width)
   313  				}
   314  				// We insert special ints (like 0) with high probability,
   315  				// so we don't try to replace to special ints them here.
   316  				// Images are large so it's hard to guess even special
   317  				// ints with random mutations.
   318  				if !image && specialIntsSet[newV] {
   319  					continue
   320  				}
   321  				// Replace size least significant bits of v with
   322  				// corresponding bits of newV. Leave the rest of v as it was.
   323  				replacer := (v &^ mask) | newV
   324  				if replacer == v {
   325  					continue
   326  				}
   327  				replacer = truncateToBitSize(replacer, bitsize)
   328  				// TODO(dvyukov): should we try replacing with arg+/-1?
   329  				// This could trigger some off-by-ones.
   330  				if replacers == nil {
   331  					replacers = make(map[uint64]bool)
   332  				}
   333  				replacers[replacer] = true
   334  			}
   335  		}
   336  	}
   337  	if replacers == nil {
   338  		return nil
   339  	}
   340  	res := make([]uint64, 0, len(replacers))
   341  	for v := range replacers {
   342  		res = append(res, v)
   343  	}
   344  	sort.Slice(res, func(i, j int) bool {
   345  		return res[i] < res[j]
   346  	})
   347  	return res
   348  }
   349  
   350  func init() {
   351  	specialIntsSet = make(map[uint64]bool)
   352  	for _, v := range specialInts {
   353  		specialIntsSet[v] = true
   354  	}
   355  }