diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2017-10-16 12:18:50 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2017-10-16 14:21:54 +0200 |
| commit | 85b1f93f8dbbc767c564e494a6353aa3517d5d49 (patch) | |
| tree | 702d2318a2ddcf1a576294a6a4981c58abcfcf61 /pkg/ipc/ipc_linux.go | |
| parent | f78642861b4dbe396a67d5e2a750e22f83f3edd5 (diff) | |
executor, pkg/ipc: unify ipc protocol between linux and other OSes
We currently use more complex and functional protocol on linux,
and a simple ad-hoc protocol on other OSes.
This leads to code duplication in both ipc and executor.
Linux supports coverage, shared memory communication and fork server,
which would also be useful for most other OSes.
Unify communication protocol and parametrize it by
(1) use of shmem or only pipes, (2) use of fork server.
This reduces duplication in ipc and executor and will
allow to support the useful features for other OSes easily.
Finally, this fixes akaros support as it currently uses
syz-stress running on host (linux) and executor running on akaros.
Diffstat (limited to 'pkg/ipc/ipc_linux.go')
| -rw-r--r-- | pkg/ipc/ipc_linux.go | 641 |
1 files changed, 0 insertions, 641 deletions
diff --git a/pkg/ipc/ipc_linux.go b/pkg/ipc/ipc_linux.go deleted file mode 100644 index 0279b0ee8..000000000 --- a/pkg/ipc/ipc_linux.go +++ /dev/null @@ -1,641 +0,0 @@ -// 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" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "syscall" - "time" - "unsafe" - - "github.com/google/syzkaller/pkg/osutil" - "github.com/google/syzkaller/prog" -) - -type Env struct { - in []byte - out []byte - - cmd *command - inFile *os.File - outFile *os.File - bin []string - pid int - config Config - - StatExecs uint64 - StatRestarts uint64 -} - -const ( - // Comparison types masks taken from KCOV headers. - compSizeMask = 6 - compSize8 = 6 - compConstMask = 1 -) - -func MakeEnv(bin string, pid int, config Config) (*Env, error) { - // IPC timeout must be larger then executor timeout. - // Otherwise IPC will kill parent executor but leave child executor alive. - if config.Timeout < 7*time.Second { - config.Timeout = 7 * time.Second - } - inf, inmem, err := createMapping(prog.ExecBufferSize) - if err != nil { - return nil, err - } - defer func() { - if inf != nil { - closeMapping(inf, inmem) - } - }() - outf, outmem, err := createMapping(outputSize) - if err != nil { - return nil, err - } - defer func() { - if outf != nil { - closeMapping(outf, outmem) - } - }() - serializeUint64(inmem[0:], config.Flags) - serializeUint64(inmem[8:], uint64(pid)) - inmem = inmem[16:] - env := &Env{ - in: inmem, - out: outmem, - inFile: inf, - outFile: outf, - bin: strings.Split(bin, " "), - pid: pid, - config: config, - } - if len(env.bin) == 0 { - return nil, fmt.Errorf("binary is empty string") - } - env.bin[0] = osutil.Abs(env.bin[0]) // we are going to chdir - // Append pid to binary name. - // E.g. if binary is 'syz-executor' and pid=15, - // we create a link from 'syz-executor15' to 'syz-executor' and use 'syz-executor15' as binary. - // This allows to easily identify program that lead to a crash in the log. - // Log contains pid in "executing program 15" and crashes usually contain "Comm: syz-executor15". - base := filepath.Base(env.bin[0]) - pidStr := fmt.Sprint(pid) - if len(base)+len(pidStr) >= 16 { - // TASK_COMM_LEN is currently set to 16 - base = base[:15-len(pidStr)] - } - binCopy := filepath.Join(filepath.Dir(env.bin[0]), base+pidStr) - if err := os.Link(env.bin[0], binCopy); err == nil { - env.bin[0] = binCopy - } - 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 -// info: per-call info -// 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(opts *ExecOpts, p *prog.Prog) (output []byte, info []CallInfo, failed, hanged bool, err0 error) { - if p != nil { - // Copy-in serialized program. - if _, err := p.SerializeForExec(env.in, env.pid); err != nil { - err0 = fmt.Errorf("executor %v: failed to serialize: %v", env.pid, err) - return - } - } - if env.config.Flags&FlagSignal != 0 { - // Zero out the first two words (ncmd and nsig), 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 - } - } - - atomic.AddUint64(&env.StatExecs, 1) - if env.cmd == nil { - atomic.AddUint64(&env.StatRestarts, 1) - env.cmd, err0 = makeCommand(env.pid, env.bin, env.config, env.inFile, env.outFile) - if err0 != nil { - return - } - } - var restart bool - output, failed, hanged, restart, err0 = env.cmd.exec(opts) - if err0 != nil || restart { - env.cmd.close() - env.cmd = nil - return - } - - if p == nil || env.config.Flags&FlagSignal == 0 && - env.config.Flags&FlagCollectComps == 0 { - return - } - info, err0 = env.readOutCoverage(p) - return -} - -func (env *Env) readOutCoverage(p *prog.Prog) (info []CallInfo, err0 error) { - out := ((*[1 << 28]uint32)(unsafe.Pointer(&env.out[0])))[:len(env.out)/int(unsafe.Sizeof(uint32(0)))] - readOut := func(v *uint32) bool { - if len(out) == 0 { - return false - } - *v = out[0] - out = out[1:] - return true - } - - readOutAndSetErr := func(v *uint32, msg string, args ...interface{}) bool { - if !readOut(v) { - err0 = fmt.Errorf(msg, args) - return false - } - return true - } - - // Reads out a 64 bits int in Little-endian as two blocks of 32 bits. - readOut64 := func(v *uint64, msg string, args ...interface{}) bool { - var a, b uint32 - if !(readOutAndSetErr(&a, msg, args) && readOutAndSetErr(&b, msg, args)) { - return false - } - *v = uint64(a) + uint64(b)<<32 - return true - } - - var ncmd uint32 - if !readOutAndSetErr(&ncmd, - "executor %v: failed to read output coverage", env.pid) { - return - } - info = make([]CallInfo, len(p.Calls)) - for i := range info { - info[i].Errno = -1 // not executed - } - dumpCov := func() string { - buf := new(bytes.Buffer) - for i, inf := range info { - str := "nil" - if inf.Signal != nil { - str = fmt.Sprint(len(inf.Signal)) - } - fmt.Fprintf(buf, "%v:%v|", i, str) - } - return buf.String() - } - for i := uint32(0); i < ncmd; i++ { - var callIndex, callNum, errno, faultInjected, signalSize, coverSize, compsSize uint32 - if !readOut(&callIndex) || !readOut(&callNum) || !readOut(&errno) || !readOut(&faultInjected) || !readOut(&signalSize) || !readOut(&coverSize) || !readOut(&compsSize) { - err0 = fmt.Errorf("executor %v: failed to read output coverage", env.pid) - return - } - if int(callIndex) >= len(info) { - err0 = fmt.Errorf("executor %v: failed to read output coverage: record %v, call %v, total calls %v (cov: %v)", - env.pid, i, callIndex, len(info), dumpCov()) - return - } - c := p.Calls[callIndex] - if num := c.Meta.ID; uint32(num) != callNum { - err0 = fmt.Errorf("executor %v: failed to read output coverage: record %v call %v: expect syscall %v, got %v, executed %v (cov: %v)", - env.pid, i, callIndex, num, callNum, ncmd, dumpCov()) - return - } - if info[callIndex].Signal != nil { - err0 = fmt.Errorf("executor %v: failed to read output coverage: double coverage for call %v (cov: %v)", - env.pid, callIndex, dumpCov()) - return - } - info[callIndex].Errno = int(errno) - info[callIndex].FaultInjected = faultInjected != 0 - if signalSize > uint32(len(out)) { - err0 = fmt.Errorf("executor %v: failed to read output signal: record %v, call %v, signalsize=%v coversize=%v", - env.pid, i, callIndex, signalSize, coverSize) - return - } - // Read out signals. - info[callIndex].Signal = out[:signalSize:signalSize] - out = out[signalSize:] - // Read out coverage. - if coverSize > uint32(len(out)) { - err0 = fmt.Errorf("executor %v: failed to read output coverage: record %v, call %v, signalsize=%v coversize=%v", - env.pid, i, callIndex, signalSize, coverSize) - return - } - info[callIndex].Cover = out[:coverSize:coverSize] - out = out[coverSize:] - // Read out comparisons. - compMap := make(prog.CompMap) - for j := uint32(0); j < compsSize; j++ { - var typ uint32 - var op1, op2 uint64 - if !readOutAndSetErr(&typ, - "executor %v: failed while reading type of comparison %v", env.pid, j) { - return - } - if typ > compConstMask|compSizeMask { - err0 = fmt.Errorf("executor %v: got wrong value (%v) while reading type of comparison %v", - env.pid, typ, j) - return - } - - isSize8 := (typ & compSizeMask) == compSize8 - isConst := (typ & compConstMask) != 0 - arg1ErrString := "executor %v: failed while reading op1 of comparison %v" - arg2ErrString := "executor %v: failed while reading op2 of comparison %v" - if isSize8 { - var tmp1, tmp2 uint32 - if !readOutAndSetErr(&tmp1, arg1ErrString, env.pid, j) || - !readOutAndSetErr(&tmp2, arg2ErrString, env.pid, j) { - return - } - op1 = uint64(tmp1) - op2 = uint64(tmp2) - } else { - if !readOut64(&op1, arg1ErrString, env.pid, j) || - !readOut64(&op2, arg2ErrString, env.pid, j) { - return - } - } - if op1 == op2 { - // It's useless to store such comparisons. - continue - } - compMap.AddComp(op2, op1) - if isConst { - // If one of the operands was const, then this operand is always - // placed first in the instrumented callbacks. Such an operand - // could not be an argument of our syscalls (because otherwise - // it wouldn't be const), thus we simply ignore it. - continue - } - compMap.AddComp(op1, op2) - } - info[callIndex].Comps = compMap - } - 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() - fname := f.Name() - f, err = os.OpenFile(f.Name(), os.O_RDWR, osutil.DefaultFilePerm) - if err != nil { - err = fmt.Errorf("failed to open shm file: %v", err) - os.Remove(fname) - 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 { - pid int - config Config - cmd *exec.Cmd - dir string - readDone chan []byte - exited chan struct{} - inrp *os.File - outwp *os.File -} - -func makeCommand(pid int, bin []string, config Config, 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) - } - - c := &command{ - pid: pid, - config: config, - dir: dir, - } - defer func() { - if c != nil { - c.close() - } - }() - - if config.Flags&(FlagSandboxSetuid|FlagSandboxNamespace) != 0 { - if err := os.Chmod(dir, 0777); err != nil { - return nil, fmt.Errorf("failed to chmod temp dir: %v", err) - } - } - - // Output capture pipe. - rp, wp, err := os.Pipe() - if err != nil { - return nil, fmt.Errorf("failed to create pipe: %v", err) - } - defer wp.Close() - - // 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 - - c.readDone = make(chan []byte, 1) - c.exited = make(chan struct{}) - - cmd := exec.Command(bin[0], bin[1:]...) - cmd.ExtraFiles = []*os.File{inFile, outFile, outrp, inwp} - cmd.Env = []string{} - cmd.Dir = dir - if config.Flags&FlagDebug == 0 { - cmd.Stdout = wp - cmd.Stderr = wp - go func(c *command) { - // Read out output in case executor constantly prints something. - bufSize := c.config.BufferSize - if bufSize == 0 { - bufSize = 128 << 10 - } - output := make([]byte, bufSize) - var size uint64 - for { - n, err := rp.Read(output[size:]) - if n > 0 { - size += uint64(n) - if size >= bufSize*3/4 { - copy(output, output[size-bufSize/2:size]) - size = bufSize / 2 - } - } - if err != nil { - rp.Close() - c.readDone <- output[:size] - close(c.readDone) - return - } - } - }(c) - } else { - close(c.readDone) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - } - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start executor binary: %v", err) - } - c.cmd = cmd - wp.Close() - inwp.Close() - if err := c.waitServing(); err != nil { - return nil, err - } - - tmp := c - c = nil // disable defer above - return tmp, nil -} - -func (c *command) close() { - if c.cmd != nil { - c.abort() - c.wait() - } - osutil.UmountAll(c.dir) - os.RemoveAll(c.dir) - if c.inrp != nil { - c.inrp.Close() - } - if c.outwp != nil { - c.outwp.Close() - } -} - -// Wait for executor to start serving (sandbox setup can take significant time). -func (c *command) waitServing() error { - read := make(chan error, 1) - go func() { - var buf [1]byte - _, err := c.inrp.Read(buf[:]) - read <- err - }() - timeout := time.NewTimer(time.Minute) - select { - case err := <-read: - timeout.Stop() - if err != nil { - c.abort() - output := <-c.readDone - err = fmt.Errorf("executor is not serving: %v\n%s", err, output) - c.wait() - 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() == statusFail { - err = ExecutorFailure(fmt.Sprintf("executor is not serving:\n%s", output)) - } - } - } - } - return err - case <-timeout.C: - return fmt.Errorf("executor is not serving") - } -} - -// abort sends the abort signal to the command and then SIGKILL if wait doesn't -// return within 5s. -func (c *command) abort() { - sig := syscall.Signal(c.config.AbortSignal) - if sig <= 0 || sig >= 32 { - sig = syscall.SIGKILL - } - syscall.Kill(c.cmd.Process.Pid, sig) - if sig != syscall.SIGKILL { - go func() { - t := time.NewTimer(5 * time.Second) - select { - case <-t.C: - syscall.Kill(c.cmd.Process.Pid, syscall.SIGKILL) - case <-c.exited: - t.Stop() - } - }() - } -} - -func (c *command) wait() error { - err := c.cmd.Wait() - select { - case <-c.exited: - // c.exited closed by an earlier call to wait. - default: - close(c.exited) - } - return err -} - -func (c *command) exec(opts *ExecOpts) (output []byte, failed, hanged, restart bool, err0 error) { - if opts.Flags&FlagInjectFault != 0 { - enableFaultOnce.Do(enableFaultInjection) - } - var inCmd [24]byte - serializeUint64(inCmd[0:], opts.Flags) - serializeUint64(inCmd[8:], uint64(opts.FaultCall)) - serializeUint64(inCmd[16:], uint64(opts.FaultNth)) - if _, err := c.outwp.Write(inCmd[:]); err != nil { - output = <-c.readDone - 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.config.Timeout) - select { - case <-t.C: - c.abort() - hang <- true - case <-done: - t.Stop() - hang <- false - } - }() - var reply [1]byte - readN, readErr := c.inrp.Read(reply[:]) - close(done) - status := 0 - if readErr == nil { - if readN != len(reply) { - panic(fmt.Sprintf("executor %v: read only %v bytes", c.pid, readN)) - } - status = int(reply[0]) - if status == 0 { - // Program was OK. - <-hang - return - } - // Executor writes magic values into the pipe before exiting, - // so proceed with killing and joining it. - } - c.abort() - output = <-c.readDone - if err := c.wait(); <-hang { - hanged = true - // In all likelihood, this will be duplicated by the default - // case below, but that's fine. - output = append(output, []byte(err.Error())...) - output = append(output, '\n') - } - // Handle magic values returned by executor. - switch status { - case statusFail: - err0 = ExecutorFailure(fmt.Sprintf("executor failed: %s", output)) - case statusError: - err0 = fmt.Errorf("executor detected kernel bug") - failed = true - case statusRetry: - // This is a temporal error (ENOMEM) or an unfortunate - // program that messes with testing setup (e.g. kills executor - // loop process). Pretend that nothing happened. - // It's better than a false crash report. - err0 = nil - hanged = false - restart = true - default: - // Failed to get a valid (or perhaps any) status from the - // executor. - // - // Once the executor is serving the status is always written to - // the pipe, so we don't bother to check the specific exit - // codes from wait. - err0 = fmt.Errorf("invalid (or no) executor status received: %d, executor exit: %s", status, c.cmd.ProcessState) - } - return -} - -func serializeUint64(buf []byte, v uint64) { - for i := 0; i < 8; i++ { - buf[i] = byte(v >> (8 * uint(i))) - } -} - -var enableFaultOnce sync.Once - -func enableFaultInjection() { - if err := osutil.WriteFile("/sys/kernel/debug/failslab/ignore-gfp-wait", []byte("N")); err != nil { - panic(fmt.Sprintf("failed to write /sys/kernel/debug/failslab/ignore-gfp-wait: %v", err)) - } - if err := osutil.WriteFile("/sys/kernel/debug/fail_futex/ignore-private", []byte("N")); err != nil { - panic(fmt.Sprintf("failed to write /sys/kernel/debug/fail_futex/ignore-private: %v", err)) - } -} |
