syzkaller/ipc/ipc.go
Dmitry Vyukov 0165a4b2e4 use fork server in executor
This avoids exec per test.
Also allows to pre-map shared memory regions.
And will allow to pre-map coverage regions, etc.

Seems to work already, but probably there are still some bugs.
2015-11-10 20:30:50 +01:00

564 lines
14 KiB
Go

// Copyright 2015 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 ipc
import (
"bytes"
"encoding/binary"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/google/syzkaller/prog"
)
type Env struct {
In []byte
Out []byte
cmd *command
inFile *os.File
outFile *os.File
bin []string
timeout time.Duration
flags uint64
}
const (
FlagDebug = uint64(1) << iota // debug output from executor
FlagCover // collect coverage
FlagThreaded // use multiple threads to mitigate blocked syscalls
FlagCollide // collide syscalls to provoke data races
FlagDedupCover // deduplicate coverage in executor
FlagDropPrivs // impersonate nobody user
FlagStrace // run executor under strace
)
func MakeEnv(bin string, timeout time.Duration, flags uint64) (*Env, error) {
inf, inmem, err := createMapping(1 << 20)
if err != nil {
return nil, err
}
defer func() {
if inf != nil {
closeMapping(inf, inmem)
}
}()
outf, outmem, err := createMapping(16 << 20)
if err != nil {
return nil, err
}
defer func() {
if outf != nil {
closeMapping(outf, outmem)
}
}()
for i := 0; i < 8; i++ {
inmem[i] = byte(flags >> (8 * uint(i)))
}
inmem = inmem[8:]
env := &Env{
In: inmem,
Out: outmem,
inFile: inf,
outFile: outf,
bin: strings.Split(bin, " "),
timeout: timeout,
flags: flags,
}
if len(env.bin) == 0 {
return nil, fmt.Errorf("binary is empty string")
}
env.bin[0], err = filepath.Abs(env.bin[0]) // we are going to chdir
if err != nil {
return nil, fmt.Errorf("filepath.Abs failed: %v", err)
}
inf = nil
outf = nil
return env, nil
}
func (env *Env) Close() error {
if env.cmd != nil {
env.cmd.close()
}
err1 := closeMapping(env.inFile, env.In)
err2 := closeMapping(env.outFile, env.Out)
switch {
case err1 != nil:
return err1
case err2 != nil:
return err2
default:
return nil
}
}
// Exec starts executor binary to execute program p and returns information about the execution:
// output: process output
// strace: strace output if env is created with FlagStrace
// cov: per-call coverage, len(cov) == len(p.Calls)
// failed: true if executor has detected a kernel bug
// hanged: program hanged and was killed
// err0: failed to start process, or executor has detected a logical error
func (env *Env) Exec(p *prog.Prog) (output, strace []byte, cov [][]uint32, failed, hanged bool, err0 error) {
if p != nil {
// Copy-in serialized program.
progData := p.SerializeForExec()
if len(progData) > len(env.In) {
panic("program is too long")
}
copy(env.In, progData)
}
if env.flags&FlagCover != 0 {
// Zero out the first word (ncmd), so that we don't have garbage there
// if executor crashes before writing non-garbage there.
for i := 0; i < 4; i++ {
env.Out[i] = 0
}
}
if env.cmd == nil {
env.cmd, err0 = makeCommand(env.bin, env.timeout, env.flags, env.inFile, env.outFile)
if err0 != nil {
return
}
}
output, strace, failed, hanged, err0 = env.cmd.exec()
if err0 != nil {
env.cmd.close()
env.cmd = nil
return
}
if env.flags&FlagCover == 0 || p == nil {
return
}
// Read out coverage information.
r := bytes.NewReader(env.Out)
var ncmd uint32
if err := binary.Read(r, binary.LittleEndian, &ncmd); err != nil {
err0 = fmt.Errorf("failed to read output coverage: %v", err)
return
}
cov = make([][]uint32, len(p.Calls))
for i := uint32(0); i < ncmd; i++ {
var callIndex, callNum, coverSize, pc uint32
if err := binary.Read(r, binary.LittleEndian, &callIndex); err != nil {
err0 = fmt.Errorf("failed to read output coverage: %v", err)
return
}
if err := binary.Read(r, binary.LittleEndian, &callNum); err != nil {
err0 = fmt.Errorf("failed to read output coverage: %v", err)
return
}
if err := binary.Read(r, binary.LittleEndian, &coverSize); err != nil {
err0 = fmt.Errorf("failed to read output coverage: %v", err)
return
}
if int(callIndex) > len(cov) {
err0 = fmt.Errorf("failed to read output coverage: expect index %v, got %v", i, callIndex)
return
}
if cov[callIndex] != nil {
err0 = fmt.Errorf("failed to read output coverage: double coverage for call %v", callIndex)
return
}
c := p.Calls[callIndex]
if num := c.Meta.ID; uint32(num) != callNum {
err0 = fmt.Errorf("failed to read output coverage: call %v: expect syscall %v, got %v, executed %v", callIndex, num, callNum, ncmd)
return
}
cov1 := make([]uint32, coverSize)
for j := uint32(0); j < coverSize; j++ {
if err := binary.Read(r, binary.LittleEndian, &pc); err != nil {
err0 = fmt.Errorf("failed to read output coverage: expect index %v, got %v", i, callIndex)
return
}
cov1[j] = pc
}
cov[callIndex] = cov1
}
return
}
/*
func (env *Env) execBin() (output, strace []byte, failed, hanged bool, err0 error) {
dir, err := ioutil.TempDir("./", "syzkaller-testdir")
if err != nil {
err0 = fmt.Errorf("failed to create temp dir: %v", err)
return
}
// Output capture pipe.
defer os.RemoveAll(dir)
rp, wp, err := os.Pipe()
if err != nil {
err0 = fmt.Errorf("failed to create pipe: %v", err)
return
}
defer rp.Close()
defer wp.Close()
// Input command pipe.
inrp, inwp, err := os.Pipe()
if err != nil {
err0 = fmt.Errorf("failed to create pipe: %v", err)
return
}
defer inrp.Close()
defer inwp.Close()
// Output command pipe.
outrp, outwp, err := os.Pipe()
if err != nil {
err0 = fmt.Errorf("failed to create pipe: %v", err)
return
}
defer outrp.Close()
defer outwp.Close()
cmd := exec.Command(env.bin[0], env.bin[1:]...)
traceFile := ""
if env.flags&FlagStrace != 0 {
f, err := ioutil.TempFile("./", "syzkaller-strace")
if err != nil {
err0 = fmt.Errorf("failed to create temp file: %v", err)
return
}
f.Close()
defer os.Remove(f.Name())
traceFile, _ = filepath.Abs(f.Name())
args := []string{"-s", "8", "-o", traceFile}
args = append(args, env.bin...)
if env.flags&FlagThreaded != 0 {
args = append([]string{"-f"}, args...)
}
cmd = exec.Command("strace", args...)
}
cmd.ExtraFiles = append(cmd.ExtraFiles, env.inFile, env.outFile, outrp, inwp)
cmd.Env = []string{}
cmd.Dir = dir
if env.flags&FlagDebug == 0 {
cmd.Stdout = wp
cmd.Stderr = wp
} else {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
}
if syscall.Getuid() == 0 {
// Running under root, more isolation is possible.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Cloneflags: syscall.CLONE_NEWNS}
} else {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
if _, err := outwp.Write([]byte{0}); err != nil {
err0 = fmt.Errorf("failed to write control pipe: %v", err)
return
}
if err := cmd.Start(); err != nil {
err0 = fmt.Errorf("failed to start executor binary: %v", err)
return
}
wp.Close()
outrp.Close()
inwp.Close()
done := make(chan bool)
hang := make(chan bool)
go func() {
t := time.NewTimer(env.timeout)
select {
case <-t.C:
// We started the process in its own process group and now kill the whole group.
// This solves a potential problem with strace:
// if we kill just strace, executor still runs and ReadAll below hangs.
fmt.Printf("KILLING %v\n", cmd.Process.Pid)
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
syscall.Kill(cmd.Process.Pid, syscall.SIGKILL)
syscall.Kill(cmd.Process.Pid, syscall.SIGKILL)
hang <- true
case <-done:
t.Stop()
hang <- false
}
}()
var tmp [1]byte
if n, err := inrp.Read(tmp[:]); n != 1 || err != nil {
err0 = fmt.Errorf("failed to read control pipe: %v", err)
return
}
output, err = ioutil.ReadAll(rp)
readErr := err
close(done)
if err = cmd.Wait(); <-hang && err != nil {
hanged = true
failed = true
}
if err != nil {
output = append(output, []byte(err.Error())...)
output = append(output, '\n')
}
if cmd.ProcessState != nil {
sys := cmd.ProcessState.Sys()
if ws, ok := sys.(syscall.WaitStatus); ok {
// Magic values returned by executor.
if ws.ExitStatus() == 67 {
err0 = fmt.Errorf("executor failed: %s", output)
return
}
if ws.ExitStatus() == 68 {
failed = true
}
}
}
if readErr != nil {
err0 = fmt.Errorf("failed to read executor output: %v", err)
return
}
if traceFile != "" {
strace, err = ioutil.ReadFile(traceFile)
if err != nil {
err0 = fmt.Errorf("failed to read strace output: %v", err)
return
}
}
return
}
*/
func createMapping(size int) (f *os.File, mem []byte, err error) {
f, err = ioutil.TempFile("./", "syzkaller-shm")
if err != nil {
err = fmt.Errorf("failed to create temp file: %v", err)
return
}
if err = f.Truncate(int64(size)); err != nil {
err = fmt.Errorf("failed to truncate shm file: %v", err)
f.Close()
os.Remove(f.Name())
return
}
f.Close()
f, err = os.OpenFile(f.Name(), os.O_RDWR, 0)
if err != nil {
err = fmt.Errorf("failed to open shm file: %v", err)
os.Remove(f.Name())
return
}
mem, err = syscall.Mmap(int(f.Fd()), 0, size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
err = fmt.Errorf("failed to mmap shm file: %v", err)
f.Close()
os.Remove(f.Name())
return
}
return
}
func closeMapping(f *os.File, mem []byte) error {
err1 := syscall.Munmap(mem)
err2 := f.Close()
err3 := os.Remove(f.Name())
switch {
case err1 != nil:
return err1
case err2 != nil:
return err2
case err3 != nil:
return err3
default:
return nil
}
}
type command struct {
timeout time.Duration
cmd *exec.Cmd
dir string
rp *os.File
inrp *os.File
outwp *os.File
}
func makeCommand(bin []string, timeout time.Duration, flags uint64, inFile *os.File, outFile *os.File) (*command, error) {
dir, err := ioutil.TempDir("./", "syzkaller-testdir")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %v", err)
}
if err := os.Chmod(dir, 0777); err != nil {
return nil, fmt.Errorf("failed to chmod temp dir: %v", err)
}
c := &command{timeout: timeout, dir: dir}
defer func() {
if c != nil {
c.close()
}
}()
// Output capture pipe.
rp, wp, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipe: %v", err)
}
defer wp.Close()
c.rp = rp
// Input command pipe.
inrp, inwp, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipe: %v", err)
}
defer inwp.Close()
c.inrp = inrp
// Output command pipe.
outrp, outwp, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipe: %v", err)
}
defer outrp.Close()
c.outwp = outwp
cmd := exec.Command(bin[0], bin[1:]...)
/*
traceFile := ""
if flags&FlagStrace != 0 {
f, err := ioutil.TempFile("./", "syzkaller-strace")
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %v", err)
}
f.Close()
defer os.Remove(f.Name())
traceFile, _ = filepath.Abs(f.Name())
args := []string{"-s", "8", "-o", traceFile}
args = append(args, env.bin...)
if env.flags&FlagThreaded != 0 {
args = append([]string{"-f"}, args...)
}
cmd = exec.Command("strace", args...)
}
*/
cmd.ExtraFiles = []*os.File{inFile, outFile, outrp, inwp}
cmd.Env = []string{}
cmd.Dir = dir
if flags&FlagDebug == 0 {
cmd.Stdout = wp
cmd.Stderr = wp
} else {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
}
if syscall.Getuid() == 0 {
// Running under root, more isolation is possible.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Cloneflags: syscall.CLONE_NEWNS}
} else {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start executor binary: %v", err)
}
c.cmd = cmd
tmp := c
c = nil // disable defer above
return tmp, nil
}
func (c *command) close() {
c.kill()
c.cmd.Wait()
os.RemoveAll(c.dir)
if c.rp != nil {
c.rp.Close()
}
if c.inrp != nil {
c.inrp.Close()
}
if c.outwp != nil {
c.outwp.Close()
}
}
func (c *command) kill() {
// We started the process in its own process group and now kill the whole group.
// This solves a potential problem with strace:
// if we kill just strace, executor still runs and ReadAll below hangs.
syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL)
syscall.Kill(c.cmd.Process.Pid, syscall.SIGKILL)
}
func (c *command) exec() (output, strace []byte, failed, hanged bool, err0 error) {
var tmp [1]byte
if _, err := c.outwp.Write(tmp[:]); err != nil {
err0 = fmt.Errorf("failed to write control pipe: %v", err)
return
}
done := make(chan bool)
hang := make(chan bool)
go func() {
t := time.NewTimer(c.timeout)
select {
case <-t.C:
c.kill()
hang <- true
case <-done:
t.Stop()
hang <- false
}
}()
//!!! handle c.rp overflow
_, readErr := c.inrp.Read(tmp[:])
close(done)
os.RemoveAll(c.dir)
if err := os.Mkdir(c.dir, 0777); err != nil {
<-hang
err0 = fmt.Errorf("failed to create temp dir: %v", err)
return
}
if readErr == nil {
<-hang
return
}
err0 = fmt.Errorf("executor did not answer")
c.kill()
var err error
output, err = ioutil.ReadAll(c.rp)
if err = c.cmd.Wait(); <-hang && err != nil {
hanged = true
}
if err != nil {
output = append(output, []byte(err.Error())...)
output = append(output, '\n')
}
if c.cmd.ProcessState != nil {
sys := c.cmd.ProcessState.Sys()
if ws, ok := sys.(syscall.WaitStatus); ok {
// Magic values returned by executor.
if ws.ExitStatus() == 67 {
err0 = fmt.Errorf("executor failed: %s", output)
return
}
if ws.ExitStatus() == 68 {
failed = true
}
}
}
/*
if traceFile != "" {
strace, err = ioutil.ReadFile(traceFile)
if err != nil {
err0 = fmt.Errorf("failed to read strace output: %v", err)
return
}
}
*/
return
}