github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/vm/gvisor/gvisor.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 gvisor provides support for gVisor, user-space kernel, testing. 5 // See https://github.com/google/gvisor 6 package gvisor 7 8 import ( 9 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "net" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "runtime" 18 "strings" 19 "syscall" 20 "time" 21 22 "github.com/google/syzkaller/pkg/config" 23 "github.com/google/syzkaller/pkg/log" 24 "github.com/google/syzkaller/pkg/osutil" 25 "github.com/google/syzkaller/pkg/report" 26 "github.com/google/syzkaller/sys/targets" 27 "github.com/google/syzkaller/vm/vmimpl" 28 ) 29 30 func init() { 31 vmimpl.Register(targets.GVisor, vmimpl.Type{ 32 Ctor: ctor, 33 Overcommit: true, 34 }) 35 } 36 37 type Config struct { 38 Count int `json:"count"` // number of VMs to use 39 RunscArgs string `json:"runsc_args"` 40 MemoryTotalBytes uint64 `json:"memory_total_bytes"` 41 CPUs uint64 `json:"cpus"` 42 } 43 44 type Pool struct { 45 env *vmimpl.Env 46 cfg *Config 47 } 48 49 type instance struct { 50 cfg *Config 51 image string 52 debug bool 53 rootDir string 54 imageDir string 55 name string 56 port int 57 cmd *exec.Cmd 58 merger *vmimpl.OutputMerger 59 } 60 61 func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { 62 cfg := &Config{ 63 Count: 1, 64 } 65 if err := config.LoadData(env.Config, cfg); err != nil { 66 return nil, fmt.Errorf("failed to parse vm config: %w", err) 67 } 68 if cfg.Count < 1 || cfg.Count > 128 { 69 return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count) 70 } 71 hostTotalMemory := osutil.SystemMemorySize() 72 minMemory := uint64(cfg.Count) * 10_000_000 73 if cfg.MemoryTotalBytes != 0 && (cfg.MemoryTotalBytes < minMemory || cfg.MemoryTotalBytes > hostTotalMemory) { 74 return nil, fmt.Errorf("invalid config param memory_total_bytes: %v, want [%d,%d]", 75 minMemory, cfg.MemoryTotalBytes, hostTotalMemory) 76 } 77 if !osutil.IsExist(env.Image) { 78 return nil, fmt.Errorf("image file %q does not exist", env.Image) 79 } 80 pool := &Pool{ 81 cfg: cfg, 82 env: env, 83 } 84 return pool, nil 85 } 86 87 func (pool *Pool) Count() int { 88 return pool.cfg.Count 89 } 90 91 func (pool *Pool) Create(_ context.Context, workdir string, index int) (vmimpl.Instance, error) { 92 rootDir := filepath.Clean(filepath.Join(workdir, "..", "gvisor_root")) 93 imageDir := filepath.Join(workdir, "image") 94 bundleDir := filepath.Join(workdir, "bundle") 95 osutil.MkdirAll(rootDir) 96 osutil.MkdirAll(bundleDir) 97 osutil.MkdirAll(imageDir) 98 99 caps := "" 100 for _, c := range sandboxCaps { 101 if caps != "" { 102 caps += ", " 103 } 104 caps += "\"" + c + "\"" 105 } 106 name := fmt.Sprintf("%v-%v", pool.env.Name, index) 107 memoryLimit := int64(pool.cfg.MemoryTotalBytes / uint64(pool.Count())) 108 if pool.cfg.MemoryTotalBytes == 0 { 109 memoryLimit = -1 110 } 111 cpuPeriod := uint64(100000) 112 cpus := pool.cfg.CPUs 113 if cpus == 0 { 114 cpus = uint64(runtime.NumCPU()) 115 } 116 cpuLimit := cpuPeriod * cpus 117 vmConfig := fmt.Sprintf(configTempl, imageDir, caps, name, memoryLimit, cpuLimit, cpuPeriod) 118 if err := osutil.WriteFile(filepath.Join(bundleDir, "config.json"), []byte(vmConfig)); err != nil { 119 return nil, err 120 } 121 bin, err := exec.LookPath(os.Args[0]) 122 if err != nil { 123 return nil, fmt.Errorf("failed to lookup %v: %w", os.Args[0], err) 124 } 125 if err := osutil.CopyFile(bin, filepath.Join(imageDir, "init")); err != nil { 126 return nil, err 127 } 128 129 panicLog := filepath.Join(bundleDir, "panic.fifo") 130 if err := syscall.Mkfifo(panicLog, 0666); err != nil { 131 return nil, err 132 } 133 defer syscall.Unlink(panicLog) 134 135 // Open the fifo for read-write to be able to open for read-only 136 // without blocking. 137 panicLogWriteFD, err := os.OpenFile(panicLog, os.O_RDWR, 0) 138 if err != nil { 139 return nil, err 140 } 141 defer panicLogWriteFD.Close() 142 143 panicLogReadFD, err := os.Open(panicLog) 144 if err != nil { 145 return nil, err 146 } 147 148 rpipe, wpipe, err := osutil.LongPipe() 149 if err != nil { 150 panicLogReadFD.Close() 151 return nil, err 152 } 153 var tee io.Writer 154 if pool.env.Debug { 155 tee = os.Stdout 156 } 157 merger := vmimpl.NewOutputMerger(tee) 158 merger.Add("runsc", rpipe) 159 merger.Add("runsc-goruntime", panicLogReadFD) 160 161 inst := &instance{ 162 cfg: pool.cfg, 163 image: pool.env.Image, 164 debug: pool.env.Debug, 165 rootDir: rootDir, 166 imageDir: imageDir, 167 name: name, 168 merger: merger, 169 } 170 171 // Kill the previous instance in case it's still running. 172 osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name)) 173 time.Sleep(3 * time.Second) 174 175 cmd := inst.runscCmd("--panic-log", panicLog, "--cpu-num-from-quota", "run", "-bundle", bundleDir, inst.name) 176 cmd.Stdout = wpipe 177 cmd.Stderr = wpipe 178 if err := cmd.Start(); err != nil { 179 wpipe.Close() 180 panicLogWriteFD.Close() 181 merger.Wait() 182 return nil, err 183 } 184 inst.cmd = cmd 185 wpipe.Close() 186 187 if err := inst.waitBoot(); err != nil { 188 panicLogWriteFD.Close() 189 inst.Close() 190 return nil, err 191 } 192 return inst, nil 193 } 194 195 func (inst *instance) waitBoot() error { 196 errorMsg := []byte("FATAL ERROR:") 197 bootedMsg := []byte(initStartMsg) 198 timeout := time.NewTimer(time.Minute) 199 defer timeout.Stop() 200 var output []byte 201 for { 202 select { 203 case out := <-inst.merger.Output: 204 output = append(output, out...) 205 if pos := bytes.Index(output, errorMsg); pos != -1 { 206 end := bytes.IndexByte(output[pos:], '\n') 207 if end == -1 { 208 end = len(output) 209 } else { 210 end += pos 211 } 212 return vmimpl.BootError{ 213 Title: string(output[pos:end]), 214 Output: output, 215 } 216 } 217 if bytes.Contains(output, bootedMsg) { 218 return nil 219 } 220 case err := <-inst.merger.Err: 221 return vmimpl.BootError{ 222 Title: fmt.Sprintf("runsc failed: %v", err), 223 Output: output, 224 } 225 case <-timeout.C: 226 return vmimpl.BootError{ 227 Title: "init process did not start", 228 Output: output, 229 } 230 } 231 } 232 } 233 234 func (inst *instance) args() []string { 235 args := []string{ 236 "-root", inst.rootDir, 237 "-watchdog-action=panic", 238 "-network=none", 239 "-debug", 240 // Send debug logs to stderr, so that they will be picked up by 241 // syzkaller. Without this, debug logs are sent to /dev/null. 242 "-debug-log=/dev/stderr", 243 } 244 if inst.cfg.RunscArgs != "" { 245 args = append(args, strings.Split(inst.cfg.RunscArgs, " ")...) 246 } 247 return args 248 } 249 250 func (inst *instance) Info() ([]byte, error) { 251 info := fmt.Sprintf("%v %v\n", inst.image, strings.Join(inst.args(), " ")) 252 return []byte(info), nil 253 } 254 255 func (inst *instance) runscCmd(add ...string) *exec.Cmd { 256 cmd := osutil.Command(inst.image, append(inst.args(), add...)...) 257 cmd.Env = []string{ 258 "GOTRACEBACK=all", 259 "GORACE=halt_on_error=1", 260 // New glibc-s enable rseq by default but ptrace and systrap 261 // platforms don't work in this case. runsc is linked with libc 262 // only when the race detector is enabled. 263 "GLIBC_TUNABLES=glibc.pthread.rseq=0", 264 } 265 return cmd 266 } 267 268 func (inst *instance) Close() error { 269 time.Sleep(3 * time.Second) 270 osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name)) 271 inst.cmd.Process.Kill() 272 inst.merger.Wait() 273 inst.cmd.Wait() 274 osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name)) 275 time.Sleep(3 * time.Second) 276 return nil 277 } 278 279 func (inst *instance) Forward(port int) (string, error) { 280 if inst.port != 0 { 281 return "", fmt.Errorf("forward port is already setup") 282 } 283 inst.port = port 284 return "stdin:0", nil 285 } 286 287 func (inst *instance) Copy(hostSrc string) (string, error) { 288 fname := filepath.Base(hostSrc) 289 if err := osutil.CopyFile(hostSrc, filepath.Join(inst.imageDir, fname)); err != nil { 290 return "", err 291 } 292 if err := os.Chmod(inst.imageDir, 0777); err != nil { 293 return "", err 294 } 295 return filepath.Join("/", fname), nil 296 } 297 298 func (inst *instance) Run(ctx context.Context, command string) ( 299 <-chan []byte, <-chan error, error) { 300 args := []string{"exec", "-user=0:0"} 301 for _, c := range sandboxCaps { 302 args = append(args, "-cap", c) 303 } 304 args = append(args, inst.name) 305 args = append(args, strings.Split(command, " ")...) 306 cmd := inst.runscCmd(args...) 307 308 rpipe, wpipe, err := osutil.LongPipe() 309 if err != nil { 310 return nil, nil, err 311 } 312 defer wpipe.Close() 313 inst.merger.Add("cmd", rpipe) 314 cmd.Stdout = wpipe 315 cmd.Stderr = wpipe 316 317 guestSock, err := inst.guestProxy() 318 if err != nil { 319 return nil, nil, err 320 } 321 if guestSock != nil { 322 defer guestSock.Close() 323 cmd.Stdin = guestSock 324 } 325 326 if err := cmd.Start(); err != nil { 327 return nil, nil, err 328 } 329 errc := make(chan error, 1) 330 signal := func(err error) { 331 select { 332 case errc <- err: 333 default: 334 } 335 } 336 337 go func() { 338 select { 339 case <-ctx.Done(): 340 signal(vmimpl.ErrTimeout) 341 case err := <-inst.merger.Err: 342 cmd.Process.Kill() 343 if cmdErr := cmd.Wait(); cmdErr == nil { 344 // If the command exited successfully, we got EOF error from merger. 345 // But in this case no error has happened and the EOF is expected. 346 err = nil 347 } 348 signal(err) 349 return 350 } 351 log.Logf(1, "stopping %s", inst.name) 352 w := make(chan bool) 353 go func() { 354 select { 355 case <-w: 356 return 357 case <-time.After(time.Minute): 358 cmd.Process.Kill() 359 } 360 }() 361 osutil.Run(time.Minute, inst.runscCmd("kill", inst.name, "9")) 362 err := cmd.Wait() 363 close(w) 364 log.Logf(1, "%s exited with %s", inst.name, err) 365 }() 366 return inst.merger.Output, errc, nil 367 } 368 369 func (inst *instance) guestProxy() (*os.File, error) { 370 if inst.port == 0 { 371 return nil, nil 372 } 373 // One does not simply let gvisor guest connect to host tcp port. 374 // We create a unix socket, pass it to guest in stdin. 375 // Guest will use it instead of dialing manager directly. 376 // On host we connect to manager tcp port and proxy between the tcp and unix connections. 377 socks, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 378 if err != nil { 379 return nil, err 380 } 381 hostSock := os.NewFile(uintptr(socks[0]), "host unix proxy") 382 guestSock := os.NewFile(uintptr(socks[1]), "guest unix proxy") 383 conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", inst.port)) 384 if err != nil { 385 hostSock.Close() 386 guestSock.Close() 387 return nil, err 388 } 389 go func() { 390 io.Copy(hostSock, conn) 391 hostSock.Close() 392 }() 393 go func() { 394 io.Copy(conn, hostSock) 395 conn.Close() 396 }() 397 return guestSock, nil 398 } 399 400 func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { 401 // TODO: stacks and dmesg are mostly useful for hangs/stalls, so we could do this only sometimes based on rep. 402 b, err := osutil.Run(time.Minute, inst.runscCmd("debug", "-stacks", "--ps", inst.name)) 403 if err != nil { 404 b = append(b, fmt.Sprintf("\n\nError collecting stacks: %v", err)...) 405 } 406 b1, err := osutil.RunCmd(time.Minute, "", "dmesg") 407 b = append(b, b1...) 408 if err != nil { 409 b = append(b, fmt.Sprintf("\n\nError collecting kernel logs: %v", err)...) 410 } 411 return b, false 412 } 413 414 func init() { 415 if os.Getenv("SYZ_GVISOR_PROXY") != "" { 416 fmt.Fprint(os.Stderr, initStartMsg) 417 // If we do select{}, we can get a deadlock panic. 418 for range time.NewTicker(time.Hour).C { 419 } 420 } 421 } 422 423 const initStartMsg = "SYZKALLER INIT STARTED\n" 424 425 const configTempl = ` 426 { 427 "root": { 428 "path": "%[1]v", 429 "readonly": true 430 }, 431 "linux": { 432 "cgroupsPath": "%[3]v", 433 "resources": { 434 "cpu": { 435 "shares": 1024, 436 "period": %[6]d, 437 "quota": %[5]d 438 }, 439 "memory": { 440 "limit": %[4]d, 441 "reservation": %[4]d, 442 "disableOOMKiller": false 443 } 444 }, 445 "sysctl": { 446 "fs.nr_open": "1048576" 447 } 448 }, 449 "process":{ 450 "args": ["/init"], 451 "cwd": "/tmp", 452 "env": ["SYZ_GVISOR_PROXY=1"], 453 "capabilities": { 454 "bounding": [%[2]v], 455 "effective": [%[2]v], 456 "inheritable": [%[2]v], 457 "permitted": [%[2]v], 458 "ambient": [%[2]v] 459 } 460 } 461 } 462 ` 463 464 var sandboxCaps = []string{ 465 "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", "CAP_FOWNER", "CAP_FSETID", 466 "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_SETPCAP", "CAP_LINUX_IMMUTABLE", 467 "CAP_NET_BIND_SERVICE", "CAP_NET_BROADCAST", "CAP_NET_ADMIN", "CAP_NET_RAW", 468 "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_CHROOT", 469 "CAP_SYS_PTRACE", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_BOOT", "CAP_SYS_NICE", 470 "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_LEASE", 471 "CAP_AUDIT_WRITE", "CAP_AUDIT_CONTROL", "CAP_SETFCAP", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN", 472 "CAP_SYSLOG", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", "CAP_AUDIT_READ", 473 }