mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-24 11:59:58 +00:00
29b0fd90e6
Currently getting a complete report requires a complex, multi-step dance (including getting information that external users are not interested in -- guilty file). Simplify interface down to 2 functions: Parse and Symbolize. Parse does what it did before, Symbolize symbolizes report and fills in maintainers. This simplifies both implementations of Reporter interface and all users of the interface. Potentially we could get this down to 1 function Parse that does everything. However, (1) Symbolize can fail, while Parse cannot, (2) usually we want to ignore (log) Symbolize errors, but otherwise proceed with the report, (3) repro does not need symbolization for all but the last report.
848 lines
21 KiB
Go
848 lines
21 KiB
Go
// Copyright 2016 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 repro
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/pkg/csource"
|
|
. "github.com/google/syzkaller/pkg/log"
|
|
"github.com/google/syzkaller/pkg/osutil"
|
|
"github.com/google/syzkaller/pkg/report"
|
|
"github.com/google/syzkaller/prog"
|
|
"github.com/google/syzkaller/syz-manager/mgrconfig"
|
|
"github.com/google/syzkaller/vm"
|
|
)
|
|
|
|
type Stats struct {
|
|
Log []byte
|
|
ExtractProgTime time.Duration
|
|
MinimizeProgTime time.Duration
|
|
SimplifyProgTime time.Duration
|
|
ExtractCTime time.Duration
|
|
SimplifyCTime time.Duration
|
|
}
|
|
|
|
type Result struct {
|
|
Prog *prog.Prog
|
|
Duration time.Duration
|
|
Opts csource.Options
|
|
CRepro bool
|
|
Stats Stats
|
|
// Information about the final (non-symbolized) crash that we reproduced.
|
|
// Can be different from what we started reproducing.
|
|
Report *report.Report
|
|
}
|
|
|
|
type context struct {
|
|
cfg *mgrconfig.Config
|
|
reporter report.Reporter
|
|
crashTitle string
|
|
instances chan *instance
|
|
bootRequests chan int
|
|
stats Stats
|
|
report *report.Report
|
|
}
|
|
|
|
type instance struct {
|
|
*vm.Instance
|
|
index int
|
|
execprogBin string
|
|
executorBin string
|
|
}
|
|
|
|
func Run(crashLog []byte, cfg *mgrconfig.Config, reporter report.Reporter, vmPool *vm.Pool,
|
|
vmIndexes []int) (*Result, error) {
|
|
if len(vmIndexes) == 0 {
|
|
return nil, fmt.Errorf("no VMs provided")
|
|
}
|
|
target, err := prog.GetTarget(cfg.TargetOS, cfg.TargetArch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries := target.ParseLog(crashLog)
|
|
if len(entries) == 0 {
|
|
return nil, fmt.Errorf("crash log does not contain any programs")
|
|
}
|
|
crashStart := len(crashLog) // assuming VM hanged
|
|
crashTitle := "hang"
|
|
if rep := reporter.Parse(crashLog); rep != nil {
|
|
crashStart = rep.StartPos
|
|
crashTitle = rep.Title
|
|
}
|
|
|
|
ctx := &context{
|
|
cfg: cfg,
|
|
reporter: reporter,
|
|
crashTitle: crashTitle,
|
|
instances: make(chan *instance, len(vmIndexes)),
|
|
bootRequests: make(chan int, len(vmIndexes)),
|
|
}
|
|
ctx.reproLog(0, "%v programs, %v VMs", len(entries), len(vmIndexes))
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(vmIndexes))
|
|
for _, vmIndex := range vmIndexes {
|
|
ctx.bootRequests <- vmIndex
|
|
go func() {
|
|
defer wg.Done()
|
|
for vmIndex := range ctx.bootRequests {
|
|
var inst *instance
|
|
maxTry := 3
|
|
for try := 0; try < maxTry; try++ {
|
|
select {
|
|
case <-vm.Shutdown:
|
|
try = maxTry
|
|
continue
|
|
default:
|
|
}
|
|
vmInst, err := vmPool.Create(vmIndex)
|
|
if err != nil {
|
|
ctx.reproLog(0, "failed to create VM: %v", err)
|
|
time.Sleep(10 * time.Second)
|
|
continue
|
|
|
|
}
|
|
execprogBin, err := vmInst.Copy(cfg.SyzExecprogBin)
|
|
if err != nil {
|
|
ctx.reproLog(0, "failed to copy to VM: %v", err)
|
|
vmInst.Close()
|
|
time.Sleep(10 * time.Second)
|
|
continue
|
|
}
|
|
executorBin, err := vmInst.Copy(cfg.SyzExecutorBin)
|
|
if err != nil {
|
|
ctx.reproLog(0, "failed to copy to VM: %v", err)
|
|
vmInst.Close()
|
|
time.Sleep(10 * time.Second)
|
|
continue
|
|
}
|
|
inst = &instance{
|
|
Instance: vmInst,
|
|
index: vmIndex,
|
|
execprogBin: execprogBin,
|
|
executorBin: executorBin,
|
|
}
|
|
break
|
|
}
|
|
if inst == nil {
|
|
break
|
|
}
|
|
ctx.instances <- inst
|
|
}
|
|
}()
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(ctx.instances)
|
|
}()
|
|
|
|
res, err := ctx.repro(entries, crashStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res != nil {
|
|
ctx.reproLog(3, "repro crashed as (corrupted=%v):\n%s",
|
|
ctx.report.Corrupted, ctx.report.Report)
|
|
// Try to rerun the repro if the report is corrupted.
|
|
for attempts := 0; ctx.report.Corrupted && attempts < 3; attempts++ {
|
|
ctx.reproLog(3, "report is corrupted, running repro again")
|
|
if res.CRepro {
|
|
_, err = ctx.testCProg(res.Prog, res.Duration, res.Opts)
|
|
} else {
|
|
_, err = ctx.testProg(res.Prog, res.Duration, res.Opts)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
ctx.reproLog(3, "final repro crashed as (corrupted=%v):\n%s",
|
|
ctx.report.Corrupted, ctx.report.Report)
|
|
res.Report = ctx.report
|
|
res.Stats = ctx.stats
|
|
}
|
|
|
|
close(ctx.bootRequests)
|
|
for inst := range ctx.instances {
|
|
inst.Close()
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) {
|
|
// Cut programs that were executed after crash.
|
|
for i, ent := range entries {
|
|
if ent.Start > crashStart {
|
|
entries = entries[:i]
|
|
break
|
|
}
|
|
}
|
|
|
|
reproStart := time.Now()
|
|
defer func() {
|
|
ctx.reproLog(3, "reproducing took %s", time.Since(reproStart))
|
|
}()
|
|
|
|
res, err := ctx.extractProg(entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res == nil {
|
|
return nil, nil
|
|
}
|
|
defer func() {
|
|
if res != nil {
|
|
res.Opts.Repro = false
|
|
}
|
|
}()
|
|
res, err = ctx.minimizeProg(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try extracting C repro without simplifying options first.
|
|
res, err = ctx.extractC(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Simplify options and try extracting C repro.
|
|
if !res.CRepro {
|
|
res, err = ctx.simplifyProg(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Simplify C related options.
|
|
if res.CRepro {
|
|
res, err = ctx.simplifyC(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (ctx *context) extractProg(entries []*prog.LogEntry) (*Result, error) {
|
|
ctx.reproLog(2, "extracting reproducer from %v programs", len(entries))
|
|
start := time.Now()
|
|
defer func() {
|
|
ctx.stats.ExtractProgTime = time.Since(start)
|
|
}()
|
|
|
|
// Extract last program on every proc.
|
|
procs := make(map[int]int)
|
|
for i, ent := range entries {
|
|
procs[ent.Proc] = i
|
|
}
|
|
var indices []int
|
|
for _, idx := range procs {
|
|
indices = append(indices, idx)
|
|
}
|
|
sort.Ints(indices)
|
|
var lastEntries []*prog.LogEntry
|
|
for i := len(indices) - 1; i >= 0; i-- {
|
|
lastEntries = append(lastEntries, entries[indices[i]])
|
|
}
|
|
|
|
// The shortest duration is 10 seconds to detect simple crashes (i.e. no races and no hangs).
|
|
// The longest duration is 5 minutes to catch races and hangs. Note that this value must be larger
|
|
// than hang/no output detection duration in vm.MonitorExecution, which is currently set to 3 mins.
|
|
timeouts := []time.Duration{10 * time.Second, 1 * time.Minute, 5 * time.Minute}
|
|
|
|
for _, timeout := range timeouts {
|
|
// Execute each program separately to detect simple crashes caused by a single program.
|
|
// Programs are executed in reverse order, usually the last program is the guilty one.
|
|
res, err := ctx.extractProgSingle(reverseEntries(lastEntries), timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res != nil {
|
|
ctx.reproLog(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
|
|
return res, nil
|
|
}
|
|
|
|
// Don't try bisecting if there's only one entry.
|
|
if len(entries) == 1 {
|
|
continue
|
|
}
|
|
|
|
// Execute all programs and bisect the log to find multiple guilty programs.
|
|
res, err = ctx.extractProgBisect(reverseEntries(entries), timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res != nil {
|
|
ctx.reproLog(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
ctx.reproLog(0, "failed to extract reproducer")
|
|
return nil, nil
|
|
}
|
|
|
|
func (ctx *context) createDefaultOps() csource.Options {
|
|
opts := csource.Options{
|
|
Threaded: true,
|
|
Collide: true,
|
|
Repeat: true,
|
|
Procs: ctx.cfg.Procs,
|
|
Sandbox: ctx.cfg.Sandbox,
|
|
EnableTun: true,
|
|
UseTmpDir: true,
|
|
HandleSegv: true,
|
|
WaitRepeat: true,
|
|
Repro: true,
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func (ctx *context) extractProgSingle(entries []*prog.LogEntry, duration time.Duration) (*Result, error) {
|
|
ctx.reproLog(3, "single: executing %d programs separately with timeout %s", len(entries), duration)
|
|
|
|
opts := ctx.createDefaultOps()
|
|
|
|
for _, ent := range entries {
|
|
opts.Fault = ent.Fault
|
|
opts.FaultCall = ent.FaultCall
|
|
opts.FaultNth = ent.FaultNth
|
|
if opts.FaultCall < 0 || opts.FaultCall >= len(ent.P.Calls) {
|
|
opts.FaultCall = len(ent.P.Calls) - 1
|
|
}
|
|
crashed, err := ctx.testProg(ent.P, duration, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crashed {
|
|
res := &Result{
|
|
Prog: ent.P,
|
|
Duration: duration * 3 / 2,
|
|
Opts: opts,
|
|
}
|
|
ctx.reproLog(3, "single: successfully extracted reproducer")
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
ctx.reproLog(3, "single: failed to extract reproducer")
|
|
return nil, nil
|
|
}
|
|
|
|
func (ctx *context) extractProgBisect(entries []*prog.LogEntry, baseDuration time.Duration) (*Result, error) {
|
|
ctx.reproLog(3, "bisect: bisecting %d programs with base timeout %s", len(entries), baseDuration)
|
|
|
|
opts := ctx.createDefaultOps()
|
|
|
|
duration := func(entries int) time.Duration {
|
|
return baseDuration + time.Duration((entries/4))*time.Second
|
|
}
|
|
|
|
// Bisect the log to find multiple guilty programs.
|
|
entries, err := ctx.bisectProgs(entries, func(progs []*prog.LogEntry) (bool, error) {
|
|
return ctx.testProgs(progs, duration(len(progs)), opts)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(entries) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// TODO: Minimize each program before concatenation.
|
|
// TODO: Return multiple programs if concatenation fails.
|
|
|
|
ctx.reproLog(3, "bisect: %d programs left: \n\n%s\n", len(entries), encodeEntries(entries))
|
|
ctx.reproLog(3, "bisect: trying to concatenate")
|
|
|
|
// Concatenate all programs into one.
|
|
prog := &prog.Prog{
|
|
Target: entries[0].P.Target,
|
|
}
|
|
for _, entry := range entries {
|
|
prog.Calls = append(prog.Calls, entry.P.Calls...)
|
|
}
|
|
dur := duration(len(entries)) * 3 / 2
|
|
|
|
// Execute the program without fault injection.
|
|
crashed, err := ctx.testProg(prog, dur, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crashed {
|
|
res := &Result{
|
|
Prog: prog,
|
|
Duration: dur,
|
|
Opts: opts,
|
|
}
|
|
ctx.reproLog(3, "bisect: concatenation succeded")
|
|
return res, nil
|
|
}
|
|
|
|
// Try with fault injection.
|
|
calls := 0
|
|
for _, entry := range entries {
|
|
if entry.Fault {
|
|
opts.FaultCall = calls + entry.FaultCall
|
|
opts.FaultNth = entry.FaultNth
|
|
if entry.FaultCall < 0 || entry.FaultCall >= len(entry.P.Calls) {
|
|
opts.FaultCall = calls + len(entry.P.Calls) - 1
|
|
}
|
|
crashed, err := ctx.testProg(prog, dur, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crashed {
|
|
res := &Result{
|
|
Prog: prog,
|
|
Duration: dur,
|
|
Opts: opts,
|
|
}
|
|
ctx.reproLog(3, "bisect: concatenation succeeded with fault injection")
|
|
return res, nil
|
|
}
|
|
}
|
|
calls += len(entry.P.Calls)
|
|
}
|
|
|
|
ctx.reproLog(3, "bisect: concatenation failed")
|
|
return nil, nil
|
|
}
|
|
|
|
// Minimize calls and arguments.
|
|
func (ctx *context) minimizeProg(res *Result) (*Result, error) {
|
|
ctx.reproLog(2, "minimizing guilty program")
|
|
start := time.Now()
|
|
defer func() {
|
|
ctx.stats.MinimizeProgTime = time.Since(start)
|
|
}()
|
|
|
|
call := -1
|
|
if res.Opts.Fault {
|
|
call = res.Opts.FaultCall
|
|
}
|
|
res.Prog, res.Opts.FaultCall = prog.Minimize(res.Prog, call, func(p1 *prog.Prog, callIndex int) bool {
|
|
crashed, err := ctx.testProg(p1, res.Duration, res.Opts)
|
|
if err != nil {
|
|
ctx.reproLog(0, "minimization failed with %v", err)
|
|
return false
|
|
}
|
|
return crashed
|
|
}, true)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Simplify repro options (threaded, collide, sandbox, etc).
|
|
func (ctx *context) simplifyProg(res *Result) (*Result, error) {
|
|
ctx.reproLog(2, "simplifying guilty program")
|
|
start := time.Now()
|
|
defer func() {
|
|
ctx.stats.SimplifyProgTime = time.Since(start)
|
|
}()
|
|
|
|
for _, simplify := range progSimplifies {
|
|
opts := res.Opts
|
|
if simplify(&opts) {
|
|
crashed, err := ctx.testProg(res.Prog, res.Duration, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crashed {
|
|
res.Opts = opts
|
|
// Simplification successfull, try extracting C repro.
|
|
res, err := ctx.extractC(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.CRepro {
|
|
return res, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Try triggering crash with a C reproducer.
|
|
func (ctx *context) extractC(res *Result) (*Result, error) {
|
|
ctx.reproLog(2, "extracting C reproducer")
|
|
start := time.Now()
|
|
defer func() {
|
|
ctx.stats.ExtractCTime = time.Since(start)
|
|
}()
|
|
|
|
crashed, err := ctx.testCProg(res.Prog, res.Duration, res.Opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.CRepro = crashed
|
|
return res, nil
|
|
}
|
|
|
|
// Try to simplify the C reproducer.
|
|
func (ctx *context) simplifyC(res *Result) (*Result, error) {
|
|
ctx.reproLog(2, "simplifying C reproducer")
|
|
start := time.Now()
|
|
defer func() {
|
|
ctx.stats.SimplifyCTime = time.Since(start)
|
|
}()
|
|
|
|
for _, simplify := range cSimplifies {
|
|
opts := res.Opts
|
|
if simplify(&opts) {
|
|
crashed, err := ctx.testCProg(res.Prog, res.Duration, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crashed {
|
|
res.Opts = opts
|
|
}
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (ctx *context) testProg(p *prog.Prog, duration time.Duration, opts csource.Options) (crashed bool, err error) {
|
|
entry := prog.LogEntry{P: p}
|
|
if opts.Fault {
|
|
entry.Fault = true
|
|
entry.FaultCall = opts.FaultCall
|
|
entry.FaultNth = opts.FaultNth
|
|
}
|
|
return ctx.testProgs([]*prog.LogEntry{&entry}, duration, opts)
|
|
}
|
|
|
|
func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, opts csource.Options) (crashed bool, err error) {
|
|
inst := <-ctx.instances
|
|
if inst == nil {
|
|
return false, fmt.Errorf("all VMs failed to boot")
|
|
}
|
|
defer ctx.returnInstance(inst)
|
|
if len(entries) == 0 {
|
|
return false, fmt.Errorf("no programs to execute")
|
|
}
|
|
|
|
pstr := encodeEntries(entries)
|
|
progFile, err := osutil.WriteTempFile(pstr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer os.Remove(progFile)
|
|
vmProgFile, err := inst.Copy(progFile)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to copy to VM: %v", err)
|
|
}
|
|
|
|
repeat := 1
|
|
if opts.Repeat {
|
|
repeat = 0
|
|
}
|
|
if !opts.Fault {
|
|
opts.FaultCall = -1
|
|
}
|
|
program := entries[0].P.String()
|
|
if len(entries) > 1 {
|
|
program = "["
|
|
for i, entry := range entries {
|
|
program += fmt.Sprintf("%v", len(entry.P.Calls))
|
|
if i != len(entries)-1 {
|
|
program += ", "
|
|
}
|
|
}
|
|
program += "]"
|
|
}
|
|
command := fmt.Sprintf("%v -executor %v -arch=%v -cover=0 -procs=%v -repeat=%v"+
|
|
" -sandbox %v -threaded=%v -collide=%v %v",
|
|
inst.execprogBin, inst.executorBin, ctx.cfg.TargetArch, opts.Procs, repeat,
|
|
opts.Sandbox, opts.Threaded, opts.Collide, vmProgFile)
|
|
ctx.reproLog(2, "testing program (duration=%v, %+v): %s", duration, opts, program)
|
|
return ctx.testImpl(inst.Instance, command, duration)
|
|
}
|
|
|
|
func (ctx *context) testCProg(p *prog.Prog, duration time.Duration, opts csource.Options) (crashed bool, err error) {
|
|
src, err := csource.Write(p, opts)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
srcf, err := osutil.WriteTempFile(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
bin, err := csource.Build(p.Target, "c", srcf)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer os.Remove(bin)
|
|
ctx.reproLog(2, "testing compiled C program (duration=%v, %+v): %s", duration, opts, p)
|
|
crashed, err = ctx.testBin(bin, duration)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return crashed, nil
|
|
}
|
|
|
|
func (ctx *context) testBin(bin string, duration time.Duration) (crashed bool, err error) {
|
|
inst := <-ctx.instances
|
|
if inst == nil {
|
|
return false, fmt.Errorf("all VMs failed to boot")
|
|
}
|
|
defer ctx.returnInstance(inst)
|
|
|
|
bin, err = inst.Copy(bin)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to copy to VM: %v", err)
|
|
}
|
|
return ctx.testImpl(inst.Instance, bin, duration)
|
|
}
|
|
|
|
func (ctx *context) testImpl(inst *vm.Instance, command string, duration time.Duration) (crashed bool, err error) {
|
|
outc, errc, err := inst.Run(duration, nil, command)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to run command in VM: %v", err)
|
|
}
|
|
rep := vm.MonitorExecution(outc, errc, ctx.reporter, true)
|
|
if rep == nil {
|
|
ctx.reproLog(2, "program did not crash")
|
|
return false, nil
|
|
}
|
|
ctx.report = rep
|
|
ctx.reproLog(2, "program crashed: %v", rep.Title)
|
|
return true, nil
|
|
}
|
|
|
|
func (ctx *context) returnInstance(inst *instance) {
|
|
ctx.bootRequests <- inst.index
|
|
inst.Close()
|
|
}
|
|
|
|
func (ctx *context) reproLog(level int, format string, args ...interface{}) {
|
|
prefix := fmt.Sprintf("reproducing crash '%v': ", ctx.crashTitle)
|
|
Logf(level, prefix+format, args...)
|
|
ctx.stats.Log = append(ctx.stats.Log, []byte(fmt.Sprintf(format, args...)+"\n")...)
|
|
}
|
|
|
|
func (ctx *context) bisectProgs(progs []*prog.LogEntry, pred func([]*prog.LogEntry) (bool, error)) ([]*prog.LogEntry, error) {
|
|
ctx.reproLog(3, "bisect: bisecting %d programs", len(progs))
|
|
|
|
compose := func(guilty1, guilty2 [][]*prog.LogEntry, chunk []*prog.LogEntry) []*prog.LogEntry {
|
|
progs := []*prog.LogEntry{}
|
|
for _, c := range guilty1 {
|
|
progs = append(progs, c...)
|
|
}
|
|
progs = append(progs, chunk...)
|
|
for _, c := range guilty2 {
|
|
progs = append(progs, c...)
|
|
}
|
|
return progs
|
|
}
|
|
|
|
logGuilty := func(guilty [][]*prog.LogEntry) string {
|
|
log := "["
|
|
for i, chunk := range guilty {
|
|
log += fmt.Sprintf("<%d>", len(chunk))
|
|
if i != len(guilty)-1 {
|
|
log += ", "
|
|
}
|
|
}
|
|
log += "]"
|
|
return log
|
|
}
|
|
|
|
ctx.reproLog(3, "bisect: executing all %d programs", len(progs))
|
|
crashed, err := pred(progs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !crashed {
|
|
ctx.reproLog(3, "bisect: didn't crash")
|
|
return nil, nil
|
|
}
|
|
|
|
guilty := [][]*prog.LogEntry{progs}
|
|
again:
|
|
ctx.reproLog(3, "bisect: guilty chunks: %v", logGuilty(guilty))
|
|
for i, chunk := range guilty {
|
|
if len(chunk) == 1 {
|
|
continue
|
|
}
|
|
|
|
guilty1 := guilty[:i]
|
|
guilty2 := guilty[i+1:]
|
|
ctx.reproLog(3, "bisect: guilty chunks split: %v, <%v>, %v", logGuilty(guilty1), len(chunk), logGuilty(guilty2))
|
|
|
|
chunk1 := chunk[0 : len(chunk)/2]
|
|
chunk2 := chunk[len(chunk)/2 : len(chunk)]
|
|
ctx.reproLog(3, "bisect: chunk split: <%v> => <%v>, <%v>", len(chunk), len(chunk1), len(chunk2))
|
|
|
|
ctx.reproLog(3, "bisect: triggering crash without chunk #1")
|
|
progs := compose(guilty1, guilty2, chunk2)
|
|
crashed, err := pred(progs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if crashed {
|
|
guilty = nil
|
|
guilty = append(guilty, guilty1...)
|
|
guilty = append(guilty, chunk2)
|
|
guilty = append(guilty, guilty2...)
|
|
ctx.reproLog(3, "bisect: crashed, chunk #1 evicted")
|
|
goto again
|
|
}
|
|
|
|
ctx.reproLog(3, "bisect: triggering crash without chunk #2")
|
|
progs = compose(guilty1, guilty2, chunk1)
|
|
crashed, err = pred(progs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if crashed {
|
|
guilty = nil
|
|
guilty = append(guilty, guilty1...)
|
|
guilty = append(guilty, chunk1)
|
|
guilty = append(guilty, guilty2...)
|
|
ctx.reproLog(3, "bisect: crashed, chunk #2 evicted")
|
|
goto again
|
|
}
|
|
|
|
guilty = nil
|
|
guilty = append(guilty, guilty1...)
|
|
guilty = append(guilty, chunk1)
|
|
guilty = append(guilty, chunk2)
|
|
guilty = append(guilty, guilty2...)
|
|
|
|
ctx.reproLog(3, "bisect: not crashed, both chunks required")
|
|
|
|
goto again
|
|
}
|
|
|
|
progs = nil
|
|
for _, chunk := range guilty {
|
|
if len(chunk) != 1 {
|
|
return nil, fmt.Errorf("bad bisect result: %v", guilty)
|
|
}
|
|
progs = append(progs, chunk[0])
|
|
}
|
|
|
|
ctx.reproLog(3, "bisect: success, %d programs left", len(progs))
|
|
return progs, nil
|
|
}
|
|
|
|
func reverseEntries(entries []*prog.LogEntry) []*prog.LogEntry {
|
|
last := len(entries) - 1
|
|
for i := 0; i < len(entries)/2; i++ {
|
|
entries[i], entries[last-i] = entries[last-i], entries[i]
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func encodeEntries(entries []*prog.LogEntry) []byte {
|
|
buf := new(bytes.Buffer)
|
|
for _, ent := range entries {
|
|
opts := ""
|
|
if ent.Fault {
|
|
opts = fmt.Sprintf(" (fault-call:%v fault-nth:%v)", ent.FaultCall, ent.FaultNth)
|
|
}
|
|
fmt.Fprintf(buf, "executing program %v%v:\n%v", ent.Proc, opts, string(ent.P.Serialize()))
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
type Simplify func(opts *csource.Options) bool
|
|
|
|
var progSimplifies = []Simplify{
|
|
func(opts *csource.Options) bool {
|
|
if !opts.Fault {
|
|
return false
|
|
}
|
|
opts.Fault = false
|
|
opts.FaultCall = 0
|
|
opts.FaultNth = 0
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.Collide {
|
|
return false
|
|
}
|
|
opts.Collide = false
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if opts.Collide || !opts.Threaded {
|
|
return false
|
|
}
|
|
opts.Threaded = false
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.Repeat {
|
|
return false
|
|
}
|
|
opts.Repeat = false
|
|
opts.Procs = 1
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if opts.Procs == 1 {
|
|
return false
|
|
}
|
|
opts.Procs = 1
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if opts.Sandbox == "none" {
|
|
return false
|
|
}
|
|
opts.Sandbox = "none"
|
|
return true
|
|
},
|
|
}
|
|
|
|
var cSimplifies = append(progSimplifies, []Simplify{
|
|
func(opts *csource.Options) bool {
|
|
if opts.Sandbox == "" {
|
|
return false
|
|
}
|
|
opts.Sandbox = ""
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.EnableTun {
|
|
return false
|
|
}
|
|
opts.EnableTun = false
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.UseTmpDir || opts.Sandbox == "namespace" {
|
|
return false
|
|
}
|
|
opts.UseTmpDir = false
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.HandleSegv {
|
|
return false
|
|
}
|
|
opts.HandleSegv = false
|
|
return true
|
|
},
|
|
func(opts *csource.Options) bool {
|
|
if !opts.WaitRepeat {
|
|
return false
|
|
}
|
|
opts.WaitRepeat = false
|
|
return true
|
|
},
|
|
}...)
|