Skip to content

Commit

Permalink
pkg/vminfo: run programs interactively
Browse files Browse the repository at this point in the history
Use the same interfaces as the fuzzer.
Now syz-manager no longer needs to treat machine check executions
differently.
  • Loading branch information
a-nogikh committed May 7, 2024
1 parent 3d0c3e3 commit 2d4d4d3
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 201 deletions.
26 changes: 26 additions & 0 deletions pkg/fuzzer/queue/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Request struct {
NeedSignal SignalType
NeedCover bool
NeedHints bool
ExecOpts *ipc.ExecOpts

// If specified, the resulting signal for call SignalFilterCall
// will include subset of it even if it's not new.
SignalFilter signal.Signal
Expand Down Expand Up @@ -285,3 +287,27 @@ func (pq *PriorityQueue) Next() *Request {
defer pq.mu.Unlock()
return pq.ops.Pop()
}

type DynamicMultiplexer struct {
mu sync.Mutex
list []Source
}

func (dm *DynamicMultiplexer) Add(source Source) {
dm.mu.Lock()
defer dm.mu.Unlock()
dm.list = append(dm.list, source)
}

func (dm *DynamicMultiplexer) Next() *Request {
dm.mu.Lock()
list := dm.list
dm.mu.Unlock()
for _, source := range list {
req := source.Next()
if req != nil {
return req
}
}
return nil
}
43 changes: 26 additions & 17 deletions pkg/vminfo/linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import (
"runtime"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/syzkaller/pkg/flatrpc"
"github.com/google/syzkaller/pkg/fuzzer/queue"
"github.com/google/syzkaller/pkg/ipc"
"github.com/google/syzkaller/pkg/rpctype"
"github.com/google/syzkaller/sys/targets"
"github.com/stretchr/testify/assert"
)

func TestLinuxSyscalls(t *testing.T) {
cfg := testConfig(t, targets.Linux, targets.AMD64)
checker := New(cfg)
_, checkProgs := checker.StartCheck()
filesystems := []string{
"", "9p", "esdfs", "incremental-fs", "cgroup", "cgroup2",
"pvfs2", "nfs", "nfs4", "fuse", "fuseblk", "afs", "pipefs",
Expand All @@ -47,21 +47,10 @@ func TestLinuxSyscalls(t *testing.T) {
Data: []byte(strings.Join(filesystems, "\nnodev\t")),
},
}
var results []rpctype.ExecutionResult
for _, req := range checkProgs {
p, err := cfg.Target.DeserializeExec(req.ProgData, nil)
if err != nil {
t.Fatal(err)
}
res := rpctype.ExecutionResult{
ID: req.ID,
Info: ipc.ProgInfo{
Calls: make([]ipc.CallInfo, len(p.Calls)),
},
}
results = append(results, res)
}
enabled, disabled, err := checker.FinishCheck(files, results)
stop := make(chan struct{})
go fakeSuccessExecutor(checker, stop)
enabled, disabled, err := checker.Run(files)
close(stop)
if err != nil {
t.Fatal(err)
}
Expand All @@ -81,6 +70,26 @@ func TestLinuxSyscalls(t *testing.T) {
}
}

func fakeSuccessExecutor(source queue.Source, stop chan struct{}) {
for {
select {
case <-stop:
return
case <-time.After(time.Millisecond):
}
req := source.Next()
if req == nil {
continue
}
req.Done(&queue.Result{
Status: queue.Success,
Info: &ipc.ProgInfo{
Calls: make([]ipc.CallInfo, len(req.Prog.Calls)),
},
})
}
}

func TestReadKVMInfo(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("not linux")
Expand Down
152 changes: 34 additions & 118 deletions pkg/vminfo/syscalls.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
package vminfo

import (
"bytes"
"encoding/gob"
"context"
"fmt"
"slices"
"strings"
"syscall"

"github.com/google/syzkaller/pkg/flatrpc"
"github.com/google/syzkaller/pkg/hash"
"github.com/google/syzkaller/pkg/fuzzer/queue"
"github.com/google/syzkaller/pkg/ipc"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/rpctype"
"github.com/google/syzkaller/prog"
"github.com/google/syzkaller/sys/targets"
)
Expand All @@ -26,44 +23,19 @@ import (
// and provides primitives for reading target VM files, checking if a file can be opened,
// executing test programs on the target VM, etc.
//
// To make use of this type simpler, we collect all test programs that need
// to be executed on the target into a batch, send them to the target VM once,
// then get results and finish the check. This means that impl.syscallCheck
// cannot e.g. issue one test program, look at results, and then issue another one.
// This is achieved by starting each impl.syscallCheck in a separate goroutine
// and then waiting when it will call ctx.execRaw to submit syscalls that
// need to be executed on the target. Once all impl.syscallCheck submit
// their test syscalls, we know that we collected all of them.
// impl.syscallCheck may also decide to read a file on the target VM instead
// of executing a test program, this also counts as submitting an empty test program.
// This means that impl.syscallCheck cannot execute a test program after reading a file,
// but can do these things in opposite order (since all files are known ahead of time).
// These rules are bit brittle, but all of the checkers are unit-tested
// and misuse (trying to execute 2 programs, etc) will either crash or hang in tests.
// Theoretically we can execute more than 1 program per checker, but it will
// require some special arrangements, e.g. see handling of PseudoSyscallDeps.
//
// The external interface of this type contains only 2 methods:
// startCheck - starts impl.syscallCheck goroutines and collects all test programs in progs,
// finishCheck - accepts results of program execution, unblocks impl.syscallCheck goroutines,
//
// waits and returns results of checking.
type checkContext struct {
impl checker
cfg *mgrconfig.Config
target *prog.Target
sandbox ipc.EnvFlags
// Checkers use requests channel to submit their test programs,
// main goroutine will wait for exactly pendingRequests message on this channel
// (similar to sync.WaitGroup, pendingRequests is incremented before starting
// a goroutine that will send on requests).
requests chan []*rpctype.ExecutionRequest
pendingRequests int
// Ready channel is closed after we've recevied results of execution of test
// programs and file contents. After this results maps and fs are populated.
ready chan bool
results map[int64]*ipc.ProgInfo
fs filesystem
ctx context.Context
impl checker
cfg *mgrconfig.Config
target *prog.Target
sandbox ipc.EnvFlags
executor queue.Executor
fs filesystem
// Once checking of a syscall is finished, the result is sent to syscalls.
// The main goroutine will wait for exactly pendingSyscalls messages.
syscalls chan syscallResult
Expand All @@ -75,24 +47,24 @@ type syscallResult struct {
reason string
}

func newCheckContext(cfg *mgrconfig.Config, impl checker) *checkContext {
func newCheckContext(cfg *mgrconfig.Config, impl checker, executor queue.Executor) *checkContext {
sandbox, err := ipc.SandboxToFlags(cfg.Sandbox)
if err != nil {
panic(fmt.Sprintf("failed to parse sandbox: %v", err))
}
return &checkContext{
ctx: context.Background(),
impl: impl,
cfg: cfg,
target: cfg.Target,
sandbox: sandbox,
requests: make(chan []*rpctype.ExecutionRequest),
results: make(map[int64]*ipc.ProgInfo),
executor: executor,
syscalls: make(chan syscallResult),
ready: make(chan bool),
}
}

func (ctx *checkContext) startCheck() []rpctype.ExecutionRequest {
func (ctx *checkContext) start(fileInfos []flatrpc.FileInfo) {
ctx.fs = createVirtualFilesystem(fileInfos)
for _, id := range ctx.cfg.Syscalls {
call := ctx.target.Syscalls[id]
if call.Attrs.Disabled {
Expand All @@ -114,48 +86,24 @@ func (ctx *checkContext) startCheck() []rpctype.ExecutionRequest {
var depsReason chan string
deps := ctx.cfg.SysTarget.PseudoSyscallDeps[call.CallName]
if len(deps) != 0 {
ctx.pendingRequests++
depsReason = make(chan string, 1)
go func() {
depsReason <- ctx.supportedSyscalls(deps)
}()
}
ctx.pendingRequests++
go func() {
reason := syscallCheck(ctx, call)
ctx.waitForResults()
if reason == "" && depsReason != nil {
reason = <-depsReason
}
ctx.syscalls <- syscallResult{call, reason}
}()
}
var progs []rpctype.ExecutionRequest
dedup := make(map[hash.Sig]int64)
for i := 0; i < ctx.pendingRequests; i++ {
for _, req := range <-ctx.requests {
sig := hashReq(req)
req.ID = dedup[sig]
if req.ID != 0 {
continue
}
req.ID = int64(len(dedup) + 1)
dedup[sig] = req.ID
progs = append(progs, *req)
}
}
ctx.requests = nil
return progs
// TODO: we used to de-dup progs here, but the question is whether several % decrease
// in the number of executed programs is worth the code complication.
}

func (ctx *checkContext) finishCheck(fileInfos []flatrpc.FileInfo, progs []rpctype.ExecutionResult) (
map[*prog.Syscall]bool, map[*prog.Syscall]string, error) {
ctx.fs = createVirtualFilesystem(fileInfos)
for i := range progs {
res := &progs[i]
ctx.results[res.ID] = &res.Info
}
close(ctx.ready)
func (ctx *checkContext) wait() (map[*prog.Syscall]bool, map[*prog.Syscall]string, error) {
enabled := make(map[*prog.Syscall]bool)
disabled := make(map[*prog.Syscall]string)
for i := 0; i < ctx.pendingSyscalls; i++ {
Expand Down Expand Up @@ -271,16 +219,12 @@ func (ctx *checkContext) val(name string) uint64 {
}

func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root bool) *ipc.ProgInfo {
if ctx.requests == nil {
panic("only one test execution per checker is supported")
}
sandbox := ctx.sandbox
if root {
sandbox = 0
}
remain := calls
var requests []*rpctype.ExecutionRequest
for len(remain) != 0 {
info := &ipc.ProgInfo{}
for remain := calls; len(remain) != 0; {
// Don't put too many syscalls into a single program,
// it will have higher chances to time out.
ncalls := min(len(remain), prog.MaxCalls/2)
Expand All @@ -290,65 +234,37 @@ func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root
if err != nil {
panic(fmt.Sprintf("failed to deserialize: %v\n%v", err, progStr))
}
data, err := p.SerializeForExec()
if err != nil {
panic(fmt.Sprintf("failed to serialize test program: %v\n%s", err, progStr))
}
requests = append(requests, &rpctype.ExecutionRequest{
ProgData: slices.Clone(data), // clone to reduce memory usage
ExecOpts: ipc.ExecOpts{
// TODO: request that the program must be re-executed on the first failure.
res := queue.Execute(ctx.ctx, ctx.executor, &queue.Request{
Prog: p,
ExecOpts: &ipc.ExecOpts{
EnvFlags: sandbox,
ExecFlags: 0,
SandboxArg: ctx.cfg.SandboxArg,
},
})
}
ctx.requests <- requests
<-ctx.ready
info := &ipc.ProgInfo{}
for _, req := range requests {
res := ctx.results[req.ID]
if res == nil {
panic(fmt.Sprintf("no result for request %v", req.ID))
}
if len(res.Calls) == 0 {
panic(fmt.Sprintf("result for request %v has no calls", req.ID))
if res.Status == queue.Success {
info.Calls = append(info.Calls, res.Info.Calls...)
} else if res.Status == queue.Crashed {
// Pretend these calls were not executed.
info.Calls = append(info.Calls, ipc.EmptyProgInfo(ncalls).Calls...)
} else {
// The program must have been either executed or not due to a crash.
panic(fmt.Sprintf("got unexpected execution status (%d) for the prog %s",
res.Status, progStr))
}
info.Calls = append(info.Calls, res.Calls...)
}
if len(info.Calls) != len(calls) {
panic(fmt.Sprintf("got only %v results for program %v with %v calls:\n%s",
len(info.Calls), requests[0].ID, len(calls), strings.Join(calls, "\n")))
panic(fmt.Sprintf("got %v != %v results for program:\n%s",
len(info.Calls), len(calls), strings.Join(calls, "\n")))
}
return info
}

func (ctx *checkContext) readFile(name string) ([]byte, error) {
ctx.waitForResults()
return ctx.fs.ReadFile(name)
}

func (ctx *checkContext) waitForResults() {
// If syscallCheck has already executed a program, then it's also waited for ctx.ready.
// If it hasn't, then we need to unblock the loop in startCheck by sending a nil request.
if ctx.requests == nil {
return
}
ctx.requests <- nil
<-ctx.ready
if ctx.fs == nil {
panic("filesystem should be initialized by now")
}
}

func hashReq(req *rpctype.ExecutionRequest) hash.Sig {
buf := new(bytes.Buffer)
if err := gob.NewEncoder(buf).Encode(req.ExecOpts); err != nil {
panic(err)
}
return hash.Hash(req.ProgData, buf.Bytes())
}

func alwaysSupported(ctx *checkContext, call *prog.Syscall) string {
return ""
}
Expand Down
Loading

0 comments on commit 2d4d4d3

Please sign in to comment.