mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-23 19:39:40 +00:00
adddc5fd46
Non-determinism is bad: - it leads to flaky coverage reports - it makes test failures non-reproducible Remove 4 sources of non-determinism related to maps: - file name generation - string generation - resource generation - hints generation All a test that ensures all main operations are fully deterministic.
240 lines
7.1 KiB
Go
240 lines
7.1 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"
|
|
"sort"
|
|
)
|
|
|
|
// 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]map[uint64]bool
|
|
|
|
const (
|
|
maxDataLength = 100
|
|
)
|
|
|
|
var specialIntsSet map[uint64]bool
|
|
|
|
func (m CompMap) AddComp(arg1, arg2 uint64) {
|
|
if _, ok := m[arg1]; !ok {
|
|
m[arg1] = make(map[uint64]bool)
|
|
}
|
|
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)) {
|
|
p = p.Clone()
|
|
c := p.Calls[callIndex]
|
|
execValidate := func() {
|
|
p.Target.SanitizeCall(c)
|
|
p.debugValidate()
|
|
exec(p)
|
|
}
|
|
ForeachArg(c, func(arg Arg, _ *ArgCtx) {
|
|
generateHints(comps, arg, execValidate)
|
|
})
|
|
}
|
|
|
|
func generateHints(compMap CompMap, arg Arg, exec func()) {
|
|
typ := arg.Type()
|
|
if typ == nil || typ.Dir() == DirOut {
|
|
return
|
|
}
|
|
switch t := typ.(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 *BufferType:
|
|
if t.Kind == BufferFilename {
|
|
// This can generate escaping paths and is probably not too useful anyway.
|
|
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
|
|
// Note: because shrinkExpand returns a map, order of programs is non-deterministic.
|
|
// This can affect test coverage reports.
|
|
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)
|
|
data := arg.Data()
|
|
size := len(data)
|
|
if size > maxDataLength {
|
|
size = maxDataLength
|
|
}
|
|
for i := 0; i < size; i++ {
|
|
original := make([]byte, 8)
|
|
copy(original, data[i:])
|
|
val := binary.LittleEndian.Uint64(original)
|
|
for _, replacer := range shrinkExpand(val, compMap) {
|
|
binary.LittleEndian.PutUint64(bytes, replacer)
|
|
copy(data[i:], bytes)
|
|
exec()
|
|
}
|
|
copy(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) []uint64 {
|
|
var replacers map[uint64]bool
|
|
for _, iwidth := range []int{8, 4, 2, 1, -4, -2, -1} {
|
|
var width int
|
|
var size, mutant uint64
|
|
if iwidth > 0 {
|
|
width = iwidth
|
|
size = uint64(width) * 8
|
|
mutant = v & ((1 << size) - 1)
|
|
} else {
|
|
width = -iwidth
|
|
size = uint64(width) * 8
|
|
mutant = v | ^((1 << size) - 1)
|
|
}
|
|
// Use big-endian match/replace for both blobs and ints.
|
|
// Sometimes we have unmarked blobs (no little/big-endian info);
|
|
// for ANYBLOBs we intentionally lose all marking;
|
|
// but even for marked ints we may need this too.
|
|
// Consider that kernel code does not convert the data
|
|
// (i.e. not ntohs(pkt->proto) == ETH_P_BATMAN),
|
|
// but instead converts the constant (i.e. pkt->proto == htons(ETH_P_BATMAN)).
|
|
// In such case we will see dynamic operand that does not match what we have in the program.
|
|
for _, bigendian := range []bool{false, true} {
|
|
if bigendian {
|
|
if width == 1 {
|
|
continue
|
|
}
|
|
mutant = swapInt(mutant, width)
|
|
}
|
|
for newV := range compMap[mutant] {
|
|
mask := uint64(1<<size - 1)
|
|
newHi := newV & ^mask
|
|
newV = newV & mask
|
|
if newHi != 0 && newHi^^mask != 0 {
|
|
continue
|
|
}
|
|
if bigendian {
|
|
newV = swapInt(newV, width)
|
|
}
|
|
if specialIntsSet[newV] {
|
|
continue
|
|
}
|
|
// Replace size least significant bits of v with
|
|
// corresponding bits of newV. Leave the rest of v as it was.
|
|
replacer := (v &^ mask) | newV
|
|
// TODO(dvyukov): should we try replacing with arg+/-1?
|
|
// This could trigger some off-by-ones.
|
|
if replacers == nil {
|
|
replacers = make(map[uint64]bool)
|
|
}
|
|
replacers[replacer] = true
|
|
}
|
|
}
|
|
}
|
|
if replacers == nil {
|
|
return nil
|
|
}
|
|
res := make([]uint64, 0, len(replacers))
|
|
for v := range replacers {
|
|
res = append(res, v)
|
|
}
|
|
sort.Slice(res, func(i, j int) bool {
|
|
return res[i] < res[j]
|
|
})
|
|
return res
|
|
}
|
|
|
|
func init() {
|
|
specialIntsSet = make(map[uint64]bool)
|
|
for _, v := range specialInts {
|
|
specialIntsSet[v] = true
|
|
}
|
|
}
|