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 }