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 ) 26 27 type uint64Set map[uint64]bool 28 29 // Example: for comparisons {(op1, op2), (op1, op3), (op1, op4), (op2, op1)} 30 // this map will store the following: 31 // m = { 32 // op1: {map[op2]: true, map[op3]: true, map[op4]: true}, 33 // op2: {map[op1]: true} 34 // }. 35 type CompMap map[uint64]uint64Set 36 37 const ( 38 maxDataLength = 100 39 ) 40 41 var specialIntsSet uint64Set 42 43 func (m CompMap) AddComp(arg1, arg2 uint64) { 44 if _, ok := m[arg1]; !ok { 45 m[arg1] = make(uint64Set) 46 } 47 m[arg1][arg2] = true 48 } 49 50 func (m CompMap) String() string { 51 buf := new(bytes.Buffer) 52 for v, comps := range m { 53 if len(buf.Bytes()) != 0 { 54 fmt.Fprintf(buf, ", ") 55 } 56 fmt.Fprintf(buf, "0x%x:", v) 57 for c := range comps { 58 fmt.Fprintf(buf, " 0x%x", c) 59 } 60 } 61 return buf.String() 62 } 63 64 // Mutates the program using the comparison operands stored in compMaps. 65 // For each of the mutants executes the exec callback. 66 func (p *Prog) MutateWithHints(callIndex int, comps CompMap, exec func(p *Prog)) { 67 p = p.Clone() 68 c := p.Calls[callIndex] 69 execValidate := func() { 70 p.Target.SanitizeCall(c) 71 p.debugValidate() 72 exec(p) 73 } 74 ForeachArg(c, func(arg Arg, _ *ArgCtx) { 75 generateHints(comps, arg, execValidate) 76 }) 77 } 78 79 func generateHints(compMap CompMap, arg Arg, exec func()) { 80 typ := arg.Type() 81 if typ == nil || typ.Dir() == DirOut { 82 return 83 } 84 switch t := typ.(type) { 85 case *ProcType: 86 // Random proc will not pass validation. 87 // We can mutate it, but only if the resulting value is within the legal range. 88 return 89 case *CsumType: 90 // Csum will not pass validation and is always computed. 91 return 92 case *BufferType: 93 if t.Kind == BufferFilename { 94 // This can generate escaping paths and is probably not too useful anyway. 95 return 96 } 97 } 98 99 switch a := arg.(type) { 100 case *ConstArg: 101 checkConstArg(a, compMap, exec) 102 case *DataArg: 103 checkDataArg(a, compMap, exec) 104 } 105 } 106 107 func checkConstArg(arg *ConstArg, compMap CompMap, exec func()) { 108 original := arg.Val 109 for replacer := range shrinkExpand(original, compMap) { 110 arg.Val = replacer 111 exec() 112 } 113 arg.Val = original 114 } 115 116 func checkDataArg(arg *DataArg, compMap CompMap, exec func()) { 117 bytes := make([]byte, 8) 118 data := arg.Data() 119 size := len(data) 120 if size > maxDataLength { 121 size = maxDataLength 122 } 123 for i := 0; i < size; i++ { 124 original := make([]byte, 8) 125 copy(original, data[i:]) 126 val := binary.LittleEndian.Uint64(original) 127 for replacer := range shrinkExpand(val, compMap) { 128 binary.LittleEndian.PutUint64(bytes, replacer) 129 copy(data[i:], bytes) 130 exec() 131 } 132 copy(data[i:], original) 133 } 134 } 135 136 // Shrink and expand mutations model the cases when the syscall arguments 137 // are casted to narrower (and wider) integer types. 138 // ====================================================================== 139 // Motivation for shrink: 140 // void f(u16 x) { 141 // u8 y = (u8)x; 142 // if (y == 0xab) {...} 143 // } 144 // If we call f(0x1234), then we'll see a comparison 0x34 vs 0xab and we'll 145 // be unable to match the argument 0x1234 with any of the comparison operands. 146 // Thus we shrink 0x1234 to 0x34 and try to match 0x34. 147 // If there's a match for the shrank value, then we replace the corresponding 148 // bytes of the input (in the given example we'll get 0x12ab). 149 // Sometimes the other comparison operand will be wider than the shrank value 150 // (in the example above consider comparison if (y == 0xdeadbeef) {...}). 151 // In this case we ignore such comparison because we couldn't come up with 152 // any valid code example that does similar things. To avoid such comparisons 153 // we check the sizes with leastSize(). 154 // ====================================================================== 155 // Motivation for expand: 156 // void f(i8 x) { 157 // i16 y = (i16)x; 158 // if (y == -2) {...} 159 // } 160 // Suppose we call f(-1), then we'll see a comparison 0xffff vs 0xfffe and be 161 // unable to match input vs any operands. Thus we sign extend the input and 162 // check the extension. 163 // As with shrink we ignore cases when the other operand is wider. 164 // Note that executor sign extends all the comparison operands to int64. 165 // ====================================================================== 166 func shrinkExpand(v uint64, compMap CompMap) (replacers uint64Set) { 167 for _, iwidth := range []int{8, 4, 2, 1, -4, -2, -1} { 168 var width int 169 var size, mutant uint64 170 if iwidth > 0 { 171 width = iwidth 172 size = uint64(width) * 8 173 mutant = v & ((1 << size) - 1) 174 } else { 175 width = -iwidth 176 size = uint64(width) * 8 177 mutant = v | ^((1 << size) - 1) 178 } 179 // Use big-endian match/replace for both blobs and ints. 180 // Sometimes we have unmarked blobs (no little/big-endian info); 181 // for ANYBLOBs we intentionally lose all marking; 182 // but even for marked ints we may need this too. 183 // Consider that kernel code does not convert the data 184 // (i.e. not ntohs(pkt->proto) == ETH_P_BATMAN), 185 // but instead converts the constant (i.e. pkt->proto == htons(ETH_P_BATMAN)). 186 // In such case we will see dynamic operand that does not match what we have in the program. 187 for _, bigendian := range []bool{false, true} { 188 if bigendian { 189 if width == 1 { 190 continue 191 } 192 mutant = swapInt(mutant, width) 193 } 194 for newV := range compMap[mutant] { 195 mask := uint64(1<<size - 1) 196 newHi := newV & ^mask 197 newV = newV & mask 198 if newHi != 0 && newHi^^mask != 0 { 199 continue 200 } 201 if bigendian { 202 newV = swapInt(newV, width) 203 } 204 if specialIntsSet[newV] { 205 continue 206 } 207 // Replace size least significant bits of v with 208 // corresponding bits of newV. Leave the rest of v as it was. 209 replacer := (v &^ mask) | newV 210 // TODO(dvyukov): should we try replacing with arg+/-1? 211 // This could trigger some off-by-ones. 212 if replacers == nil { 213 replacers = make(uint64Set) 214 } 215 replacers[replacer] = true 216 } 217 } 218 } 219 return 220 } 221 222 func init() { 223 specialIntsSet = make(uint64Set) 224 for _, v := range specialInts { 225 specialIntsSet[v] = true 226 } 227 } 228