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 }