github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/runtest/run.go (about)

     1  // Copyright 2018 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // Package runtest is a driver for end-to-end testing of syzkaller programs.
     5  // It tests program execution via both executor and csource,
     6  // with different sandboxes and execution modes (threaded, repeated, etc).
     7  // It can run test OS programs locally via run_test.go
     8  // and all other real OS programs via tools/syz-runtest
     9  // which uses manager config to wind up VMs.
    10  // Test programs are located in sys/*/test/* files.
    11  package runtest
    12  
    13  import (
    14  	"bufio"
    15  	"bytes"
    16  	"context"
    17  	"fmt"
    18  	"os"
    19  	"path/filepath"
    20  	"regexp"
    21  	"runtime"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/google/syzkaller/pkg/csource"
    28  	"github.com/google/syzkaller/pkg/flatrpc"
    29  	"github.com/google/syzkaller/pkg/fuzzer/queue"
    30  	"github.com/google/syzkaller/pkg/ipc"
    31  	"github.com/google/syzkaller/prog"
    32  	"github.com/google/syzkaller/sys/targets"
    33  	"golang.org/x/sync/errgroup"
    34  )
    35  
    36  type runRequest struct {
    37  	*queue.Request
    38  
    39  	err     error
    40  	result  *queue.Result
    41  	results *ipc.ProgInfo // the expected results
    42  
    43  	name   string
    44  	broken string
    45  	skip   string
    46  }
    47  
    48  type Context struct {
    49  	Dir          string
    50  	Target       *prog.Target
    51  	Features     flatrpc.Feature
    52  	EnabledCalls map[string]map[*prog.Syscall]bool
    53  	LogFunc      func(text string)
    54  	Retries      int // max number of test retries to deal with flaky tests
    55  	Verbose      bool
    56  	Debug        bool
    57  	Tests        string // prefix to match test file names
    58  
    59  	executor queue.PlainQueue
    60  }
    61  
    62  func (ctx *Context) log(msg string, args ...interface{}) {
    63  	ctx.LogFunc(fmt.Sprintf(msg, args...))
    64  }
    65  
    66  func (ctx *Context) Run() error {
    67  	if ctx.Retries%2 == 0 {
    68  		ctx.Retries++
    69  	}
    70  	progs := make(chan *runRequest, 1000)
    71  	var eg errgroup.Group
    72  	eg.Go(func() error {
    73  		defer close(progs)
    74  		return ctx.generatePrograms(progs)
    75  	})
    76  	done := make(chan *runRequest)
    77  	eg.Go(func() error {
    78  		return ctx.processResults(done)
    79  	})
    80  
    81  	var wg sync.WaitGroup
    82  	for req := range progs {
    83  		req := req
    84  		if req.broken != "" || req.skip != "" {
    85  			done <- req
    86  			continue
    87  		}
    88  		wg.Add(1)
    89  		go func() {
    90  			defer wg.Done()
    91  			// The tests depend on timings and may be flaky, esp on overloaded/slow machines.
    92  			// We don't want to fix this by significantly bumping all timeouts,
    93  			// because if a program fails all the time with the default timeouts,
    94  			// it will also fail during fuzzing. And we want to ensure that it's not the case.
    95  			// So what we want is to tolerate episodic failures with the default timeouts.
    96  			// To achieve this we run each test several times and ensure that it passes
    97  			// in 50+% of cases (i.e. 1/1, 2/3, 3/5, 4/7, etc).
    98  			// In the best case this allows to get off with just 1 test run.
    99  			var resultErr error
   100  			for try, failed := 0, 0; try < ctx.Retries; try++ {
   101  				ctx.executor.Submit(req.Request)
   102  				req.result = req.Request.Wait(context.Background())
   103  				if req.result.Err != nil {
   104  					resultErr = req.result.Err
   105  					break
   106  				}
   107  				err := checkResult(req)
   108  				if err != nil {
   109  					failed++
   110  					resultErr = err
   111  				}
   112  				if ok := try + 1 - failed; ok > failed {
   113  					resultErr = nil
   114  					break
   115  				}
   116  			}
   117  			req.err = resultErr
   118  			done <- req
   119  		}()
   120  	}
   121  	wg.Wait()
   122  	close(done)
   123  	return eg.Wait()
   124  }
   125  
   126  func (ctx *Context) Next() *queue.Request {
   127  	return ctx.executor.Next()
   128  }
   129  
   130  func (ctx *Context) processResults(requests chan *runRequest) error {
   131  	var ok, fail, broken, skip int
   132  	for req := range requests {
   133  		result := ""
   134  		verbose := false
   135  		if req.broken != "" {
   136  			broken++
   137  			result = fmt.Sprintf("BROKEN (%v)", req.broken)
   138  			verbose = true
   139  		} else if req.skip != "" {
   140  			skip++
   141  			result = fmt.Sprintf("SKIP (%v)", req.skip)
   142  			verbose = true
   143  		} else {
   144  			if req.err != nil {
   145  				fail++
   146  				result = fmt.Sprintf("FAIL: %v",
   147  					strings.Replace(req.err.Error(), "\n", "\n\t", -1))
   148  				res := req.result
   149  				if len(res.Output) != 0 {
   150  					result += fmt.Sprintf("\n\t%s",
   151  						strings.Replace(string(res.Output), "\n", "\n\t", -1))
   152  				}
   153  			} else {
   154  				ok++
   155  				result = "OK"
   156  			}
   157  		}
   158  		if !verbose || ctx.Verbose {
   159  			ctx.log("%-38v: %v", req.name, result)
   160  		}
   161  		if req.Request != nil && req.Request.BinaryFile != "" {
   162  			os.Remove(req.BinaryFile)
   163  		}
   164  	}
   165  	ctx.log("ok: %v, broken: %v, skip: %v, fail: %v", ok, broken, skip, fail)
   166  	if fail != 0 {
   167  		return fmt.Errorf("tests failed")
   168  	}
   169  	return nil
   170  }
   171  
   172  func (ctx *Context) generatePrograms(progs chan *runRequest) error {
   173  	cover := []bool{false}
   174  	if ctx.Features&flatrpc.FeatureCoverage != 0 {
   175  		cover = append(cover, true)
   176  	}
   177  	var sandboxes []string
   178  	for sandbox := range ctx.EnabledCalls {
   179  		sandboxes = append(sandboxes, sandbox)
   180  	}
   181  	sort.Strings(sandboxes)
   182  	files, err := progFileList(ctx.Dir, ctx.Tests)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	for _, file := range files {
   187  		if err := ctx.generateFile(progs, sandboxes, cover, file); err != nil {
   188  			return err
   189  		}
   190  	}
   191  	return nil
   192  }
   193  
   194  func progFileList(dir, filter string) ([]string, error) {
   195  	files, err := os.ReadDir(dir)
   196  	if err != nil {
   197  		return nil, fmt.Errorf("failed to read %v: %w", dir, err)
   198  	}
   199  	var res []string
   200  	for _, file := range files {
   201  		if strings.HasSuffix(file.Name(), "~") ||
   202  			strings.HasSuffix(file.Name(), ".swp") ||
   203  			!strings.HasPrefix(file.Name(), filter) {
   204  			continue
   205  		}
   206  		res = append(res, file.Name())
   207  	}
   208  	return res, nil
   209  }
   210  
   211  func (ctx *Context) generateFile(progs chan *runRequest, sandboxes []string, cover []bool, filename string) error {
   212  	p, requires, results, err := parseProg(ctx.Target, ctx.Dir, filename)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	if p == nil {
   217  		return nil
   218  	}
   219  	sysTarget := targets.Get(ctx.Target.OS, ctx.Target.Arch)
   220  nextSandbox:
   221  	for _, sandbox := range sandboxes {
   222  		name := fmt.Sprintf("%v %v", filename, sandbox)
   223  		for _, call := range p.Calls {
   224  			if !ctx.EnabledCalls[sandbox][call.Meta] {
   225  				progs <- &runRequest{
   226  					name: name,
   227  					skip: fmt.Sprintf("unsupported call %v", call.Meta.Name),
   228  				}
   229  				continue nextSandbox
   230  			}
   231  		}
   232  		properties := map[string]bool{
   233  			"manual":             ctx.Tests != "", // "manual" tests run only if selected by the filter explicitly.
   234  			"sandbox=" + sandbox: true,
   235  			"littleendian":       ctx.Target.LittleEndian,
   236  		}
   237  		for _, threaded := range []bool{false, true} {
   238  			name := name
   239  			if threaded {
   240  				name += "/thr"
   241  			}
   242  			properties["threaded"] = threaded
   243  			for _, times := range []int{1, 3} {
   244  				properties["repeat"] = times > 1
   245  				properties["norepeat"] = times <= 1
   246  				if times > 1 {
   247  					name += "/repeat"
   248  				}
   249  				for _, cov := range cover {
   250  					if sandbox == "" {
   251  						break // executor does not support empty sandbox
   252  					}
   253  					name := name
   254  					if cov {
   255  						name += "/cover"
   256  					}
   257  					properties["cover"] = cov
   258  					properties["C"] = false
   259  					properties["executor"] = true
   260  					req, err := ctx.createSyzTest(p, sandbox, threaded, cov, times)
   261  					if err != nil {
   262  						return err
   263  					}
   264  					ctx.produceTest(progs, req, name, properties, requires, results)
   265  				}
   266  				if sysTarget.HostFuzzer {
   267  					// For HostFuzzer mode, we need to cross-compile
   268  					// and copy the binary to the target system.
   269  					continue
   270  				}
   271  				name := name
   272  				properties["C"] = true
   273  				properties["executor"] = false
   274  				name += " C"
   275  				if !sysTarget.ExecutorUsesForkServer && times > 1 {
   276  					// Non-fork loop implementation does not support repetition.
   277  					progs <- &runRequest{
   278  						name:   name,
   279  						broken: "non-forking loop",
   280  					}
   281  					continue
   282  				}
   283  				req, err := ctx.createCTest(p, sandbox, threaded, times)
   284  				if err != nil {
   285  					return err
   286  				}
   287  				ctx.produceTest(progs, req, name, properties, requires, results)
   288  			}
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func parseProg(target *prog.Target, dir, filename string) (*prog.Prog, map[string]bool, *ipc.ProgInfo, error) {
   295  	data, err := os.ReadFile(filepath.Join(dir, filename))
   296  	if err != nil {
   297  		return nil, nil, nil, fmt.Errorf("failed to read %v: %w", filename, err)
   298  	}
   299  	requires := parseRequires(data)
   300  	// Need to check arch requirement early as some programs
   301  	// may fail to deserialize on some arches due to missing syscalls.
   302  	if !checkArch(requires, target.Arch) {
   303  		return nil, nil, nil, nil
   304  	}
   305  	p, err := target.Deserialize(data, prog.Strict)
   306  	if err != nil {
   307  		return nil, nil, nil, fmt.Errorf("failed to deserialize %v: %w", filename, err)
   308  	}
   309  	errnos := map[string]int{
   310  		"":           0,
   311  		"EPERM":      1,
   312  		"ENOENT":     2,
   313  		"E2BIG":      7,
   314  		"ENOEXEC":    8,
   315  		"EBADF":      9,
   316  		"ENOMEM":     12,
   317  		"EACCES":     13,
   318  		"EFAULT":     14,
   319  		"EXDEV":      18,
   320  		"EINVAL":     22,
   321  		"ENOTTY":     25,
   322  		"EOPNOTSUPP": 95,
   323  
   324  		// Fuchsia specific errors.
   325  		"ZX_ERR_NO_RESOURCES":   3,
   326  		"ZX_ERR_INVALID_ARGS":   10,
   327  		"ZX_ERR_BAD_HANDLE":     11,
   328  		"ZX_ERR_BAD_STATE":      20,
   329  		"ZX_ERR_TIMED_OUT":      21,
   330  		"ZX_ERR_SHOULD_WAIT":    22,
   331  		"ZX_ERR_PEER_CLOSED":    24,
   332  		"ZX_ERR_ALREADY_EXISTS": 26,
   333  		"ZX_ERR_ACCESS_DENIED":  30,
   334  	}
   335  	info := &ipc.ProgInfo{Calls: make([]ipc.CallInfo, len(p.Calls))}
   336  	for i, call := range p.Calls {
   337  		info.Calls[i].Flags |= ipc.CallExecuted | ipc.CallFinished
   338  		switch call.Comment {
   339  		case "blocked":
   340  			info.Calls[i].Flags |= ipc.CallBlocked
   341  		case "unfinished":
   342  			info.Calls[i].Flags &^= ipc.CallFinished
   343  		case "unexecuted":
   344  			info.Calls[i].Flags &^= ipc.CallExecuted | ipc.CallFinished
   345  		default:
   346  			res, ok := errnos[call.Comment]
   347  			if !ok {
   348  				return nil, nil, nil, fmt.Errorf("%v: unknown call comment %q",
   349  					filename, call.Comment)
   350  			}
   351  			info.Calls[i].Errno = res
   352  		}
   353  	}
   354  	return p, requires, info, nil
   355  }
   356  
   357  func parseRequires(data []byte) map[string]bool {
   358  	requires := make(map[string]bool)
   359  	for s := bufio.NewScanner(bytes.NewReader(data)); s.Scan(); {
   360  		const prefix = "# requires:"
   361  		line := s.Text()
   362  		if !strings.HasPrefix(line, prefix) {
   363  			continue
   364  		}
   365  		for _, req := range strings.Fields(line[len(prefix):]) {
   366  			positive := true
   367  			if req[0] == '-' {
   368  				positive = false
   369  				req = req[1:]
   370  			}
   371  			requires[req] = positive
   372  		}
   373  	}
   374  	return requires
   375  }
   376  
   377  func checkArch(requires map[string]bool, arch string) bool {
   378  	for req, positive := range requires {
   379  		const prefix = "arch="
   380  		if strings.HasPrefix(req, prefix) &&
   381  			arch != req[len(prefix):] == positive {
   382  			return false
   383  		}
   384  	}
   385  	return true
   386  }
   387  
   388  func (ctx *Context) produceTest(progs chan *runRequest, req *runRequest, name string,
   389  	properties, requires map[string]bool, results *ipc.ProgInfo) {
   390  	req.name = name
   391  	req.results = results
   392  	if !match(properties, requires) {
   393  		req.skip = "excluded by constraints"
   394  	}
   395  	progs <- req
   396  }
   397  
   398  func match(props, requires map[string]bool) bool {
   399  	for req, positive := range requires {
   400  		if positive {
   401  			if !props[req] {
   402  				return false
   403  			}
   404  			continue
   405  		}
   406  		matched := true
   407  		for _, req1 := range strings.Split(req, ",") {
   408  			if !props[req1] {
   409  				matched = false
   410  			}
   411  		}
   412  		if matched {
   413  			return false
   414  		}
   415  	}
   416  	return true
   417  }
   418  
   419  func (ctx *Context) createSyzTest(p *prog.Prog, sandbox string, threaded, cov bool, times int) (*runRequest, error) {
   420  	var opts ipc.ExecOpts
   421  	sandboxFlags, err := ipc.SandboxToFlags(sandbox)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  	opts.EnvFlags |= sandboxFlags
   426  	if threaded {
   427  		opts.ExecFlags |= flatrpc.ExecFlagThreaded
   428  	}
   429  	if cov {
   430  		opts.EnvFlags |= flatrpc.ExecEnvSignal
   431  		opts.ExecFlags |= flatrpc.ExecFlagCollectSignal
   432  		opts.ExecFlags |= flatrpc.ExecFlagCollectCover
   433  	}
   434  	opts.EnvFlags |= ipc.FeaturesToFlags(ctx.Features, nil)
   435  	if ctx.Debug {
   436  		opts.EnvFlags |= flatrpc.ExecEnvDebug
   437  	}
   438  	req := &runRequest{
   439  		Request: &queue.Request{
   440  			Prog:     p,
   441  			ExecOpts: opts,
   442  			Repeat:   times,
   443  		},
   444  	}
   445  	return req, nil
   446  }
   447  
   448  func (ctx *Context) createCTest(p *prog.Prog, sandbox string, threaded bool, times int) (*runRequest, error) {
   449  	opts := csource.Options{
   450  		Threaded:    threaded,
   451  		Repeat:      times > 1,
   452  		RepeatTimes: times,
   453  		Procs:       1,
   454  		Slowdown:    1,
   455  		Sandbox:     sandbox,
   456  		UseTmpDir:   true,
   457  		HandleSegv:  true,
   458  		Cgroups:     p.Target.OS == targets.Linux && sandbox != "",
   459  		Trace:       true,
   460  		Swap:        ctx.Features&flatrpc.FeatureSwap != 0,
   461  	}
   462  	if sandbox != "" {
   463  		if ctx.Features&flatrpc.FeatureNetInjection != 0 {
   464  			opts.NetInjection = true
   465  		}
   466  		if ctx.Features&flatrpc.FeatureNetDevices != 0 {
   467  			opts.NetDevices = true
   468  		}
   469  		if ctx.Features&flatrpc.FeatureVhciInjection != 0 {
   470  			opts.VhciInjection = true
   471  		}
   472  		if ctx.Features&flatrpc.FeatureWifiEmulation != 0 {
   473  			opts.Wifi = true
   474  		}
   475  		if ctx.Features&flatrpc.FeatureLRWPANEmulation != 0 {
   476  			opts.IEEE802154 = true
   477  		}
   478  	}
   479  	src, err := csource.Write(p, opts)
   480  	if err != nil {
   481  		return nil, fmt.Errorf("failed to create C source: %w", err)
   482  	}
   483  	bin, err := csource.Build(p.Target, src)
   484  	if err != nil {
   485  		return nil, fmt.Errorf("failed to build C program: %w", err)
   486  	}
   487  	var ipcFlags flatrpc.ExecFlag
   488  	if threaded {
   489  		ipcFlags |= flatrpc.ExecFlagThreaded
   490  	}
   491  	req := &runRequest{
   492  		Request: &queue.Request{
   493  			Prog:       p,
   494  			BinaryFile: bin,
   495  			ExecOpts: ipc.ExecOpts{
   496  				ExecFlags: ipcFlags,
   497  			},
   498  			Repeat: times,
   499  		},
   500  	}
   501  	return req, nil
   502  }
   503  
   504  func checkResult(req *runRequest) error {
   505  	if req.result.Status != queue.Success {
   506  		return fmt.Errorf("non-successful result status (%v)", req.result.Status)
   507  	}
   508  	var infos []ipc.ProgInfo
   509  	isC := req.BinaryFile != ""
   510  	if isC {
   511  		var err error
   512  		if infos, err = parseBinOutput(req); err != nil {
   513  			return err
   514  		}
   515  	} else {
   516  		raw := req.result.Info
   517  		for len(raw.Calls) != 0 {
   518  			ncalls := min(len(raw.Calls), len(req.Prog.Calls))
   519  			infos = append(infos, ipc.ProgInfo{
   520  				Extra: raw.Extra,
   521  				Calls: raw.Calls[:ncalls],
   522  			})
   523  			raw.Calls = raw.Calls[ncalls:]
   524  		}
   525  	}
   526  	if req.Repeat != len(infos) {
   527  		return fmt.Errorf("should repeat %v times, but repeated %v, prog calls %v, info calls %v\n%s",
   528  			req.Repeat, len(infos), req.Prog.Calls, len(req.result.Info.Calls), req.result.Output)
   529  	}
   530  	calls := make(map[string]bool)
   531  	for run, info := range infos {
   532  		for call := range info.Calls {
   533  			if err := checkCallResult(req, isC, run, call, info, calls); err != nil {
   534  				return err
   535  			}
   536  		}
   537  	}
   538  	return nil
   539  }
   540  
   541  func checkCallResult(req *runRequest, isC bool, run, call int, info ipc.ProgInfo, calls map[string]bool) error {
   542  	inf := info.Calls[call]
   543  	want := req.results.Calls[call]
   544  	for flag, what := range map[ipc.CallFlags]string{
   545  		ipc.CallExecuted: "executed",
   546  		ipc.CallBlocked:  "blocked",
   547  		ipc.CallFinished: "finished",
   548  	} {
   549  		if flag != ipc.CallFinished {
   550  			if isC {
   551  				// C code does not detect blocked/non-finished calls.
   552  				continue
   553  			}
   554  			if req.ExecOpts.ExecFlags&flatrpc.ExecFlagThreaded == 0 {
   555  				// In non-threaded mode blocked syscalls will block main thread
   556  				// and we won't detect blocked/unfinished syscalls.
   557  				continue
   558  			}
   559  		}
   560  		if runtime.GOOS == targets.FreeBSD && flag == ipc.CallBlocked {
   561  			// Blocking detection is flaky on freebsd.
   562  			// TODO(dvyukov): try to increase the timeout in executor to make it non-flaky.
   563  			continue
   564  		}
   565  		if (inf.Flags^want.Flags)&flag != 0 {
   566  			not := " not"
   567  			if inf.Flags&flag != 0 {
   568  				not = ""
   569  			}
   570  			return fmt.Errorf("run %v: call %v is%v %v", run, call, not, what)
   571  		}
   572  	}
   573  	if inf.Flags&ipc.CallFinished != 0 && inf.Errno != want.Errno {
   574  		return fmt.Errorf("run %v: wrong call %v result %v, want %v",
   575  			run, call, inf.Errno, want.Errno)
   576  	}
   577  	if isC || inf.Flags&ipc.CallExecuted == 0 {
   578  		return nil
   579  	}
   580  	if req.ExecOpts.EnvFlags&flatrpc.ExecEnvSignal != 0 {
   581  		// Signal is always deduplicated, so we may not get any signal
   582  		// on a second invocation of the same syscall.
   583  		// For calls that are not meant to collect synchronous coverage we
   584  		// allow the signal to be empty as long as the extra signal is not.
   585  		callName := req.Prog.Calls[call].Meta.CallName
   586  		if len(inf.Signal) < 2 && !calls[callName] && len(info.Extra.Signal) == 0 {
   587  			return fmt.Errorf("run %v: call %v: no signal", run, call)
   588  		}
   589  		// syz_btf_id_by_name is a pseudo-syscall that might not provide
   590  		// any coverage when invoked.
   591  		if len(inf.Cover) == 0 && callName != "syz_btf_id_by_name" {
   592  			return fmt.Errorf("run %v: call %v: no cover", run, call)
   593  		}
   594  		calls[callName] = true
   595  	} else {
   596  		if len(inf.Signal) != 0 {
   597  			return fmt.Errorf("run %v: call %v: got %v unwanted signal", run, call, len(inf.Signal))
   598  		}
   599  	}
   600  	return nil
   601  }
   602  
   603  func parseBinOutput(req *runRequest) ([]ipc.ProgInfo, error) {
   604  	var infos []ipc.ProgInfo
   605  	s := bufio.NewScanner(bytes.NewReader(req.result.Output))
   606  	re := regexp.MustCompile("^### call=([0-9]+) errno=([0-9]+)$")
   607  	for s.Scan() {
   608  		if s.Text() == "### start" {
   609  			infos = append(infos, ipc.ProgInfo{Calls: make([]ipc.CallInfo, len(req.Prog.Calls))})
   610  		}
   611  		match := re.FindSubmatch(s.Bytes())
   612  		if match == nil {
   613  			continue
   614  		}
   615  		if len(infos) == 0 {
   616  			return nil, fmt.Errorf("call completed without start")
   617  		}
   618  		call, err := strconv.ParseUint(string(match[1]), 10, 64)
   619  		if err != nil {
   620  			return nil, fmt.Errorf("failed to parse call %q in %q",
   621  				string(match[1]), s.Text())
   622  		}
   623  		errno, err := strconv.ParseUint(string(match[2]), 10, 32)
   624  		if err != nil {
   625  			return nil, fmt.Errorf("failed to parse errno %q in %q",
   626  				string(match[2]), s.Text())
   627  		}
   628  		info := &infos[len(infos)-1]
   629  		if call >= uint64(len(info.Calls)) {
   630  			return nil, fmt.Errorf("bad call index %v", call)
   631  		}
   632  		if info.Calls[call].Flags != 0 {
   633  			return nil, fmt.Errorf("double result for call %v", call)
   634  		}
   635  		info.Calls[call].Flags |= ipc.CallExecuted | ipc.CallFinished
   636  		info.Calls[call].Errno = int(errno)
   637  	}
   638  	return infos, nil
   639  }