syzkaller/prog/hints.go
Dmitry Vyukov b0fa969c09 prog: speedup and simplify hints code
Clone program only once.
Preallocate slices in clone.
Remove the clone full mode.
Always mutate args in place.
Allocate replacers map lazily.
Don't allocate res map at all (calculate valus on the go).
Remove sliceToUint64, pad.

benchmark          old ns/op     new ns/op     delta
BenchmarkHints     122100048     7466013       -93.89%
2017-12-08 12:27:39 +01:00

215 lines
6.3 KiB
Go

// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package prog
// A hint is basically a tuple consisting of a pointer to an argument
// in one of the syscalls of a program and a value, which should be
// assigned to that argument (we call it a replacer).
// A simplified version of hints workflow looks like this:
// 1. Fuzzer launches a program (we call it a hint seed) and collects all
// the comparisons' data for every syscall in the program.
// 2. Next it tries to match the obtained comparison operands' values
// vs. the input arguments' values.
// 3. For every such match the fuzzer mutates the program by
// replacing the pointed argument with the saved value.
// 4. If a valid program is obtained, then fuzzer launches it and
// checks if new coverage is obtained.
// For more insights on particular mutations please see prog/hints_test.go.
import (
"bytes"
"encoding/binary"
"fmt"
)
type uint64Set map[uint64]bool
// Example: for comparisons {(op1, op2), (op1, op3), (op1, op4), (op2, op1)}
// this map will store the following:
// m = {
// op1: {map[op2]: true, map[op3]: true, map[op4]: true},
// op2: {map[op1]: true}
// }.
type CompMap map[uint64]uint64Set
const (
maxDataLength = 100
)
var specialIntsSet uint64Set
func (m CompMap) AddComp(arg1, arg2 uint64) {
if _, ok := m[arg1]; !ok {
m[arg1] = make(uint64Set)
}
m[arg1][arg2] = true
}
func (m CompMap) String() string {
buf := new(bytes.Buffer)
for v, comps := range m {
if len(buf.Bytes()) != 0 {
fmt.Fprintf(buf, ", ")
}
fmt.Fprintf(buf, "0x%x:", v)
for c := range comps {
fmt.Fprintf(buf, " 0x%x", c)
}
}
return buf.String()
}
// Mutates the program using the comparison operands stored in compMaps.
// For each of the mutants executes the exec callback.
func (p *Prog) MutateWithHints(callIndex int, comps CompMap, exec func(p *Prog)) {
if p.Calls[callIndex].Meta == p.Target.MmapSyscall {
return
}
p = p.Clone()
c := p.Calls[callIndex]
execValidate := func() {
if debug {
if err := p.validate(); err != nil {
panic(fmt.Sprintf("invalid hints candidate: %v", err))
}
}
exec(p)
}
foreachArg(c, func(arg, _ Arg, _ *[]Arg) {
generateHints(p, comps, c, arg, execValidate)
})
}
func generateHints(p *Prog, compMap CompMap, c *Call, arg Arg, exec func()) {
if arg.Type().Dir() == DirOut {
return
}
switch arg.Type().(type) {
case *ProcType:
// Random proc will not pass validation.
// We can mutate it, but only if the resulting value is within the legal range.
return
case *CsumType:
// Csum will not pass validation and is always computed.
return
case *LenType:
// Mutating len type causes panics during mmap/mremap analysis:
// panic: address is out of bounds: page=7 len=34359738367 bound=4096
// We can mutate len theoretically, but we need to be careful.
return
}
switch a := arg.(type) {
case *ConstArg:
checkConstArg(a, compMap, exec)
case *DataArg:
checkDataArg(a, compMap, exec)
}
}
func checkConstArg(arg *ConstArg, compMap CompMap, exec func()) {
original := arg.Val
for replacer := range shrinkExpand(original, compMap) {
arg.Val = replacer
exec()
}
arg.Val = original
}
func checkDataArg(arg *DataArg, compMap CompMap, exec func()) {
bytes := make([]byte, 8)
for i := 0; i < min(len(arg.Data), maxDataLength); i++ {
original := make([]byte, 8)
copy(original, arg.Data[i:])
val := binary.LittleEndian.Uint64(original)
for replacer := range shrinkExpand(val, compMap) {
binary.LittleEndian.PutUint64(bytes, replacer)
copy(arg.Data[i:], bytes)
exec()
}
copy(arg.Data[i:], original)
}
}
// Shrink and expand mutations model the cases when the syscall arguments
// are casted to narrower (and wider) integer types.
// ======================================================================
// Motivation for shrink:
// void f(u16 x) {
// u8 y = (u8)x;
// if (y == 0xab) {...}
// }
// If we call f(0x1234), then we'll see a comparison 0x34 vs 0xab and we'll
// be unable to match the argument 0x1234 with any of the comparison operands.
// Thus we shrink 0x1234 to 0x34 and try to match 0x34.
// If there's a match for the shrank value, then we replace the corresponding
// bytes of the input (in the given example we'll get 0x12ab).
// Sometimes the other comparison operand will be wider than the shrank value
// (in the example above consider comparison if (y == 0xdeadbeef) {...}).
// In this case we ignore such comparison because we couldn't come up with
// any valid code example that does similar things. To avoid such comparisons
// we check the sizes with leastSize().
// ======================================================================
// Motivation for expand:
// void f(i8 x) {
// i16 y = (i16)x;
// if (y == -2) {...}
// }
// Suppose we call f(-1), then we'll see a comparison 0xffff vs 0xfffe and be
// unable to match input vs any operands. Thus we sign extend the input and
// check the extension.
// As with shrink we ignore cases when the other operand is wider.
// Note that executor sign extends all the comparison operands to int64.
// ======================================================================
func shrinkExpand(v uint64, compMap CompMap) (replacers uint64Set) {
var prev uint64
for _, isize := range []int{64, 32, 16, 8, -32, -16, -8} {
var mutant uint64
var size uint
if isize > 0 {
size = uint(isize)
mutant = v & ((1 << size) - 1)
} else {
size = uint(-isize)
mutant = v | ^((1 << size) - 1)
}
if size != 64 && prev == mutant {
continue
}
prev = mutant
for newV := range compMap[mutant] {
mask := uint64(1<<size - 1)
if newHi := newV & ^mask; newHi == 0 || newHi^^mask == 0 {
if !specialIntsSet[newV&mask] {
// Replace size least significant bits of v with
// corresponding bits of newV. Leave the rest of v as it was.
replacer := (v &^ mask) | (newV & mask)
// TODO(dvyukov): should we try replacing with arg+/-1?
// This could trigger some off-by-ones.
if replacers == nil {
replacers = make(uint64Set)
}
replacers[replacer] = true
}
}
}
}
return
}
func init() {
specialIntsSet = make(uint64Set)
for _, v := range specialInts {
specialIntsSet[v] = true
}
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}