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 }