aboutsummaryrefslogtreecommitdiffstats
path: root/syz-verifier
diff options
context:
space:
mode:
authorTaras Madan <tarasmadan@google.com>2022-03-22 10:49:49 +0100
committerGitHub <noreply@github.com>2022-03-22 10:49:49 +0100
commitd88ef0c5c80d45a060e170c2706371f6b2957f55 (patch)
tree742b3ec54df49deb796b201cefc7b7ba6035045e /syz-verifier
parente2d91b1d0dd8c8b4760986ec8114469246022bb8 (diff)
syz-verifier: redesigned the analysis program generation and analysis flow (#2908)
Program verification logic is located in one function now. VMs fetch programs from priority queues, not from the generator. VMs operate the tasks, not programs now. For the crashed VM - return error for every program in the queue *fixed some road errors
Diffstat (limited to 'syz-verifier')
-rw-r--r--syz-verifier/execresult.go66
-rw-r--r--syz-verifier/execresult_test.go62
-rw-r--r--syz-verifier/exectask.go141
-rwxr-xr-xsyz-verifier/main.go33
-rw-r--r--syz-verifier/main_test.go552
-rw-r--r--syz-verifier/rpcserver.go171
-rw-r--r--syz-verifier/rpcserver_test.go32
-rw-r--r--syz-verifier/utils_test.go11
-rw-r--r--syz-verifier/verifier.go257
-rw-r--r--syz-verifier/verifier_test.go279
10 files changed, 802 insertions, 802 deletions
diff --git a/syz-verifier/execresult.go b/syz-verifier/execresult.go
index d3db18882..9db053a35 100644
--- a/syz-verifier/execresult.go
+++ b/syz-verifier/execresult.go
@@ -21,9 +21,34 @@ type ExecResult struct {
// in the generated programs.
Info ipc.ProgInfo
// Crashed is set to true if a crash occurred while executing the program.
+ // TODO: is not used properly. Crashes are just an errors now.
Crashed bool
+ // Source task ID is used to route result back to the caller.
+ ExecTaskID int64
+ // To signal the processing errors.
+ Error error
+}
+
+func (l *ExecResult) IsEqual(r *ExecResult) bool {
+ if l.Crashed || r.Crashed {
+ return false
+ }
+
+ lCalls := l.Info.Calls
+ rCalls := r.Info.Calls
+
+ if len(lCalls) != len(rCalls) {
+ return false
+ }
- RunIdx int
+ for i := 0; i < len(lCalls); i++ {
+ if lCalls[i].Errno != rCalls[i].Errno ||
+ lCalls[i].Flags != rCalls[i].Flags {
+ return false
+ }
+ }
+
+ return true
}
type ResultReport struct {
@@ -31,6 +56,8 @@ type ResultReport struct {
Prog string
// Reports contains information about each system call.
Reports []*CallReport
+ // Mismatch says whether the Reports differ.
+ Mismatch bool
}
type CallReport struct {
@@ -69,30 +96,10 @@ func (s ReturnState) String() string {
return state
}
-// VeifyRerun compares the results obtained from rerunning a program with what
-// was reported in the initial result report.
-func VerifyRerun(res []*ExecResult, rr *ResultReport) bool {
- for idx, cr := range rr.Reports {
- for _, r := range res {
- var state ReturnState
- if r.Crashed {
- state = ReturnState{Crashed: true}
- } else {
- ci := r.Info.Calls[idx]
- state = ReturnState{Errno: ci.Errno, Flags: ci.Flags}
- }
- if state != cr.States[r.Pool] {
- return false
- }
- }
- }
- return true
-}
-
-// Verify checks whether the Results of the same program, executed on different
-// kernels, are the same. If that's not the case, it returns a ResultReport,
-// highlighting the differences.
-func Verify(res []*ExecResult, prog *prog.Prog, s *Stats) *ResultReport {
+// CompareResults checks whether the ExecResult of the same program,
+// executed on different kernels, are the same.
+// It returns s ResultReport, highlighting the differences.
+func CompareResults(res []*ExecResult, prog *prog.Prog) *ResultReport {
rr := &ResultReport{
Prog: string(prog.Serialize()),
}
@@ -100,7 +107,6 @@ func Verify(res []*ExecResult, prog *prog.Prog, s *Stats) *ResultReport {
// Build the CallReport for each system call in the program.
for idx, call := range prog.Calls {
cn := call.Meta.Name
- s.Calls[cn].Occurrences++
cr := &CallReport{
Call: cn,
@@ -119,7 +125,6 @@ func Verify(res []*ExecResult, prog *prog.Prog, s *Stats) *ResultReport {
rr.Reports = append(rr.Reports, cr)
}
- var send bool
pool0 := res[0].Pool
for _, cr := range rr.Reports {
for _, state := range cr.States {
@@ -127,13 +132,10 @@ func Verify(res []*ExecResult, prog *prog.Prog, s *Stats) *ResultReport {
// the pools that executed the program are the same
if state0 := cr.States[pool0]; state0 != state {
cr.Mismatch = true
- send = true
+ rr.Mismatch = true
}
}
}
- if send {
- return rr
- }
- return nil
+ return rr
}
diff --git a/syz-verifier/execresult_test.go b/syz-verifier/execresult_test.go
index aae7bcdac..bddd91042 100644
--- a/syz-verifier/execresult_test.go
+++ b/syz-verifier/execresult_test.go
@@ -10,7 +10,62 @@ import (
"github.com/google/syzkaller/prog"
)
-func TestVerify(t *testing.T) {
+func TestIsEqual(t *testing.T) {
+ tests := []struct {
+ name string
+ res []*ExecResult
+ want bool
+ }{
+ {
+ name: "only crashes",
+ res: []*ExecResult{
+ makeExecResultCrashed(1),
+ makeExecResultCrashed(4),
+ },
+ want: false,
+ },
+ {
+ name: "mismatch because result and crash",
+ res: []*ExecResult{
+ makeExecResultCrashed(1),
+ makeExecResult(2, []int{11, 33, 22}, []int{1, 3, 3}...),
+ },
+ want: false,
+ },
+ {
+ name: "mismatches because of diffent length",
+ res: []*ExecResult{
+ makeExecResult(2, []int{11, 33}, []int{1, 3}...),
+ makeExecResult(4, []int{11, 33, 22}, []int{1, 3, 3}...)},
+ want: false,
+ },
+ {
+ name: "mismatches not found",
+ res: []*ExecResult{
+ makeExecResult(2, []int{11, 33, 22}, []int{1, 3, 3}...),
+ makeExecResult(4, []int{11, 33, 22}, []int{1, 3, 3}...)},
+ want: true,
+ },
+ {
+ name: "mismatches found in results",
+ res: []*ExecResult{
+ makeExecResult(1, []int{1, 3, 2}, []int{4, 7, 7}...),
+ makeExecResult(4, []int{1, 3, 5}, []int{4, 7, 3}...),
+ },
+ want: false,
+ }}
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got := test.res[0].IsEqual(test.res[1])
+ if diff := cmp.Diff(test.want, got); diff != "" {
+ t.Errorf("ExecResult.IsEqual failure (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestCompareResults(t *testing.T) {
p := "breaks_returns()\n" +
"minimize$0(0x1, 0x1)\n" +
"test$res0()\n"
@@ -54,6 +109,7 @@ func TestVerify(t *testing.T) {
4: returnState(22, 3)},
Mismatch: true},
},
+ Mismatch: true,
},
},
{
@@ -76,6 +132,7 @@ func TestVerify(t *testing.T) {
{Call: "minimize$0", States: map[int]ReturnState{1: {Errno: 3, Flags: 7}, 4: {Errno: 3, Flags: 7}}},
{Call: "test$res0", States: map[int]ReturnState{1: {Errno: 2, Flags: 7}, 4: {Errno: 5, Flags: 3}}, Mismatch: true},
},
+ Mismatch: true,
},
}}
@@ -86,8 +143,7 @@ func TestVerify(t *testing.T) {
if err != nil {
t.Fatalf("failed to deserialise test program: %v", err)
}
- stats := emptyTestStats()
- got := Verify(test.res, prog, stats)
+ got := CompareResults(test.res, prog)
if diff := cmp.Diff(test.wantReport, got); diff != "" {
t.Errorf("Verify report mismatch (-want +got):\n%s", diff)
}
diff --git a/syz-verifier/exectask.go b/syz-verifier/exectask.go
new file mode 100644
index 000000000..052469ed7
--- /dev/null
+++ b/syz-verifier/exectask.go
@@ -0,0 +1,141 @@
+// Copyright 2021 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 main
+
+import (
+ "container/heap"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/google/syzkaller/pkg/rpctype"
+ "github.com/google/syzkaller/prog"
+)
+
+type EnvDescr int64
+
+const (
+ AnyEnvironment EnvDescr = iota
+ NewEnvironment
+ // TODO: add CleanVMEnvironment support.
+
+ EnvironmentsCount
+)
+
+// ExecTask is the atomic analysis entity. Once executed, it could trigger the
+// pipeline propagation fof the program.
+type ExecTask struct {
+ CreationTime time.Time
+ Program *prog.Prog
+ ID int64
+ ExecResultChan ExecResultChan
+
+ priority int // The priority of the item in the queue.
+ // The index is needed by update and is maintained by the heap.Interface methods.
+ index int // The index of the item in the heap.
+}
+
+func (t *ExecTask) ToRPC() *rpctype.ExecTask {
+ return &rpctype.ExecTask{
+ Prog: t.Program.Serialize(),
+ ID: t.ID,
+ }
+}
+
+var (
+ ChanMapMutex = sync.Mutex{}
+ TaskIDToExecResultChan = map[int64]ExecResultChan{}
+ TaskCounter = int64(-1)
+)
+
+type ExecResultChan chan *ExecResult
+
+func MakeExecTask(prog *prog.Prog) *ExecTask {
+ task := &ExecTask{
+ CreationTime: time.Now(),
+ Program: prog,
+ ExecResultChan: make(ExecResultChan),
+ ID: atomic.AddInt64(&TaskCounter, 1),
+ }
+
+ ChanMapMutex.Lock()
+ defer ChanMapMutex.Unlock()
+ TaskIDToExecResultChan[task.ID] = task.ExecResultChan
+
+ return task
+}
+
+func DeleteExecTask(task *ExecTask) {
+ ChanMapMutex.Lock()
+ defer ChanMapMutex.Unlock()
+ delete(TaskIDToExecResultChan, task.ID)
+}
+
+func GetExecResultChan(taskID int64) ExecResultChan {
+ ChanMapMutex.Lock()
+ defer ChanMapMutex.Unlock()
+
+ return TaskIDToExecResultChan[taskID]
+}
+
+func MakeExecTaskQueue() *ExecTaskQueue {
+ return &ExecTaskQueue{
+ pq: make(ExecTaskPriorityQueue, 0),
+ }
+}
+
+// ExecTaskQueue respects the pq.priority. Internally it is a thread-safe PQ.
+type ExecTaskQueue struct {
+ pq ExecTaskPriorityQueue
+}
+
+// PopTask return false if no tasks are available.
+func (q *ExecTaskQueue) PopTask() (*ExecTask, bool) {
+ if q.pq.Len() == 0 {
+ return nil, false
+ }
+
+ return heap.Pop(&q.pq).(*ExecTask), true
+}
+
+func (q *ExecTaskQueue) PushTask(task *ExecTask) {
+ heap.Push(&q.pq, task)
+}
+
+func (q *ExecTaskQueue) Len() int {
+ return q.pq.Len()
+}
+
+// ExecTaskPriorityQueue reused example from https://pkg.go.dev/container/heap
+type ExecTaskPriorityQueue []*ExecTask
+
+func (pq ExecTaskPriorityQueue) Len() int { return len(pq) }
+
+func (pq ExecTaskPriorityQueue) Less(i, j int) bool {
+ // We want Pop to give us the highest, not lowest, priority so we use greater than here.
+ return pq[i].priority > pq[j].priority
+}
+
+func (pq ExecTaskPriorityQueue) Swap(i, j int) {
+ pq[i], pq[j] = pq[j], pq[i]
+ pq[i].index = i
+ pq[j].index = j
+}
+
+func (pq *ExecTaskPriorityQueue) Push(x interface{}) {
+ n := len(*pq)
+ item := x.(*ExecTask)
+ item.index = n
+ *pq = append(*pq, item)
+}
+
+func (pq *ExecTaskPriorityQueue) Pop() interface{} {
+ old := *pq
+ n := len(old)
+ item := old[n-1]
+ old[n-1] = nil // avoid memory leak
+ item.index = -1 // for safety
+ *pq = old[0 : n-1]
+ return item
+}
diff --git a/syz-verifier/main.go b/syz-verifier/main.go
index d17d01437..7ecbaa76b 100755
--- a/syz-verifier/main.go
+++ b/syz-verifier/main.go
@@ -8,11 +8,9 @@ package main
import (
"flag"
"io"
- "math/rand"
"os"
"path/filepath"
"strconv"
- "time"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/mgrconfig"
@@ -35,33 +33,11 @@ type poolInfo struct {
cfg *mgrconfig.Config
pool *vm.Pool
Reporter *report.Reporter
- // runners keeps track of what programs have been sent to each Runner.
- // There is one Runner executing per VM instance.
- runners map[int]runnerProgs
- // progs stores the programs that haven't been sent to this kernel yet but
- // have been sent to at least one other kernel.
- progs []*progInfo
- // toRerun stores the programs that still need to be rerun by this kernel.
- toRerun []*progInfo
// checked is set to true when the set of system calls not supported on the
// kernel is known.
checked bool
}
-type progInfo struct {
- prog *prog.Prog
- idx int
- serialized []byte
- res [][]*ExecResult
- // received stores the number of results received for this program.
- received int
-
- runIdx int
- report *ResultReport
-}
-
-type runnerProgs map[int]*progInfo
-
func main() {
var cfgs tool.CfgsFlag
flag.Var(&cfgs, "configs", "[MANDATORY] list of at least two kernel-specific comma-sepatated configuration files")
@@ -154,7 +130,6 @@ func main() {
if err != nil {
log.Fatalf("failed to create reporter for instance-%d: %v", idx, err)
}
- pi.runners = make(map[int]runnerProgs)
}
calls := make(map[*prog.Syscall]bool)
@@ -172,7 +147,6 @@ func main() {
target: target,
calls: calls,
reasons: make(map[*prog.Syscall]string),
- rnd: rand.New(rand.NewSource(time.Now().UnixNano() + 1e12)),
runnerBin: runnerBin,
executorBin: execBin,
addr: addr,
@@ -183,15 +157,14 @@ func main() {
reruns: *flagReruns,
}
- vrf.srv, err = startRPCServer(vrf)
- if err != nil {
- log.Fatalf("failed to initialise RPC server: %v", err)
- }
+ vrf.Init()
+ vrf.StartProgramsAnalysis()
vrf.startInstances()
monitor := MakeMonitor()
monitor.SetStatsTracking(vrf.stats)
+
// TODO: move binding address to configuration
log.Logf(0, "run the Monitor at http://127.0.0.1:8080/")
go monitor.ListenAndServe("127.0.0.1:8080")
diff --git a/syz-verifier/main_test.go b/syz-verifier/main_test.go
index 1dfefaa61..197daa285 100644
--- a/syz-verifier/main_test.go
+++ b/syz-verifier/main_test.go
@@ -1,555 +1,3 @@
// Copyright 2021 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 main
-
-import (
- "bytes"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "github.com/google/syzkaller/pkg/osutil"
- "github.com/google/syzkaller/pkg/rpctype"
- "github.com/google/syzkaller/prog"
-)
-
-func TestNewProgram(t *testing.T) {
- tests := []struct {
- name string
- pool, vm, retProgIdx, srvProgs int
- }{
- {
- name: "doesn't generate new program",
- pool: 1,
- vm: 1,
- retProgIdx: 3,
- srvProgs: 2,
- },
- {
- name: "generates new program",
- pool: 2,
- vm: 2,
- retProgIdx: 4,
- srvProgs: 3,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- srv := createTestServer(t)
- srv.pools = map[int]*poolInfo{
- 1: {
- runners: map[int]runnerProgs{
- 1: {1: {}},
- },
- progs: []*progInfo{{idx: 3}},
- },
- 2: {runners: map[int]runnerProgs{
- 2: {1: {}}},
- progs: []*progInfo{},
- },
- }
-
- srv.progs = map[int]*progInfo{
- 1: {idx: 1},
- 3: {idx: 3},
- }
-
- _, gotProgIdx, _ := srv.newProgram(test.pool, test.vm)
- if gotProgIdx != test.retProgIdx {
- t.Errorf("srv.newProgram returned idx: got %d, want %d", gotProgIdx, test.retProgIdx)
- }
-
- if got, want := len(srv.progs), test.srvProgs; got != want {
- t.Errorf("len(srv.progs): got %d, want %d", got, want)
- }
- })
- }
-}
-
-func TestNewResult(t *testing.T) {
- tests := []struct {
- name string
- idx int
- wantReady bool
- }{
- {
- name: "Results ready for verification",
- idx: 3,
- wantReady: true,
- },
- {
- name: "No results ready for verification",
- idx: 1,
- wantReady: false,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- srv := createTestServer(t)
- srv.pools = map[int]*poolInfo{0: {}, 1: {}}
- srv.progs = map[int]*progInfo{
- 1: {idx: 1,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = make([]*ExecResult, 2)
- return res
- }(),
- },
- 3: {idx: 3,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = make([]*ExecResult, 2)
- res[0][1] = &ExecResult{Pool: 1}
- return res
- }(),
- received: 1,
- },
- }
- gotReady := srv.newResult(&ExecResult{Pool: 0}, srv.progs[test.idx])
- if test.wantReady != gotReady {
- t.Errorf("srv.newResult: got %v want %v", gotReady, test.wantReady)
- }
- })
- }
-}
-
-func TestConnect(t *testing.T) {
- srv := createTestServer(t)
- srv.pools = map[int]*poolInfo{
- 1: {
- runners: map[int]runnerProgs{
- 0: {1: {idx: 1}},
- },
- progs: []*progInfo{{
- idx: 3}},
- }}
- a := &rpctype.RunnerConnectArgs{
- Pool: 1,
- VM: 1,
- }
- r := &rpctype.RunnerConnectRes{}
- if err := srv.Connect(a, r); err != nil {
- t.Fatalf("srv.Connect failed: %v", err)
- }
- if diff := cmp.Diff(&rpctype.RunnerConnectRes{CheckUnsupportedCalls: true}, r); diff != "" {
- t.Errorf("Connect result mismatch (-want +got):\n%s", diff)
- }
- want, got := map[int]runnerProgs{
- 0: {1: {idx: 1}},
- 1: {},
- }, srv.pools[a.Pool].runners
- if diff := cmp.Diff(want, got, cmp.AllowUnexported(progInfo{})); diff != "" {
- t.Errorf("srv.progs[a.Name] mismatch (-want +got):\n%s", diff)
- }
-}
-
-func TestFinalizeCallSet(t *testing.T) {
- target, err := prog.GetTarget("test", "64")
- if err != nil {
- t.Fatalf("failed to initialise test target: %v", err)
- }
-
- vrf := Verifier{
- target: target,
- reasons: map[*prog.Syscall]string{
- target.SyscallMap["test$res0"]: "foo",
- target.SyscallMap["minimize$0"]: "bar",
- },
- calls: map[*prog.Syscall]bool{
- target.SyscallMap["minimize$0"]: true,
- target.SyscallMap["test$res0"]: true,
- target.SyscallMap["disabled1"]: true,
- },
- reportReasons: true,
- }
-
- out := bytes.Buffer{}
- vrf.finalizeCallSet(&out)
- wantLines := []string{
- "The following calls have been disabled:\n",
- "\ttest$res0: foo\n",
- "\tminimize$0: bar\n",
- }
- output := out.String()
- for _, line := range wantLines {
- if !strings.Contains(output, line) {
- t.Errorf("finalizeCallSet: %q missing in reported output", line)
- }
- }
-
- wantCalls, gotCalls := map[*prog.Syscall]bool{
- target.SyscallMap["disabled1"]: true,
- }, vrf.calls
- if diff := cmp.Diff(wantCalls, gotCalls); diff != "" {
- t.Errorf("srv.calls mismatch (-want +got):\n%s", diff)
- }
-}
-
-func TestUpdateUnsupported(t *testing.T) {
- target, err := prog.GetTarget("test", "64")
- if err != nil {
- t.Fatalf("failed to initialise test target: %v", err)
- }
-
- tests := []struct {
- name string
- vrfPools map[int]*poolInfo
- wantPools map[int]*poolInfo
- wantCalls map[*prog.Syscall]bool
- wantNotChecked int
- nilCT bool
- }{
- {
- name: "choice table not generated",
- vrfPools: map[int]*poolInfo{0: {}, 1: {}},
- wantPools: map[int]*poolInfo{0: {checked: true}, 1: {}},
- wantNotChecked: 1,
- wantCalls: map[*prog.Syscall]bool{
- target.SyscallMap["minimize$0"]: true,
- target.SyscallMap["breaks_returns"]: true,
- target.SyscallMap["test$res0"]: true,
- target.SyscallMap["test$union0"]: true,
- },
- nilCT: true,
- },
- {
- name: "choice table generated",
- vrfPools: map[int]*poolInfo{0: {}},
- wantPools: map[int]*poolInfo{0: {checked: true}},
- wantNotChecked: 0,
- wantCalls: map[*prog.Syscall]bool{
- target.SyscallMap["minimize$0"]: true,
- target.SyscallMap["breaks_returns"]: true,
- },
- nilCT: false,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- vrf := Verifier{
- target: target,
- pools: test.vrfPools,
- reasons: make(map[*prog.Syscall]string),
- reportReasons: true,
- calls: map[*prog.Syscall]bool{
- target.SyscallMap["minimize$0"]: true,
- target.SyscallMap["breaks_returns"]: true,
- target.SyscallMap["test$res0"]: true,
- target.SyscallMap["test$union0"]: true,
- },
- stats: MakeStats(),
- }
- srv, err := startRPCServer(&vrf)
- if err != nil {
- t.Fatalf("failed to initialise RPC server: %v", err)
- }
-
- a := &rpctype.UpdateUnsupportedArgs{
- Pool: 0,
- UnsupportedCalls: []rpctype.SyscallReason{
- {ID: target.SyscallMap["test$res0"].ID, Reason: "foo"},
- {ID: target.SyscallMap["csource1"].ID, Reason: "bar"},
- {ID: target.SyscallMap["test$union0"].ID, Reason: "tar"},
- }}
- if err := srv.UpdateUnsupported(a, nil); err != nil {
- t.Fatalf("srv.UpdateUnsupported failed: %v", err)
- }
-
- if diff := cmp.Diff(test.wantPools, srv.pools, cmp.AllowUnexported(poolInfo{})); diff != "" {
- t.Errorf("srv.pools mismatch (-want +got):\n%s", diff)
- }
-
- wantReasons := map[*prog.Syscall]string{
- target.SyscallMap["test$res0"]: "foo",
- target.SyscallMap["test$union0"]: "tar",
- }
- if diff := cmp.Diff(wantReasons, vrf.reasons); diff != "" {
- t.Errorf("srv.reasons mismatch (-want +got):\n%s", diff)
- }
-
- if diff := cmp.Diff(test.wantCalls, vrf.calls); diff != "" {
- t.Errorf("srv.calls mismatch (-want +got):\n%s", diff)
- }
-
- if want, got := test.wantNotChecked, srv.notChecked; want != got {
- t.Errorf("srv.notChecked: got %d want %d", got, want)
- }
-
- if want, got := test.nilCT, vrf.choiceTable == nil; want != got {
- t.Errorf("vrf.choiceTable == nil: want nil, got: %v", srv.vrf.choiceTable)
- }
- })
- }
-}
-
-func TestUpdateUnsupportedNotCalledTwice(t *testing.T) {
- vrf := Verifier{
- pools: map[int]*poolInfo{
- 0: {runners: map[int]runnerProgs{0: nil, 1: nil}, checked: false},
- 1: {runners: map[int]runnerProgs{}, checked: false},
- },
- }
- srv, err := startRPCServer(&vrf)
- if err != nil {
- t.Fatalf("failed to initialise RPC server: %v", err)
- }
- a := &rpctype.UpdateUnsupportedArgs{Pool: 0}
-
- if err := srv.UpdateUnsupported(a, nil); err != nil {
- t.Fatalf("srv.UpdateUnsupported failed: %v", err)
- }
- if want, got := 1, srv.notChecked; want != got {
- t.Errorf("srv.notChecked: got %d want %d", got, want)
- }
-
- if err := srv.UpdateUnsupported(a, nil); err != nil {
- t.Fatalf("srv.UpdateUnsupported failed: %v", err)
- }
- if want, got := 1, srv.notChecked; want != got {
- t.Fatalf("srv.UpdateUnsupported called twice")
- }
-
- wantPools := map[int]*poolInfo{
- 0: {runners: map[int]runnerProgs{0: nil, 1: nil}, checked: true},
- 1: {runners: map[int]runnerProgs{}, checked: false},
- }
- if diff := cmp.Diff(wantPools, srv.pools, cmp.AllowUnexported(poolInfo{}, progInfo{})); diff != "" {
- t.Errorf("srv.pools mismatch (-want +got):\n%s", diff)
- }
-}
-
-func TestProcessResults(t *testing.T) {
- tests := []struct {
- name string
- res []*ExecResult
- prog string
- wantExist bool
- wantStats *Stats
- }{
- {
- name: "report written",
- res: []*ExecResult{
- makeExecResult(0, []int{1, 3, 2}),
- makeExecResult(1, []int{1, 3, 5}),
- },
- wantExist: true,
- wantStats: &Stats{
- TotalMismatches: 1,
- TotalProgs: 1,
- Calls: map[string]*CallStats{
- "breaks_returns": makeCallStats("breaks_returns", 1, 0, map[ReturnState]bool{}),
- "test$res0": makeCallStats("test$res0", 1, 1, map[ReturnState]bool{{Errno: 2}: true, {Errno: 5}: true}),
- "minimize$0": makeCallStats("minimize$0", 1, 0, map[ReturnState]bool{}),
- },
- },
- },
- {
- name: "no report written",
- res: []*ExecResult{
- makeExecResult(0, []int{11, 33, 22}),
- makeExecResult(1, []int{11, 33, 22}),
- },
- wantStats: &Stats{
- TotalProgs: 1,
- Calls: map[string]*CallStats{
- "breaks_returns": makeCallStats("breaks_returns", 1, 0, map[ReturnState]bool{}),
- "minimize$0": makeCallStats("minimize$0", 1, 0, map[ReturnState]bool{}),
- "test$res0": makeCallStats("test$res0", 1, 0, map[ReturnState]bool{}),
- },
- },
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- prog := getTestProgram(t)
- pi := &progInfo{
- prog: prog,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = test.res
- return res
- }()}
- vrf := Verifier{
- resultsdir: makeTestResultDirectory(t),
- stats: emptyTestStats(),
- }
- resultFile := filepath.Join(vrf.resultsdir, "result-0")
-
- vrf.processResults(pi)
-
- if diff := cmp.Diff(test.wantStats, vrf.stats); diff != "" {
- t.Errorf("vrf.stats mismatch (-want +got):\n%s", diff)
- }
-
- if got, want := osutil.IsExist(resultFile), test.wantExist; got != want {
- t.Errorf("osutil.IsExist report file: got %v want %v", got, want)
- }
- os.Remove(filepath.Join(vrf.resultsdir, "result-0"))
- })
- }
-}
-
-func TestCreateReport(t *testing.T) {
- rr := ResultReport{
- Prog: "breaks_returns()\n" +
- "minimize$0(0x1, 0x1)\n" +
- "test$res0()\n",
- Reports: []*CallReport{
- {Call: "breaks_returns", States: map[int]ReturnState{
- 0: returnState(1, 1),
- 1: returnState(1, 1),
- 2: returnState(1, 1)}},
- {Call: "minimize$0", States: map[int]ReturnState{
- 0: returnState(3, 3),
- 1: returnState(3, 3),
- 2: returnState(3, 3)}},
- {Call: "test$res0", States: map[int]ReturnState{
- 0: returnState(2, 7),
- 1: returnState(5, 3),
- 2: returnState(22, 1)},
- Mismatch: true},
- },
- }
- got := string(createReport(&rr, 3))
- want := "ERRNO mismatches found for program:\n\n" +
- "[=] breaks_returns()\n" +
- "\t↳ Pool: 0, Flags: 1, Errno: 1 (operation not permitted)\n" +
- "\t↳ Pool: 1, Flags: 1, Errno: 1 (operation not permitted)\n" +
- "\t↳ Pool: 2, Flags: 1, Errno: 1 (operation not permitted)\n\n" +
- "[=] minimize$0(0x1, 0x1)\n" +
- "\t↳ Pool: 0, Flags: 3, Errno: 3 (no such process)\n" +
- "\t↳ Pool: 1, Flags: 3, Errno: 3 (no such process)\n" +
- "\t↳ Pool: 2, Flags: 3, Errno: 3 (no such process)\n\n" +
- "[!] test$res0()\n" +
- "\t↳ Pool: 0, Flags: 7, Errno: 2 (no such file or directory)\n" +
- "\t↳ Pool: 1, Flags: 3, Errno: 5 (input/output error)\n" +
- "\t↳ Pool: 2, Flags: 1, Errno: 22 (invalid argument)\n\n"
- if diff := cmp.Diff(got, want); diff != "" {
- t.Errorf("createReport: (-want +got):\n%s", diff)
- }
-}
-
-func TestCleanup(t *testing.T) {
- prog := getTestProgram(t)
- tests := []struct {
- name string
- progs map[int]*progInfo
- wantProg *progInfo
- wantStats *Stats
- progExists bool
- fileExists bool
- }{
- {
- name: "results not ready for verification",
- progs: map[int]*progInfo{
- 4: {
- idx: 4,
- received: 0,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = make([]*ExecResult, 3)
- return res
- }(),
- }},
- wantProg: &progInfo{
- idx: 4,
- received: 1,
- res: [][]*ExecResult{{makeExecResultCrashed(0), nil, nil}},
- },
- wantStats: emptyTestStats(),
- fileExists: false,
- },
- {
- name: "results sent for verification, no report generated",
- progs: map[int]*progInfo{
- 4: {
- idx: 4,
- prog: prog,
- received: 2,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = make([]*ExecResult, 3)
- res[0][1] = makeExecResultCrashed(1)
- res[0][2] = makeExecResultCrashed(2)
- return res
- }(),
- }},
- wantStats: &Stats{
- TotalProgs: 1,
- Calls: map[string]*CallStats{
- "breaks_returns": makeCallStats("breaks_returns", 1, 0, map[ReturnState]bool{}),
- "minimize$0": makeCallStats("minimize$0", 1, 0, map[ReturnState]bool{}),
- "test$res0": makeCallStats("test$res0", 1, 0, map[ReturnState]bool{}),
- },
- },
- fileExists: false,
- },
- {
- name: "results sent for verification, report generation",
- progs: map[int]*progInfo{
- 4: {
- idx: 4,
- prog: prog,
- received: 2,
- res: func() [][]*ExecResult {
- res := make([][]*ExecResult, 1)
- res[0] = make([]*ExecResult, 3)
- res[0][1] = makeExecResult(1, []int{11, 33, 44})
- res[0][2] = makeExecResult(2, []int{11, 33, 22})
- return res
- }(),
- }},
- wantStats: &Stats{
- TotalMismatches: 3,
- TotalProgs: 1,
- MismatchingProgs: 1,
- Calls: map[string]*CallStats{
- "breaks_returns": makeCallStats("breaks_returns", 1, 1,
- map[ReturnState]bool{
- crashedReturnState(): true,
- returnState(11): true}),
- "minimize$0": makeCallStats("minimize$0", 1, 1,
- map[ReturnState]bool{
- crashedReturnState(): true,
- returnState(33): true}),
- "test$res0": makeCallStats("test$res0", 1, 1,
- map[ReturnState]bool{
- crashedReturnState(): true,
- returnState(22): true,
- returnState(44): true}),
- },
- },
- fileExists: true,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- srv := createTestServer(t)
- srv.progs = test.progs
- srv.pools = map[int]*poolInfo{
- 0: {runners: map[int]runnerProgs{
- 0: {4: srv.progs[4]}},
- }, 1: {}, 2: {}}
- resultFile := filepath.Join(srv.vrf.resultsdir, "result-0")
-
- srv.cleanup(0, 0)
-
- prog := srv.progs[4]
- if diff := cmp.Diff(test.wantProg, prog, cmp.AllowUnexported(progInfo{})); diff != "" {
- t.Errorf("srv.progs[4] mismatch (-want +got):\n%s", diff)
- }
-
- if diff := cmp.Diff(test.wantStats, srv.vrf.stats); diff != "" {
- t.Errorf("srv.vrf.stats mismatch (-want +got):\n%s", diff)
- }
-
- if got, want := osutil.IsExist(resultFile), test.fileExists; got != want {
- t.Errorf("osutil.IsExist report file: got %v want %v", got, want)
- }
- os.Remove(filepath.Join(srv.vrf.resultsdir, "result-0"))
- })
- }
-}
diff --git a/syz-verifier/rpcserver.go b/syz-verifier/rpcserver.go
index e392e1c9e..3726799c1 100644
--- a/syz-verifier/rpcserver.go
+++ b/syz-verifier/rpcserver.go
@@ -4,6 +4,7 @@
package main
import (
+ "errors"
"net"
"os"
"sync"
@@ -15,25 +16,22 @@ import (
// RPCServer is a wrapper around the rpc.Server. It communicates with Runners,
// generates programs and sends complete Results for verification.
type RPCServer struct {
- vrf *Verifier
- port int
- mu sync.Mutex
- cond *sync.Cond
- pools map[int]*poolInfo
- progs map[int]*progInfo
- notChecked int
- rerunsAvailable *sync.Cond
+ vrf *Verifier
+ port int
+
+ // protects next variables
+ mu sync.Mutex
+ // used to count the pools w/o UnsupportedCalls result
+ notChecked int
+ // vmTasks store the per-VM currently assigned tasks Ids
+ vmTasksInProgress map[int]map[int64]bool
}
func startRPCServer(vrf *Verifier) (*RPCServer, error) {
srv := &RPCServer{
vrf: vrf,
- pools: vrf.pools,
- progs: make(map[int]*progInfo),
notChecked: len(vrf.pools),
}
- srv.cond = sync.NewCond(&srv.mu)
- srv.rerunsAvailable = sync.NewCond(&srv.mu)
s, err := rpctype.NewRPCServer(vrf.addr, "Verifier", srv)
if err != nil {
@@ -49,11 +47,7 @@ func startRPCServer(vrf *Verifier) (*RPCServer, error) {
// Connect notifies the RPCServer that a new Runner was started.
func (srv *RPCServer) Connect(a *rpctype.RunnerConnectArgs, r *rpctype.RunnerConnectRes) error {
- srv.mu.Lock()
- defer srv.mu.Unlock()
- pool, vm := a.Pool, a.VM
- srv.pools[pool].runners[vm] = make(runnerProgs)
- r.CheckUnsupportedCalls = !srv.pools[pool].checked
+ r.CheckUnsupportedCalls = !srv.vrf.pools[a.Pool].checked
return nil
}
@@ -66,10 +60,11 @@ func (srv *RPCServer) Connect(a *rpctype.RunnerConnectArgs, r *rpctype.RunnerCon
func (srv *RPCServer) UpdateUnsupported(a *rpctype.UpdateUnsupportedArgs, r *int) error {
srv.mu.Lock()
defer srv.mu.Unlock()
- if srv.pools[a.Pool].checked {
+
+ if srv.vrf.pools[a.Pool].checked {
return nil
}
- srv.pools[a.Pool].checked = true
+ srv.vrf.pools[a.Pool].checked = true
vrf := srv.vrf
for _, unsupported := range a.UnsupportedCalls {
@@ -86,7 +81,7 @@ func (srv *RPCServer) UpdateUnsupported(a *rpctype.UpdateUnsupportedArgs, r *int
vrf.SetPrintStatAtSIGINT()
vrf.choiceTable = vrf.target.BuildChoiceTable(nil, vrf.calls)
- srv.cond.Signal()
+ vrf.progGeneratorInit.Done()
}
return nil
}
@@ -94,119 +89,63 @@ func (srv *RPCServer) UpdateUnsupported(a *rpctype.UpdateUnsupportedArgs, r *int
// NextExchange is called when a Runner requests a new program to execute and,
// potentially, wants to send a new Result to the RPCServer.
func (srv *RPCServer) NextExchange(a *rpctype.NextExchangeArgs, r *rpctype.NextExchangeRes) error {
- srv.mu.Lock()
- defer srv.mu.Unlock()
-
- var res *ExecResult
- var prog *progInfo
if a.Info.Calls != nil {
- res = &ExecResult{
- Pool: a.Pool,
- Hanged: a.Hanged,
- Info: a.Info,
- RunIdx: a.RunIdx,
- }
-
- prog = srv.progs[a.ProgIdx]
- if prog == nil {
- // This case can happen if both conditions are true:
- // 1. a Runner calls Verifier.NextExchange, then crashes,
- // its corresponding Pool being the only one that hasn't
- // sent results for the program yet
- // 2.the cleanup call for the crash got the server's mutex before
- // the NextExchange call.
- // As the pool was the only one that hasn't sent the result, the
- // cleanup call has already removed prog from srv.progs by the time
- // the NextExchange call gets the server's mutex, which is why the
- // variable is nil. As the results for this program have already
- // been sent for verification, we discard this one.
- return nil
- }
-
- delete(srv.pools[a.Pool].runners[a.VM], prog.idx)
- if srv.newResult(res, prog) {
- if srv.vrf.processResults(prog) {
- delete(srv.progs, prog.idx)
- }
- }
+ srv.stopWaitResult(a.Pool, a.VM, a.ExecTaskID)
+ PutExecResult(&ExecResult{
+ Pool: a.Pool,
+ Hanged: a.Hanged,
+ Info: a.Info,
+ ExecTaskID: a.ExecTaskID,
+ })
}
- if srv.notChecked > 0 {
- // Runner is blocked until the choice table is created.
- srv.cond.Wait()
- }
+ // TODO: NewEnvironment is the currently hardcoded logic. Relax it.
+ task := srv.vrf.GetRunnerTask(a.Pool, NewEnvironment)
+ srv.startWaitResult(a.Pool, a.VM, task.ID)
+ r.ExecTask = *task
- newProg, pi, ri := srv.newProgram(a.Pool, a.VM)
- r.Prog = rpctype.Prog{Bytes: newProg, ProgIdx: pi, RunIdx: ri}
return nil
}
-// newResult is called when a Runner sends a new Result. It returns true if all
-// Results from the corresponding programs have been received and they can be
-// sent for verification. Otherwise, it returns false.
-func (srv *RPCServer) newResult(res *ExecResult, prog *progInfo) bool {
- ri := prog.runIdx
- if prog.res[ri][res.Pool] != nil {
- return false
- }
- prog.res[ri][res.Pool] = res
- prog.received++
- return prog.received == len(srv.pools)
+func vmTasksKey(poolID, vmID int) int {
+ return poolID*1000 + vmID
}
-func (srv *RPCServer) newRun(p *progInfo) {
- p.runIdx++
- p.received = 0
- p.res[p.runIdx] = make([]*ExecResult, len(srv.pools))
- for _, pool := range srv.pools {
- pool.toRerun = append(pool.toRerun, p)
- }
-}
-
-// newProgram returns a new program for the Runner identified by poolIdx and
-// vmIdx and the program's index.
-func (srv *RPCServer) newProgram(poolIdx, vmIdx int) ([]byte, int, int) {
- pool := srv.pools[poolIdx]
+func (srv *RPCServer) startWaitResult(poolID, vmID int, taskID int64) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
- if len(pool.toRerun) != 0 {
- p := pool.toRerun[0]
- pool.runners[vmIdx][p.idx] = p
- pool.toRerun = pool.toRerun[1:]
- return p.serialized, p.idx, p.runIdx
+ if srv.vmTasksInProgress == nil {
+ srv.vmTasksInProgress = make(map[int]map[int64]bool)
}
- if len(pool.progs) == 0 {
- prog, progIdx := srv.vrf.generate()
- pi := &progInfo{
- prog: prog,
- idx: progIdx,
- serialized: prog.Serialize(),
- res: make([][]*ExecResult, srv.vrf.reruns),
- }
- pi.res[0] = make([]*ExecResult, len(srv.pools))
- for _, pool := range srv.pools {
- pool.progs = append(pool.progs, pi)
- }
- srv.progs[progIdx] = pi
+ if srv.vmTasksInProgress[vmTasksKey(poolID, vmID)] == nil {
+ srv.vmTasksInProgress[vmTasksKey(poolID, vmID)] =
+ make(map[int64]bool)
}
- p := pool.progs[0]
- pool.runners[vmIdx][p.idx] = p
- pool.progs = pool.progs[1:]
- return p.serialized, p.idx, p.runIdx
+
+ srv.vmTasksInProgress[vmTasksKey(poolID, vmID)][taskID] = true
+}
+
+func (srv *RPCServer) stopWaitResult(poolID, vmID int, taskID int64) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+ delete(srv.vmTasksInProgress[vmTasksKey(poolID, vmID)], taskID)
}
// cleanup is called when a vm.Instance crashes.
-func (srv *RPCServer) cleanup(poolIdx, vmIdx int) {
+func (srv *RPCServer) cleanup(poolID, vmID int) {
srv.mu.Lock()
defer srv.mu.Unlock()
- progs := srv.pools[poolIdx].runners[vmIdx]
-
- for _, prog := range progs {
- if srv.newResult(&ExecResult{Pool: poolIdx, Crashed: true}, prog) {
- srv.vrf.processResults(prog)
- delete(srv.progs, prog.idx)
- delete(srv.pools[poolIdx].runners[vmIdx], prog.idx)
- continue
- }
+
+ // Signal error for every VM related task and let upper level logic to process it.
+ for taskID := range srv.vmTasksInProgress[vmTasksKey(poolID, vmID)] {
+ PutExecResult(&ExecResult{
+ Pool: poolID,
+ ExecTaskID: taskID,
+ Crashed: true,
+ Error: errors.New("VM crashed during the task execution"),
+ })
}
+ delete(srv.vmTasksInProgress, vmTasksKey(poolID, vmID))
}
diff --git a/syz-verifier/rpcserver_test.go b/syz-verifier/rpcserver_test.go
new file mode 100644
index 000000000..8d630e8c6
--- /dev/null
+++ b/syz-verifier/rpcserver_test.go
@@ -0,0 +1,32 @@
+// Copyright 2021 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 main
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/syzkaller/pkg/rpctype"
+)
+
+func TestConnect(t *testing.T) {
+ vrf := createTestVerifier(t)
+ vrf.pools = make(map[int]*poolInfo)
+ vrf.pools[1] = &poolInfo{}
+
+ a := &rpctype.RunnerConnectArgs{
+ Pool: 1,
+ VM: 1,
+ }
+
+ r := &rpctype.RunnerConnectRes{}
+
+ if err := vrf.srv.Connect(a, r); err != nil {
+ t.Fatalf("srv.Connect failed: %v", err)
+ }
+
+ if diff := cmp.Diff(&rpctype.RunnerConnectRes{CheckUnsupportedCalls: true}, r); diff != "" {
+ t.Errorf("Connect result mismatch (-want +got):\n%s", diff)
+ }
+}
diff --git a/syz-verifier/utils_test.go b/syz-verifier/utils_test.go
index a4affe90a..ffd255894 100644
--- a/syz-verifier/utils_test.go
+++ b/syz-verifier/utils_test.go
@@ -5,35 +5,32 @@ package main
import (
"io/ioutil"
- "math/rand"
"os"
"testing"
- "time"
"github.com/google/syzkaller/pkg/ipc"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/prog"
)
-func createTestServer(t *testing.T) *RPCServer {
+func createTestVerifier(t *testing.T) *Verifier {
target, err := prog.GetTarget("test", "64")
if err != nil {
t.Fatalf("failed to initialise test target: %v", err)
}
- vrf := Verifier{
+ vrf := &Verifier{
target: target,
choiceTable: target.DefaultChoiceTable(),
- rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
progIdx: 3,
reruns: 1,
}
vrf.resultsdir = makeTestResultDirectory(t)
vrf.stats = emptyTestStats()
- srv, err := startRPCServer(&vrf)
+ vrf.srv, err = startRPCServer(vrf)
if err != nil {
t.Fatalf("failed to initialise RPC server: %v", err)
}
- return srv
+ return vrf
}
func getTestProgram(t *testing.T) *prog.Prog {
diff --git a/syz-verifier/verifier.go b/syz-verifier/verifier.go
index 043ed07d7..1ad9eaab9 100644
--- a/syz-verifier/verifier.go
+++ b/syz-verifier/verifier.go
@@ -12,11 +12,14 @@ import (
"os/signal"
"path/filepath"
"strings"
+ "sync"
+ "sync/atomic"
"time"
"github.com/google/syzkaller/pkg/instance"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/osutil"
+ "github.com/google/syzkaller/pkg/rpctype"
"github.com/google/syzkaller/prog"
"github.com/google/syzkaller/vm"
)
@@ -31,24 +34,172 @@ type Verifier struct {
// - <workdir>/corpus.db: corpus with interesting programs
// - <workdir>/<OS-Arch>/instance-x: per VM instance temporary files
// grouped by OS/Arch
- workdir string
- crashdir string
- resultsdir string
- target *prog.Target
- runnerBin string
- executorBin string
- choiceTable *prog.ChoiceTable
- rnd *rand.Rand
- progIdx int
- addr string
- srv *RPCServer
- calls map[*prog.Syscall]bool
- reasons map[*prog.Syscall]string
- reportReasons bool
- stats *Stats
- statsWrite io.Writer
- newEnv bool
- reruns int
+ workdir string
+ crashdir string
+ resultsdir string
+ target *prog.Target
+ runnerBin string
+ executorBin string
+ progGeneratorInit sync.WaitGroup
+ choiceTable *prog.ChoiceTable
+ progIdx int
+ addr string
+ srv *RPCServer
+ calls map[*prog.Syscall]bool
+ reasons map[*prog.Syscall]string
+ reportReasons bool
+ stats *Stats
+ statsWrite io.Writer
+ newEnv bool
+ reruns int
+
+ // We use single queue for every kernel environment.
+ tasksMutex sync.Mutex
+ onTaskAdded *sync.Cond
+ kernelEnvTasks [][]*ExecTaskQueue
+}
+
+func (vrf *Verifier) Init() {
+ vrf.progGeneratorInit.Add(1)
+
+ vrf.onTaskAdded = sync.NewCond(&vrf.tasksMutex)
+
+ vrf.kernelEnvTasks = make([][]*ExecTaskQueue, len(vrf.pools))
+ for i := range vrf.kernelEnvTasks {
+ vrf.kernelEnvTasks[i] = make([]*ExecTaskQueue, EnvironmentsCount)
+ for j := range vrf.kernelEnvTasks[i] {
+ vrf.kernelEnvTasks[i][j] = MakeExecTaskQueue()
+ }
+ }
+
+ srv, err := startRPCServer(vrf)
+ if err != nil {
+ log.Fatalf("failed to initialise RPC server: %v", err)
+ }
+ vrf.srv = srv
+}
+
+func (vrf *Verifier) StartProgramsAnalysis() {
+ go func() {
+ vrf.progGeneratorInit.Wait()
+
+ type AnalysisResult struct {
+ Diff []*ExecResult
+ Prog *prog.Prog
+ }
+
+ results := make(chan *AnalysisResult)
+ go func() {
+ for result := range results {
+ if result.Diff != nil {
+ vrf.SaveDiffResults(result.Diff, result.Prog)
+ }
+ }
+ }()
+
+ for i := 0; i < 100; i++ {
+ go func() {
+ for {
+ prog := vrf.generate()
+ results <- &AnalysisResult{
+ vrf.TestProgram(prog),
+ prog,
+ }
+ }
+ }()
+ }
+ }()
+}
+
+func (vrf *Verifier) GetRunnerTask(kernel int, existing EnvDescr) *rpctype.ExecTask {
+ vrf.tasksMutex.Lock()
+ defer vrf.tasksMutex.Unlock()
+
+ for {
+ for env := existing; env >= AnyEnvironment; env-- {
+ if task, ok := vrf.kernelEnvTasks[kernel][env].PopTask(); ok {
+ return task.ToRPC()
+ }
+ }
+
+ vrf.onTaskAdded.Wait()
+ }
+}
+
+func PutExecResult(result *ExecResult) {
+ c := GetExecResultChan(result.ExecTaskID)
+ c <- result
+}
+
+// TestProgram return the results slice if some exec diff was found.
+func (vrf *Verifier) TestProgram(prog *prog.Prog) (result []*ExecResult) {
+ steps := []EnvDescr{
+ NewEnvironment,
+ NewEnvironment,
+ }
+
+ defer atomic.AddInt64(&vrf.stats.TotalProgs, 1)
+
+ for i, env := range steps {
+ stepRes, err := vrf.Run(prog, env)
+ if err != nil {
+ return
+ }
+ vrf.AddCallsExecutionStat(stepRes, prog)
+ if stepRes[0].IsEqual(stepRes[1]) {
+ if i != 0 {
+ atomic.AddInt64(&vrf.stats.FlakyProgs, 1)
+ }
+ return
+ }
+ if i == len(steps)-1 {
+ atomic.AddInt64(&vrf.stats.MismatchingProgs, 1)
+ return stepRes
+ }
+ }
+ return
+}
+
+// Run sends the program for verification to execution queues and return
+// result once it's ready.
+// In case of time-out, return (nil, error).
+func (vrf *Verifier) Run(prog *prog.Prog, env EnvDescr) (result []*ExecResult, err error) {
+ totalKernels := len(vrf.kernelEnvTasks)
+ result = make([]*ExecResult, totalKernels)
+
+ wg := sync.WaitGroup{}
+ wg.Add(totalKernels)
+ for i := 0; i < totalKernels; i++ {
+ i := i
+ q := vrf.kernelEnvTasks[i][env]
+
+ go func() {
+ defer wg.Done()
+ task := MakeExecTask(prog)
+ defer DeleteExecTask(task)
+
+ vrf.tasksMutex.Lock()
+ q.PushTask(task)
+ vrf.onTaskAdded.Signal()
+ vrf.tasksMutex.Unlock()
+
+ result[i] = <-task.ExecResultChan
+ }()
+ }
+ wg.Wait()
+
+ for _, item := range result {
+ if item == nil {
+ err = errors.New("something went wrong and we exit w/o results")
+ return nil, err
+ }
+ if item.Error != nil {
+ err = item.Error
+ return nil, err
+ }
+ }
+
+ return result, nil
}
// SetPrintStatAtSIGINT asks Stats object to report verification
@@ -77,24 +228,24 @@ func (vrf *Verifier) SetPrintStatAtSIGINT() error {
}
func (vrf *Verifier) startInstances() {
- for idx, pi := range vrf.pools {
- go func(pi *poolInfo, idx int) {
+ for poolID, pi := range vrf.pools {
+ go func(pi *poolInfo, poolID int) {
for {
// TODO: implement support for multiple VMs per Pool.
- vrf.createAndManageInstance(pi, idx)
+ vrf.createAndManageInstance(pi, poolID)
}
- }(pi, idx)
+ }(pi, poolID)
}
}
-func (vrf *Verifier) createAndManageInstance(pi *poolInfo, idx int) {
+func (vrf *Verifier) createAndManageInstance(pi *poolInfo, poolID int) {
inst, err := pi.pool.Create(0)
if err != nil {
log.Fatalf("failed to create instance: %v", err)
}
defer inst.Close()
- defer vrf.srv.cleanup(idx, 0)
+ defer vrf.srv.cleanup(poolID, 0)
fwdAddr, err := inst.Forward(vrf.srv.port)
if err != nil {
@@ -110,7 +261,7 @@ func (vrf *Verifier) createAndManageInstance(pi *poolInfo, idx int) {
log.Fatalf("failed to copy executor binary: %v", err)
}
- cmd := instance.RunnerCmd(runnerBin, fwdAddr, vrf.target.OS, vrf.target.Arch, idx, 0, false, vrf.newEnv)
+ cmd := instance.RunnerCmd(runnerBin, fwdAddr, vrf.target.OS, vrf.target.Arch, poolID, 0, false, vrf.newEnv)
outc, errc, err := inst.Run(pi.cfg.Timeouts.VMRunningTime, vrf.vmStop, cmd)
if err != nil {
log.Fatalf("failed to start runner: %v", err)
@@ -118,7 +269,7 @@ func (vrf *Verifier) createAndManageInstance(pi *poolInfo, idx int) {
inst.MonitorExecution(outc, errc, pi.Reporter, vm.ExitTimeout)
- log.Logf(0, "reboot the VM in pool %d", idx)
+ log.Logf(0, "reboot the VM in pool %d", poolID)
}
// finalizeCallSet removes the system calls that are not supported from the set
@@ -154,41 +305,16 @@ func (vrf *Verifier) finalizeCallSet(w io.Writer) {
}
}
-// processResults will send a set of complete results for verification and, in
-// case differences are found, it will start the rerun process for the program
-// (if reruns are enabled). If every rerun produces the same results, the result
-// report will be printed to persistent storage. Otherwise, the program is
-// discarded as flaky.
-func (vrf *Verifier) processResults(prog *progInfo) bool {
- // TODO: Simplify this if clause.
- if prog.runIdx == 0 {
- vrf.stats.TotalProgs++
- prog.report = Verify(prog.res[0], prog.prog, vrf.stats)
- if prog.report == nil {
- return true
- }
- } else {
- if !VerifyRerun(prog.res[prog.runIdx], prog.report) {
- vrf.stats.FlakyProgs++
- log.Logf(0, "flaky results detected: %d", vrf.stats.FlakyProgs)
- return true
- }
- }
-
- if prog.runIdx < vrf.reruns-1 {
- vrf.srv.newRun(prog)
- return false
- }
-
- rr := prog.report
- vrf.stats.MismatchingProgs++
-
+func (vrf *Verifier) AddCallsExecutionStat(results []*ExecResult, program *prog.Prog) {
+ rr := CompareResults(results, program)
for _, cr := range rr.Reports {
+ atomic.AddInt64(&vrf.stats.Calls[cr.Call].Occurrences, 1)
+
if !cr.Mismatch {
- break
+ continue
}
- vrf.stats.Calls[cr.Call].Mismatches++
- vrf.stats.TotalMismatches++
+ atomic.AddInt64(&vrf.stats.Calls[cr.Call].Mismatches, 1)
+ atomic.AddInt64(&vrf.stats.TotalMismatches, 1)
for _, state := range cr.States {
if state0 := cr.States[0]; state0 != state {
vrf.stats.Calls[cr.Call].States[state] = true
@@ -196,6 +322,11 @@ func (vrf *Verifier) processResults(prog *progInfo) bool {
}
}
}
+}
+
+// SaveDiffResults extract diff and save result on the persistent storage.
+func (vrf *Verifier) SaveDiffResults(results []*ExecResult, program *prog.Prog) bool {
+ rr := CompareResults(results, program)
oldest := 0
var oldestTime time.Time
@@ -226,10 +357,12 @@ func (vrf *Verifier) processResults(prog *progInfo) bool {
return true
}
-// generate will return a newly generated program and its index.
-func (vrf *Verifier) generate() (*prog.Prog, int) {
- vrf.progIdx++
- return vrf.target.Generate(vrf.rnd, prog.RecommendedCalls, vrf.choiceTable), vrf.progIdx
+// generate returns a newly generated program or error.
+func (vrf *Verifier) generate() *prog.Prog {
+ vrf.progGeneratorInit.Wait()
+
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano() + 1e12))
+ return vrf.target.Generate(rnd, prog.RecommendedCalls, vrf.choiceTable)
}
func createReport(rr *ResultReport, pools int) []byte {
diff --git a/syz-verifier/verifier_test.go b/syz-verifier/verifier_test.go
new file mode 100644
index 000000000..f3068787a
--- /dev/null
+++ b/syz-verifier/verifier_test.go
@@ -0,0 +1,279 @@
+// Copyright 2021 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 main
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/syzkaller/pkg/osutil"
+ "github.com/google/syzkaller/pkg/rpctype"
+ "github.com/google/syzkaller/prog"
+)
+
+func TestFinalizeCallSet(t *testing.T) {
+ target, err := prog.GetTarget("test", "64")
+ if err != nil {
+ t.Fatalf("failed to initialise test target: %v", err)
+ }
+
+ vrf := Verifier{
+ target: target,
+ reasons: map[*prog.Syscall]string{
+ target.SyscallMap["test$res0"]: "foo",
+ target.SyscallMap["minimize$0"]: "bar",
+ },
+ calls: map[*prog.Syscall]bool{
+ target.SyscallMap["minimize$0"]: true,
+ target.SyscallMap["test$res0"]: true,
+ target.SyscallMap["disabled1"]: true,
+ },
+ reportReasons: true,
+ }
+
+ out := bytes.Buffer{}
+ vrf.finalizeCallSet(&out)
+ wantLines := []string{
+ "The following calls have been disabled:\n",
+ "\ttest$res0: foo\n",
+ "\tminimize$0: bar\n",
+ }
+ output := out.String()
+ for _, line := range wantLines {
+ if !strings.Contains(output, line) {
+ t.Errorf("finalizeCallSet: %q missing in reported output", line)
+ }
+ }
+
+ wantCalls, gotCalls := map[*prog.Syscall]bool{
+ target.SyscallMap["disabled1"]: true,
+ }, vrf.calls
+ if diff := cmp.Diff(wantCalls, gotCalls); diff != "" {
+ t.Errorf("srv.calls mismatch (-want +got):\n%s", diff)
+ }
+}
+
+func TestUpdateUnsupported(t *testing.T) {
+ target, err := prog.GetTarget("test", "64")
+ if err != nil {
+ t.Fatalf("failed to initialise test target: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ vrfPools map[int]*poolInfo
+ wantPools map[int]*poolInfo
+ wantCalls map[*prog.Syscall]bool
+ wantNotChecked int
+ nilCT bool
+ }{
+ {
+ name: "choice table not generated",
+ vrfPools: map[int]*poolInfo{0: {}, 1: {}},
+ wantPools: map[int]*poolInfo{0: {checked: true}, 1: {}},
+ wantNotChecked: 1,
+ wantCalls: map[*prog.Syscall]bool{
+ target.SyscallMap["minimize$0"]: true,
+ target.SyscallMap["breaks_returns"]: true,
+ target.SyscallMap["test$res0"]: true,
+ target.SyscallMap["test$union0"]: true,
+ },
+ nilCT: true,
+ },
+ {
+ name: "choice table generated",
+ vrfPools: map[int]*poolInfo{0: {}},
+ wantPools: map[int]*poolInfo{0: {checked: true}},
+ wantNotChecked: 0,
+ wantCalls: map[*prog.Syscall]bool{
+ target.SyscallMap["minimize$0"]: true,
+ target.SyscallMap["breaks_returns"]: true,
+ },
+ nilCT: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ vrf := Verifier{
+ target: target,
+ pools: test.vrfPools,
+ reasons: make(map[*prog.Syscall]string),
+ reportReasons: true,
+ calls: map[*prog.Syscall]bool{
+ target.SyscallMap["minimize$0"]: true,
+ target.SyscallMap["breaks_returns"]: true,
+ target.SyscallMap["test$res0"]: true,
+ target.SyscallMap["test$union0"]: true,
+ },
+ stats: MakeStats(),
+ }
+ vrf.Init()
+
+ a := &rpctype.UpdateUnsupportedArgs{
+ Pool: 0,
+ UnsupportedCalls: []rpctype.SyscallReason{
+ {ID: 142, Reason: "foo"},
+ {ID: 2, Reason: "bar"},
+ {ID: 156, Reason: "tar"},
+ }}
+ if err := vrf.srv.UpdateUnsupported(a, nil); err != nil {
+ t.Fatalf("srv.UpdateUnsupported failed: %v", err)
+ }
+
+ if diff := cmp.Diff(test.wantPools, vrf.pools, cmp.AllowUnexported(poolInfo{})); diff != "" {
+ t.Errorf("srv.pools mismatch (-want +got):\n%s", diff)
+ }
+
+ wantReasons := map[*prog.Syscall]string{
+ target.SyscallMap["test$res0"]: "foo",
+ target.SyscallMap["test$union0"]: "tar",
+ }
+ if diff := cmp.Diff(wantReasons, vrf.reasons); diff != "" {
+ t.Errorf("srv.reasons mismatch (-want +got):\n%s", diff)
+ }
+
+ if diff := cmp.Diff(test.wantCalls, vrf.calls); diff != "" {
+ t.Errorf("srv.calls mismatch (-want +got):\n%s", diff)
+ }
+
+ if want, got := test.wantNotChecked, vrf.srv.notChecked; want != got {
+ t.Errorf("srv.notChecked: got %d want %d", got, want)
+ }
+
+ if want, got := test.nilCT, vrf.choiceTable == nil; want != got {
+ t.Errorf("vrf.choiceTable == nil: want nil, got: %v", vrf.choiceTable)
+ }
+ })
+ }
+}
+
+func TestUpdateUnsupportedNotCalledTwice(t *testing.T) {
+ vrf := Verifier{
+ pools: map[int]*poolInfo{
+ 0: {checked: false},
+ 1: {checked: false},
+ },
+ }
+ srv, err := startRPCServer(&vrf)
+ if err != nil {
+ t.Fatalf("failed to initialise RPC server: %v", err)
+ }
+ a := &rpctype.UpdateUnsupportedArgs{Pool: 0}
+
+ if err := srv.UpdateUnsupported(a, nil); err != nil {
+ t.Fatalf("srv.UpdateUnsupported failed: %v", err)
+ }
+ if want, got := 1, srv.notChecked; want != got {
+ t.Errorf("srv.notChecked: got %d want %d", got, want)
+ }
+
+ if err := srv.UpdateUnsupported(a, nil); err != nil {
+ t.Fatalf("srv.UpdateUnsupported failed: %v", err)
+ }
+ if want, got := 1, srv.notChecked; want != got {
+ t.Fatalf("srv.UpdateUnsupported called twice")
+ }
+
+ wantPools := map[int]*poolInfo{
+ 0: {checked: true},
+ 1: {checked: false},
+ }
+ if diff := cmp.Diff(wantPools, vrf.pools, cmp.AllowUnexported(poolInfo{})); diff != "" {
+ t.Errorf("srv.pools mismatch (-want +got):\n%s", diff)
+ }
+}
+
+func TestSaveDiffResults(t *testing.T) {
+ tests := []struct {
+ name string
+ res []*ExecResult
+ prog string
+ wantExist bool
+ wantStats *Stats
+ }{
+ {
+ name: "report written",
+ res: []*ExecResult{
+ makeExecResult(0, []int{1, 3, 2}),
+ makeExecResult(1, []int{1, 3, 5}),
+ },
+ wantExist: true,
+ wantStats: &Stats{
+ TotalMismatches: 1,
+ Calls: map[string]*CallStats{
+ "breaks_returns": makeCallStats("breaks_returns", 1, 0, map[ReturnState]bool{}),
+ "test$res0": makeCallStats("test$res0", 1, 1, map[ReturnState]bool{{Errno: 2}: true, {Errno: 5}: true}),
+ "minimize$0": makeCallStats("minimize$0", 1, 0, map[ReturnState]bool{}),
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ prog := getTestProgram(t)
+ vrf := Verifier{
+ resultsdir: makeTestResultDirectory(t),
+ stats: emptyTestStats(),
+ }
+ resultFile := filepath.Join(vrf.resultsdir, "result-0")
+
+ vrf.AddCallsExecutionStat(test.res, prog)
+ vrf.SaveDiffResults(test.res, prog)
+
+ if diff := cmp.Diff(test.wantStats, vrf.stats); diff != "" {
+ t.Errorf("vrf.stats mismatch (-want +got):\n%s", diff)
+ }
+
+ if got, want := osutil.IsExist(resultFile), test.wantExist; got != want {
+ t.Errorf("osutil.IsExist report file: got %v want %v", got, want)
+ }
+ os.Remove(filepath.Join(vrf.resultsdir, "result-0"))
+ })
+ }
+}
+
+func TestCreateReport(t *testing.T) {
+ rr := ResultReport{
+ Prog: "breaks_returns()\n" +
+ "minimize$0(0x1, 0x1)\n" +
+ "test$res0()\n",
+ Reports: []*CallReport{
+ {Call: "breaks_returns", States: map[int]ReturnState{
+ 0: returnState(1, 1),
+ 1: returnState(1, 1),
+ 2: returnState(1, 1)}},
+ {Call: "minimize$0", States: map[int]ReturnState{
+ 0: returnState(3, 3),
+ 1: returnState(3, 3),
+ 2: returnState(3, 3)}},
+ {Call: "test$res0", States: map[int]ReturnState{
+ 0: returnState(2, 7),
+ 1: returnState(5, 3),
+ 2: returnState(22, 1)},
+ Mismatch: true},
+ },
+ }
+ got := string(createReport(&rr, 3))
+ want := "ERRNO mismatches found for program:\n\n" +
+ "[=] breaks_returns()\n" +
+ "\t↳ Pool: 0, Flags: 1, Errno: 1 (operation not permitted)\n" +
+ "\t↳ Pool: 1, Flags: 1, Errno: 1 (operation not permitted)\n" +
+ "\t↳ Pool: 2, Flags: 1, Errno: 1 (operation not permitted)\n\n" +
+ "[=] minimize$0(0x1, 0x1)\n" +
+ "\t↳ Pool: 0, Flags: 3, Errno: 3 (no such process)\n" +
+ "\t↳ Pool: 1, Flags: 3, Errno: 3 (no such process)\n" +
+ "\t↳ Pool: 2, Flags: 3, Errno: 3 (no such process)\n\n" +
+ "[!] test$res0()\n" +
+ "\t↳ Pool: 0, Flags: 7, Errno: 2 (no such file or directory)\n" +
+ "\t↳ Pool: 1, Flags: 3, Errno: 5 (input/output error)\n" +
+ "\t↳ Pool: 2, Flags: 1, Errno: 22 (invalid argument)\n\n"
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("createReport: (-want +got):\n%s", diff)
+ }
+}