github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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  	"errors"
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"runtime"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    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/manager"
    31  	"github.com/google/syzkaller/prog"
    32  	"github.com/google/syzkaller/sys/targets"
    33  )
    34  
    35  // Pseudo-syscalls that might not provide any coverage when invoked.
    36  var noCovSyscalls = map[string]struct{}{
    37  	"syz_btf_id_by_name":            {},
    38  	"syz_kvm_assert_syzos_uexit":    {},
    39  	"syz_kvm_assert_syzos_kvm_exit": {},
    40  }
    41  
    42  type runRequest struct {
    43  	*queue.Request
    44  	sourceOpts *csource.Options
    45  	executor   queue.Executor
    46  
    47  	ok      int
    48  	failed  int
    49  	err     error
    50  	result  *queue.Result
    51  	results *flatrpc.ProgInfo // the expected results
    52  	repeat  int               // only relevant for C tests
    53  
    54  	name    string
    55  	broken  string
    56  	failing string
    57  	skip    string
    58  }
    59  
    60  type Context struct {
    61  	Dir          string
    62  	Target       *prog.Target
    63  	Features     flatrpc.Feature
    64  	EnabledCalls map[string]map[*prog.Syscall]bool
    65  	LogFunc      func(text string)
    66  	Retries      int // max number of test retries to deal with flaky tests
    67  	Verbose      bool
    68  	Debug        bool
    69  	Tests        string // prefix to match test file names
    70  
    71  	executor *queue.DynamicOrderer
    72  	requests []*runRequest
    73  	buildSem chan bool
    74  }
    75  
    76  func (ctx *Context) Init() {
    77  	// Run usually runs in a separate goroutine concurrently with request consumer (Next calls),
    78  	// so at least executor needs to be initialized before Run.
    79  	ctx.executor = queue.DynamicOrder()
    80  	ctx.buildSem = make(chan bool, runtime.GOMAXPROCS(0))
    81  }
    82  
    83  func (ctx *Context) log(msg string, args ...interface{}) {
    84  	ctx.LogFunc(fmt.Sprintf(msg, args...))
    85  }
    86  
    87  func (ctx *Context) Run(waitCtx context.Context) error {
    88  	ctx.generatePrograms()
    89  	var ok, fail, broken, skip int
    90  	for _, req := range ctx.requests {
    91  		result := ""
    92  		verbose := false
    93  		if req.broken != "" {
    94  			broken++
    95  			result = fmt.Sprintf("BROKEN (%v)", req.broken)
    96  			verbose = true
    97  		} else if req.failing != "" {
    98  			fail++
    99  			result = fmt.Sprintf("FAIL (%v)", req.failing)
   100  			verbose = true
   101  		} else if req.skip != "" {
   102  			skip++
   103  			result = fmt.Sprintf("SKIP (%v)", req.skip)
   104  			verbose = true
   105  		} else {
   106  			req.Request.Wait(waitCtx)
   107  			if req.err != nil {
   108  				fail++
   109  				result = fmt.Sprintf("FAIL: %v",
   110  					strings.ReplaceAll(req.err.Error(), "\n", "\n\t"))
   111  				if req.result != nil && len(req.result.Output) != 0 {
   112  					result += fmt.Sprintf("\n\t%s",
   113  						strings.ReplaceAll(string(req.result.Output), "\n", "\n\t"))
   114  				}
   115  			} else {
   116  				ok++
   117  				result = "OK"
   118  			}
   119  		}
   120  		if !verbose || ctx.Verbose {
   121  			ctx.log("%-38v: %v", req.name, result)
   122  		}
   123  		if req.Request != nil && req.Type == flatrpc.RequestTypeBinary && req.BinaryFile != "" {
   124  			os.Remove(req.BinaryFile)
   125  		}
   126  	}
   127  	ctx.log("ok: %v, broken: %v, skip: %v, fail: %v", ok, broken, skip, fail)
   128  	if fail != 0 {
   129  		return fmt.Errorf("tests failed")
   130  	}
   131  	return nil
   132  }
   133  
   134  func (ctx *Context) Next() *queue.Request {
   135  	return ctx.executor.Next()
   136  }
   137  
   138  func (ctx *Context) onDone(req *runRequest, res *queue.Result) bool {
   139  	// The tests depend on timings and may be flaky, esp on overloaded/slow machines.
   140  	// We don't want to fix this by significantly bumping all timeouts,
   141  	// because if a program fails all the time with the default timeouts,
   142  	// it will also fail during fuzzing. And we want to ensure that it's not the case.
   143  	// So what we want is to tolerate episodic failures with the default timeouts.
   144  	// To achieve this we run each test several times and ensure that it passes
   145  	// in 50+% of cases (i.e. 1/1, 2/3, 3/5, 4/7, etc).
   146  	// In the best case this allows to get off with just 1 test run.
   147  	if res.Err != nil {
   148  		req.err = res.Err
   149  		return true
   150  	}
   151  	req.result = res
   152  	err := checkResult(req)
   153  	if err == nil {
   154  		req.ok++
   155  	} else {
   156  		req.failed++
   157  		req.err = err
   158  	}
   159  	if req.ok > req.failed {
   160  		// There are more successful than failed runs.
   161  		req.err = nil
   162  		return true
   163  	}
   164  	// We need at least `failed - ok + 1` more runs <=> `failed + ok + need` in total,
   165  	// which simplifies to `failed * 2 + 1`.
   166  	retries := ctx.Retries
   167  	if retries%2 == 0 {
   168  		retries++
   169  	}
   170  	if req.failed*2+1 <= retries {
   171  		// We can still retry the execution.
   172  		ctx.submit(req)
   173  		return false
   174  	}
   175  	// Give up and fail on this request.
   176  	return true
   177  }
   178  
   179  func (ctx *Context) generatePrograms() error {
   180  	cover := []bool{false}
   181  	if ctx.Features&flatrpc.FeatureCoverage != 0 {
   182  		cover = append(cover, true)
   183  	}
   184  	var sandboxes []string
   185  	for sandbox := range ctx.EnabledCalls {
   186  		sandboxes = append(sandboxes, sandbox)
   187  	}
   188  	sort.Strings(sandboxes)
   189  	files, err := progFileList(ctx.Dir, ctx.Tests)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	for _, file := range files {
   194  		if err := ctx.generateFile(sandboxes, cover, file); err != nil {
   195  			// Treat invalid programs as failing.
   196  			ctx.createTest(&runRequest{
   197  				name:    file,
   198  				failing: err.Error(),
   199  			})
   200  			continue
   201  		}
   202  	}
   203  	return nil
   204  }
   205  
   206  func progFileList(dir, filter string) ([]string, error) {
   207  	files, err := os.ReadDir(dir)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("failed to read %v: %w", dir, err)
   210  	}
   211  	var res []string
   212  	for _, file := range files {
   213  		if strings.HasSuffix(file.Name(), "~") ||
   214  			strings.HasSuffix(file.Name(), ".swp") ||
   215  			!strings.HasPrefix(file.Name(), filter) {
   216  			continue
   217  		}
   218  		res = append(res, file.Name())
   219  	}
   220  	return res, nil
   221  }
   222  
   223  func (ctx *Context) generateFile(sandboxes []string, cover []bool, filename string) error {
   224  	p, requires, results, err := parseProg(ctx.Target, ctx.Dir, filename, nil)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	if p == nil {
   229  		return nil
   230  	}
   231  	sysTarget := targets.Get(ctx.Target.OS, ctx.Target.Arch)
   232  nextSandbox:
   233  	for _, sandbox := range sandboxes {
   234  		name := fmt.Sprintf("%v %v", filename, sandbox)
   235  		for _, call := range p.Calls {
   236  			if !ctx.EnabledCalls[sandbox][call.Meta] {
   237  				ctx.createTest(&runRequest{
   238  					name: name,
   239  					skip: fmt.Sprintf("unsupported call %v", call.Meta.Name),
   240  				})
   241  				continue nextSandbox
   242  			}
   243  		}
   244  		properties := map[string]bool{
   245  			"manual":                  ctx.Tests != "", // "manual" tests run only if selected by the filter explicitly.
   246  			"sandbox=" + sandbox:      true,
   247  			"bigendian":               sysTarget.BigEndian,
   248  			"arch=" + ctx.Target.Arch: true,
   249  		}
   250  		for _, threaded := range []bool{false, true} {
   251  			if threaded {
   252  				name += "/thr"
   253  			}
   254  			properties["threaded"] = threaded
   255  			for _, times := range []int{1, 3} {
   256  				properties["repeat"] = times > 1
   257  				properties["norepeat"] = times <= 1
   258  				if times > 1 {
   259  					name += "/repeat"
   260  				}
   261  				for _, cov := range cover {
   262  					if sandbox == "" {
   263  						break // executor does not support empty sandbox
   264  					}
   265  					if times != 1 {
   266  						break
   267  					}
   268  					if cov {
   269  						name += "/cover"
   270  					}
   271  					properties["cover"] = cov
   272  					properties["C"] = false
   273  					properties["executor"] = true
   274  					req, err := ctx.createSyzTest(p, sandbox, threaded, cov)
   275  					if err != nil {
   276  						return err
   277  					}
   278  					ctx.produceTest(req, name, properties, requires, results)
   279  				}
   280  				if sysTarget.HostFuzzer {
   281  					// For HostFuzzer mode, we need to cross-compile
   282  					// and copy the binary to the target system.
   283  					continue
   284  				}
   285  				properties["C"] = true
   286  				properties["executor"] = false
   287  				name += " C"
   288  				if !sysTarget.ExecutorUsesForkServer && times > 1 {
   289  					// Non-fork loop implementation does not support repetition.
   290  					ctx.createTest(&runRequest{
   291  						name:   name,
   292  						broken: "non-forking loop",
   293  					})
   294  					continue
   295  				}
   296  				req, err := ctx.createCTest(p, sandbox, threaded, times)
   297  				if err != nil {
   298  					return err
   299  				}
   300  				ctx.produceTest(req, name, properties, requires, results)
   301  			}
   302  		}
   303  	}
   304  	return nil
   305  }
   306  
   307  func parseProg(target *prog.Target, dir, filename string, requires map[string]bool) (
   308  	*prog.Prog, map[string]bool, *flatrpc.ProgInfo, error) {
   309  	data, err := os.ReadFile(filepath.Join(dir, filename))
   310  	if err != nil {
   311  		return nil, nil, nil, fmt.Errorf("failed to read %v: %w", filename, err)
   312  	}
   313  	p, properties, err := manager.ParseSeedWithRequirements(target, data, requires)
   314  	if errors.Is(err, manager.ErrSkippedTest) {
   315  		return nil, nil, nil, nil
   316  	}
   317  	if err != nil {
   318  		return nil, nil, nil, fmt.Errorf("failed to deserialize %v: %w", filename, err)
   319  	}
   320  	errnos := map[string]int32{
   321  		"":           0,
   322  		"EPERM":      1,
   323  		"ENOENT":     2,
   324  		"E2BIG":      7,
   325  		"ENOEXEC":    8,
   326  		"EBADF":      9,
   327  		"ENOMEM":     12,
   328  		"EACCES":     13,
   329  		"EFAULT":     14,
   330  		"EXDEV":      18,
   331  		"EINVAL":     22,
   332  		"ENOTTY":     25,
   333  		"EOPNOTSUPP": 95,
   334  
   335  		// Fuchsia specific errors.
   336  		"ZX_ERR_NO_RESOURCES":   3,
   337  		"ZX_ERR_INVALID_ARGS":   10,
   338  		"ZX_ERR_BAD_HANDLE":     11,
   339  		"ZX_ERR_BAD_STATE":      20,
   340  		"ZX_ERR_TIMED_OUT":      21,
   341  		"ZX_ERR_SHOULD_WAIT":    22,
   342  		"ZX_ERR_PEER_CLOSED":    24,
   343  		"ZX_ERR_ALREADY_EXISTS": 26,
   344  		"ZX_ERR_ACCESS_DENIED":  30,
   345  	}
   346  	info := &flatrpc.ProgInfo{}
   347  	for _, call := range p.Calls {
   348  		ci := &flatrpc.CallInfo{
   349  			Flags: flatrpc.CallFlagExecuted | flatrpc.CallFlagFinished,
   350  		}
   351  		switch call.Comment {
   352  		case "blocked":
   353  			ci.Flags |= flatrpc.CallFlagBlocked
   354  		case "unfinished":
   355  			ci.Flags &^= flatrpc.CallFlagFinished
   356  		case "unexecuted":
   357  			ci.Flags &^= flatrpc.CallFlagExecuted | flatrpc.CallFlagFinished
   358  		default:
   359  			res, ok := errnos[call.Comment]
   360  			if !ok {
   361  				return nil, nil, nil, fmt.Errorf("%v: unknown call comment %q",
   362  					filename, call.Comment)
   363  			}
   364  			ci.Error = res
   365  		}
   366  		info.Calls = append(info.Calls, ci)
   367  	}
   368  	return p, properties, info, nil
   369  }
   370  
   371  func (ctx *Context) produceTest(req *runRequest, name string, properties,
   372  	requires map[string]bool, results *flatrpc.ProgInfo) {
   373  	req.name = name
   374  	req.results = results
   375  	if !manager.MatchRequirements(properties, requires) {
   376  		req.skip = "excluded by constraints"
   377  	}
   378  	ctx.createTest(req)
   379  }
   380  
   381  func (ctx *Context) createTest(req *runRequest) {
   382  	req.executor = ctx.executor.Append()
   383  	ctx.requests = append(ctx.requests, req)
   384  	if req.skip != "" || req.broken != "" || req.failing != "" {
   385  		return
   386  	}
   387  	if req.sourceOpts == nil {
   388  		ctx.submit(req)
   389  		return
   390  	}
   391  	go func() {
   392  		ctx.buildSem <- true
   393  		defer func() {
   394  			<-ctx.buildSem
   395  		}()
   396  		src, err := csource.Write(req.Prog, *req.sourceOpts)
   397  		if err != nil {
   398  			req.err = fmt.Errorf("failed to create C source: %w", err)
   399  			req.Request.Done(&queue.Result{})
   400  		}
   401  		bin, err := csource.Build(ctx.Target, src)
   402  		if err != nil {
   403  			req.err = fmt.Errorf("failed to build C program: %w", err)
   404  			req.Request.Done(&queue.Result{})
   405  			return
   406  		}
   407  		req.Type = flatrpc.RequestTypeBinary
   408  		req.BinaryFile = bin
   409  		ctx.submit(req)
   410  	}()
   411  }
   412  
   413  func (ctx *Context) submit(req *runRequest) {
   414  	req.OnDone(func(_ *queue.Request, res *queue.Result) bool {
   415  		return ctx.onDone(req, res)
   416  	})
   417  	req.executor.Submit(req.Request)
   418  }
   419  
   420  func (ctx *Context) createSyzTest(p *prog.Prog, sandbox string, threaded, cov bool) (*runRequest, error) {
   421  	var opts flatrpc.ExecOpts
   422  	sandboxFlags, err := flatrpc.SandboxToFlags(sandbox)
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  	opts.EnvFlags |= sandboxFlags
   427  	if threaded {
   428  		opts.ExecFlags |= flatrpc.ExecFlagThreaded
   429  	}
   430  	if cov {
   431  		opts.EnvFlags |= flatrpc.ExecEnvSignal
   432  		opts.ExecFlags |= flatrpc.ExecFlagCollectSignal
   433  		opts.ExecFlags |= flatrpc.ExecFlagCollectCover
   434  	}
   435  	opts.EnvFlags |= csource.FeaturesToFlags(ctx.Features, nil)
   436  	if ctx.Debug {
   437  		opts.EnvFlags |= flatrpc.ExecEnvDebug
   438  	}
   439  	req := &runRequest{
   440  		Request: &queue.Request{
   441  			Prog:     p,
   442  			ExecOpts: opts,
   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  	var ipcFlags flatrpc.ExecFlag
   480  	if threaded {
   481  		ipcFlags |= flatrpc.ExecFlagThreaded
   482  	}
   483  	req := &runRequest{
   484  		sourceOpts: &opts,
   485  		Request: &queue.Request{
   486  			Prog: p,
   487  			ExecOpts: flatrpc.ExecOpts{
   488  				ExecFlags: ipcFlags,
   489  			},
   490  		},
   491  		repeat: times,
   492  	}
   493  	return req, nil
   494  }
   495  
   496  func checkResult(req *runRequest) error {
   497  	if req.result.Status != queue.Success {
   498  		return fmt.Errorf("non-successful result status (%v)", req.result.Status)
   499  	}
   500  	infos := []*flatrpc.ProgInfo{req.result.Info}
   501  	isC := req.Type == flatrpc.RequestTypeBinary
   502  	if isC {
   503  		var err error
   504  		if infos, err = parseBinOutput(req); err != nil {
   505  			return err
   506  		}
   507  		if req.repeat != len(infos) {
   508  			infoCalls := -1
   509  			if req.result.Info != nil {
   510  				infoCalls = len(req.result.Info.Calls)
   511  			}
   512  			return fmt.Errorf("should repeat %v times, but repeated %v, prog calls %v, info calls %v\n%s",
   513  				req.repeat, len(infos), req.Prog.Calls, infoCalls, req.result.Output)
   514  		}
   515  	}
   516  	calls := make(map[string]bool)
   517  	for run, info := range infos {
   518  		for call := range info.Calls {
   519  			if err := checkCallResult(req, isC, run, call, info, calls); err != nil {
   520  				return err
   521  			}
   522  		}
   523  	}
   524  	return nil
   525  }
   526  
   527  func checkCallResult(req *runRequest, isC bool, run, call int, info *flatrpc.ProgInfo, calls map[string]bool) error {
   528  	inf := info.Calls[call]
   529  	want := req.results.Calls[call]
   530  	if err := checkCallStatus(req, isC, run, call, inf, want); err != nil {
   531  		return err
   532  	}
   533  	if isC || inf.Flags&flatrpc.CallFlagExecuted == 0 {
   534  		return nil
   535  	}
   536  	// We check coverage only for syz-executor.
   537  	return checkCallCoverage(req, run, call, info, calls)
   538  }
   539  
   540  func checkCallStatus(req *runRequest, isC bool, run, call int, inf, want *flatrpc.CallInfo) error {
   541  	// In non-threaded mode blocked syscalls will block the main thread
   542  	// and we won't detect blocked/unfinished syscalls.
   543  	// C code also does not detect blocked/non-finished calls.
   544  	ignoreFlags := isC || req.ExecOpts.ExecFlags&flatrpc.ExecFlagThreaded == 0
   545  	for flag, what := range flatrpc.EnumNamesCallFlag {
   546  		if ignoreFlags && flag != flatrpc.CallFlagFinished {
   547  			continue
   548  		}
   549  		if runtime.GOOS == targets.FreeBSD && flag == flatrpc.CallFlagBlocked {
   550  			// Blocking detection is flaky on freebsd.
   551  			// TODO(dvyukov): try to increase the timeout in executor to make it non-flaky.
   552  			continue
   553  		}
   554  		if (inf.Flags^want.Flags)&flag != 0 {
   555  			return fmt.Errorf("run %v: call %v %v %v, want %v",
   556  				run, call, flagStatus(inf.Flags, flag), what, flagStatus(want.Flags, flag))
   557  		}
   558  	}
   559  	if inf.Flags&flatrpc.CallFlagFinished != 0 && inf.Error != want.Error {
   560  		return fmt.Errorf("run %v: wrong call %v result %v, want %v",
   561  			run, call, inf.Error, want.Error)
   562  	}
   563  	return nil
   564  }
   565  
   566  func flagStatus(flags, flag flatrpc.CallFlag) string {
   567  	if flags&flag != 0 {
   568  		return "is"
   569  	}
   570  	return "is not"
   571  }
   572  
   573  func checkCallCoverage(req *runRequest, run, call int, info *flatrpc.ProgInfo, calls map[string]bool) error {
   574  	inf := info.Calls[call]
   575  	if req.ExecOpts.EnvFlags&flatrpc.ExecEnvSignal == 0 {
   576  		if len(inf.Signal) != 0 {
   577  			return fmt.Errorf("run %v: call %v: got %v unwanted signal", run, call, len(inf.Signal))
   578  		}
   579  		return nil
   580  	}
   581  	callName := req.Prog.Calls[call].Meta.CallName
   582  	_, isNoCov := noCovSyscalls[callName]
   583  	// Signal is always deduplicated, so we may not get any signal
   584  	// on a second invocation of the same syscall.
   585  	// For calls that are not meant to collect synchronous coverage we
   586  	// allow the signal to be empty as long as the extra signal is not.
   587  	if !isNoCov && !calls[callName] {
   588  		if len(inf.Signal) < 2 && len(info.Extra.Signal) == 0 {
   589  			return fmt.Errorf("run %v: call %v: no signal", run, call)
   590  		}
   591  	}
   592  	if !isNoCov && len(inf.Cover) == 0 {
   593  		return fmt.Errorf("run %v: call %v: no cover", run, call)
   594  	}
   595  	calls[callName] = true
   596  	return nil
   597  }
   598  
   599  func parseBinOutput(req *runRequest) ([]*flatrpc.ProgInfo, error) {
   600  	var infos []*flatrpc.ProgInfo
   601  	s := bufio.NewScanner(bytes.NewReader(req.result.Output))
   602  	re := regexp.MustCompile("^### call=([0-9]+) errno=([0-9]+)$")
   603  	for s.Scan() {
   604  		if s.Text() == "### start" {
   605  			pi := &flatrpc.ProgInfo{}
   606  			for range req.Prog.Calls {
   607  				pi.Calls = append(pi.Calls, &flatrpc.CallInfo{})
   608  			}
   609  			infos = append(infos, pi)
   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 |= flatrpc.CallFlagExecuted | flatrpc.CallFlagFinished
   636  		info.Calls[call].Error = int32(errno)
   637  	}
   638  	return infos, nil
   639  }