github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/integration/execin_test.go (about) 1 package integration 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "strconv" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/containerd/console" 14 "github.com/opencontainers/runc/libcontainer" 15 "github.com/opencontainers/runc/libcontainer/configs" 16 "github.com/opencontainers/runc/libcontainer/utils" 17 18 "golang.org/x/sys/unix" 19 ) 20 21 func TestExecIn(t *testing.T) { 22 if testing.Short() { 23 return 24 } 25 config := newTemplateConfig(t, nil) 26 container, err := newContainer(t, config) 27 ok(t, err) 28 defer destroyContainer(container) 29 30 // Execute a first process in the container 31 stdinR, stdinW, err := os.Pipe() 32 ok(t, err) 33 process := &libcontainer.Process{ 34 Cwd: "/", 35 Args: []string{"cat"}, 36 Env: standardEnvironment, 37 Stdin: stdinR, 38 Init: true, 39 } 40 err = container.Run(process) 41 _ = stdinR.Close() 42 defer stdinW.Close() //nolint: errcheck 43 ok(t, err) 44 45 buffers := newStdBuffers() 46 ps := &libcontainer.Process{ 47 Cwd: "/", 48 Args: []string{"ps"}, 49 Env: standardEnvironment, 50 Stdin: buffers.Stdin, 51 Stdout: buffers.Stdout, 52 Stderr: buffers.Stderr, 53 } 54 55 err = container.Run(ps) 56 ok(t, err) 57 waitProcess(ps, t) 58 _ = stdinW.Close() 59 waitProcess(process, t) 60 61 out := buffers.Stdout.String() 62 if !strings.Contains(out, "cat") || !strings.Contains(out, "ps") { 63 t.Fatalf("unexpected running process, output %q", out) 64 } 65 if strings.Contains(out, "\r") { 66 t.Fatalf("unexpected carriage-return in output %q", out) 67 } 68 } 69 70 func TestExecInUsernsRlimit(t *testing.T) { 71 if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { 72 t.Skip("Test requires userns.") 73 } 74 75 testExecInRlimit(t, true) 76 } 77 78 func TestExecInRlimit(t *testing.T) { 79 testExecInRlimit(t, false) 80 } 81 82 func testExecInRlimit(t *testing.T, userns bool) { 83 if testing.Short() { 84 return 85 } 86 87 config := newTemplateConfig(t, &tParam{userns: userns}) 88 container, err := newContainer(t, config) 89 ok(t, err) 90 defer destroyContainer(container) 91 92 stdinR, stdinW, err := os.Pipe() 93 ok(t, err) 94 process := &libcontainer.Process{ 95 Cwd: "/", 96 Args: []string{"cat"}, 97 Env: standardEnvironment, 98 Stdin: stdinR, 99 Init: true, 100 } 101 err = container.Run(process) 102 _ = stdinR.Close() 103 defer stdinW.Close() //nolint: errcheck 104 ok(t, err) 105 106 buffers := newStdBuffers() 107 ps := &libcontainer.Process{ 108 Cwd: "/", 109 Args: []string{"/bin/sh", "-c", "ulimit -n"}, 110 Env: standardEnvironment, 111 Stdin: buffers.Stdin, 112 Stdout: buffers.Stdout, 113 Stderr: buffers.Stderr, 114 Rlimits: []configs.Rlimit{ 115 // increase process rlimit higher than container rlimit to test per-process limit 116 {Type: unix.RLIMIT_NOFILE, Hard: 1026, Soft: 1026}, 117 }, 118 Init: true, 119 } 120 err = container.Run(ps) 121 ok(t, err) 122 waitProcess(ps, t) 123 124 _ = stdinW.Close() 125 waitProcess(process, t) 126 127 out := buffers.Stdout.String() 128 if limit := strings.TrimSpace(out); limit != "1026" { 129 t.Fatalf("expected rlimit to be 1026, got %s", limit) 130 } 131 } 132 133 func TestExecInAdditionalGroups(t *testing.T) { 134 if testing.Short() { 135 return 136 } 137 138 config := newTemplateConfig(t, nil) 139 container, err := newContainer(t, config) 140 ok(t, err) 141 defer destroyContainer(container) 142 143 // Execute a first process in the container 144 stdinR, stdinW, err := os.Pipe() 145 ok(t, err) 146 process := &libcontainer.Process{ 147 Cwd: "/", 148 Args: []string{"cat"}, 149 Env: standardEnvironment, 150 Stdin: stdinR, 151 Init: true, 152 } 153 err = container.Run(process) 154 _ = stdinR.Close() 155 defer stdinW.Close() //nolint: errcheck 156 ok(t, err) 157 158 var stdout bytes.Buffer 159 pconfig := libcontainer.Process{ 160 Cwd: "/", 161 Args: []string{"sh", "-c", "id", "-Gn"}, 162 Env: standardEnvironment, 163 Stdin: nil, 164 Stdout: &stdout, 165 AdditionalGroups: []string{"plugdev", "audio"}, 166 } 167 err = container.Run(&pconfig) 168 ok(t, err) 169 170 // Wait for process 171 waitProcess(&pconfig, t) 172 173 _ = stdinW.Close() 174 waitProcess(process, t) 175 176 outputGroups := stdout.String() 177 178 // Check that the groups output has the groups that we specified 179 if !strings.Contains(outputGroups, "audio") { 180 t.Fatalf("Listed groups do not contain the audio group as expected: %v", outputGroups) 181 } 182 183 if !strings.Contains(outputGroups, "plugdev") { 184 t.Fatalf("Listed groups do not contain the plugdev group as expected: %v", outputGroups) 185 } 186 } 187 188 func TestExecInError(t *testing.T) { 189 if testing.Short() { 190 return 191 } 192 config := newTemplateConfig(t, nil) 193 container, err := newContainer(t, config) 194 ok(t, err) 195 defer destroyContainer(container) 196 197 // Execute a first process in the container 198 stdinR, stdinW, err := os.Pipe() 199 ok(t, err) 200 process := &libcontainer.Process{ 201 Cwd: "/", 202 Args: []string{"cat"}, 203 Env: standardEnvironment, 204 Stdin: stdinR, 205 Init: true, 206 } 207 err = container.Run(process) 208 _ = stdinR.Close() 209 defer func() { 210 _ = stdinW.Close() 211 if _, err := process.Wait(); err != nil { 212 t.Log(err) 213 } 214 }() 215 ok(t, err) 216 217 for i := 0; i < 42; i++ { 218 unexistent := &libcontainer.Process{ 219 Cwd: "/", 220 Args: []string{"unexistent"}, 221 Env: standardEnvironment, 222 } 223 err = container.Run(unexistent) 224 if err == nil { 225 t.Fatal("Should be an error") 226 } 227 if !strings.Contains(err.Error(), "executable file not found") { 228 t.Fatalf("Should be error about not found executable, got %s", err) 229 } 230 } 231 } 232 233 func TestExecInTTY(t *testing.T) { 234 if testing.Short() { 235 return 236 } 237 t.Skip("racy; see https://github.com/opencontainers/runc/issues/2425") 238 config := newTemplateConfig(t, nil) 239 container, err := newContainer(t, config) 240 ok(t, err) 241 defer destroyContainer(container) 242 243 // Execute a first process in the container 244 stdinR, stdinW, err := os.Pipe() 245 ok(t, err) 246 process := &libcontainer.Process{ 247 Cwd: "/", 248 Args: []string{"cat"}, 249 Env: standardEnvironment, 250 Stdin: stdinR, 251 Init: true, 252 } 253 err = container.Run(process) 254 _ = stdinR.Close() 255 defer func() { 256 _ = stdinW.Close() 257 if _, err := process.Wait(); err != nil { 258 t.Log(err) 259 } 260 }() 261 ok(t, err) 262 263 ps := &libcontainer.Process{ 264 Cwd: "/", 265 Args: []string{"ps"}, 266 Env: standardEnvironment, 267 } 268 269 // Repeat to increase chances to catch a race; see 270 // https://github.com/opencontainers/runc/issues/2425. 271 for i := 0; i < 300; i++ { 272 var stdout bytes.Buffer 273 274 parent, child, err := utils.NewSockPair("console") 275 ok(t, err) 276 ps.ConsoleSocket = child 277 278 done := make(chan (error)) 279 go func() { 280 f, err := utils.RecvFile(parent) 281 if err != nil { 282 done <- fmt.Errorf("RecvFile: %w", err) 283 return 284 } 285 c, err := console.ConsoleFromFile(f) 286 if err != nil { 287 done <- fmt.Errorf("ConsoleFromFile: %w", err) 288 return 289 } 290 err = console.ClearONLCR(c.Fd()) 291 if err != nil { 292 done <- fmt.Errorf("ClearONLCR: %w", err) 293 return 294 } 295 // An error from io.Copy is expected once the terminal 296 // is gone, so we deliberately ignore it. 297 _, _ = io.Copy(&stdout, c) 298 done <- nil 299 }() 300 301 err = container.Run(ps) 302 ok(t, err) 303 304 select { 305 case <-time.After(5 * time.Second): 306 t.Fatal("Waiting for copy timed out") 307 case err := <-done: 308 ok(t, err) 309 } 310 311 waitProcess(ps, t) 312 _ = parent.Close() 313 _ = child.Close() 314 315 out := stdout.String() 316 if !strings.Contains(out, "cat") || !strings.Contains(out, "ps") { 317 t.Fatalf("unexpected running process, output %q", out) 318 } 319 if strings.Contains(out, "\r") { 320 t.Fatalf("unexpected carriage-return in output %q", out) 321 } 322 } 323 } 324 325 func TestExecInEnvironment(t *testing.T) { 326 if testing.Short() { 327 return 328 } 329 config := newTemplateConfig(t, nil) 330 container, err := newContainer(t, config) 331 ok(t, err) 332 defer destroyContainer(container) 333 334 // Execute a first process in the container 335 stdinR, stdinW, err := os.Pipe() 336 ok(t, err) 337 process := &libcontainer.Process{ 338 Cwd: "/", 339 Args: []string{"cat"}, 340 Env: standardEnvironment, 341 Stdin: stdinR, 342 Init: true, 343 } 344 err = container.Run(process) 345 _ = stdinR.Close() 346 defer stdinW.Close() //nolint: errcheck 347 ok(t, err) 348 349 buffers := newStdBuffers() 350 process2 := &libcontainer.Process{ 351 Cwd: "/", 352 Args: []string{"env"}, 353 Env: []string{ 354 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 355 "DEBUG=true", 356 "DEBUG=false", 357 "ENV=test", 358 }, 359 Stdin: buffers.Stdin, 360 Stdout: buffers.Stdout, 361 Stderr: buffers.Stderr, 362 Init: true, 363 } 364 err = container.Run(process2) 365 ok(t, err) 366 waitProcess(process2, t) 367 368 _ = stdinW.Close() 369 waitProcess(process, t) 370 371 out := buffers.Stdout.String() 372 // check execin's process environment 373 if !strings.Contains(out, "DEBUG=false") || 374 !strings.Contains(out, "ENV=test") || 375 !strings.Contains(out, "HOME=/root") || 376 !strings.Contains(out, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") || 377 strings.Contains(out, "DEBUG=true") { 378 t.Fatalf("unexpected running process, output %q", out) 379 } 380 } 381 382 func TestExecinPassExtraFiles(t *testing.T) { 383 if testing.Short() { 384 return 385 } 386 config := newTemplateConfig(t, nil) 387 container, err := newContainer(t, config) 388 ok(t, err) 389 defer destroyContainer(container) 390 391 // Execute a first process in the container 392 stdinR, stdinW, err := os.Pipe() 393 ok(t, err) 394 process := &libcontainer.Process{ 395 Cwd: "/", 396 Args: []string{"cat"}, 397 Env: standardEnvironment, 398 Stdin: stdinR, 399 Init: true, 400 } 401 err = container.Run(process) 402 _ = stdinR.Close() 403 defer stdinW.Close() //nolint: errcheck 404 ok(t, err) 405 406 var stdout bytes.Buffer 407 pipeout1, pipein1, err := os.Pipe() 408 ok(t, err) 409 pipeout2, pipein2, err := os.Pipe() 410 ok(t, err) 411 inprocess := &libcontainer.Process{ 412 Cwd: "/", 413 Args: []string{"sh", "-c", "cd /proc/$$/fd; echo -n *; echo -n 1 >3; echo -n 2 >4"}, 414 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 415 ExtraFiles: []*os.File{pipein1, pipein2}, 416 Stdin: nil, 417 Stdout: &stdout, 418 } 419 err = container.Run(inprocess) 420 ok(t, err) 421 422 waitProcess(inprocess, t) 423 _ = stdinW.Close() 424 waitProcess(process, t) 425 426 out := stdout.String() 427 // fd 5 is the directory handle for /proc/$$/fd 428 if out != "0 1 2 3 4 5" { 429 t.Fatalf("expected to have the file descriptors '0 1 2 3 4 5' passed to exec, got '%s'", out) 430 } 431 buf := []byte{0} 432 _, err = pipeout1.Read(buf) 433 ok(t, err) 434 out1 := string(buf) 435 if out1 != "1" { 436 t.Fatalf("expected first pipe to receive '1', got '%s'", out1) 437 } 438 439 _, err = pipeout2.Read(buf) 440 ok(t, err) 441 out2 := string(buf) 442 if out2 != "2" { 443 t.Fatalf("expected second pipe to receive '2', got '%s'", out2) 444 } 445 } 446 447 func TestExecInOomScoreAdj(t *testing.T) { 448 if testing.Short() { 449 return 450 } 451 config := newTemplateConfig(t, nil) 452 config.OomScoreAdj = ptrInt(200) 453 container, err := newContainer(t, config) 454 ok(t, err) 455 defer destroyContainer(container) 456 457 stdinR, stdinW, err := os.Pipe() 458 ok(t, err) 459 process := &libcontainer.Process{ 460 Cwd: "/", 461 Args: []string{"cat"}, 462 Env: standardEnvironment, 463 Stdin: stdinR, 464 Init: true, 465 } 466 err = container.Run(process) 467 _ = stdinR.Close() 468 defer stdinW.Close() //nolint: errcheck 469 ok(t, err) 470 471 buffers := newStdBuffers() 472 ps := &libcontainer.Process{ 473 Cwd: "/", 474 Args: []string{"/bin/sh", "-c", "cat /proc/self/oom_score_adj"}, 475 Env: standardEnvironment, 476 Stdin: buffers.Stdin, 477 Stdout: buffers.Stdout, 478 Stderr: buffers.Stderr, 479 } 480 err = container.Run(ps) 481 ok(t, err) 482 waitProcess(ps, t) 483 484 _ = stdinW.Close() 485 waitProcess(process, t) 486 487 out := buffers.Stdout.String() 488 if oomScoreAdj := strings.TrimSpace(out); oomScoreAdj != strconv.Itoa(*config.OomScoreAdj) { 489 t.Fatalf("expected oomScoreAdj to be %d, got %s", *config.OomScoreAdj, oomScoreAdj) 490 } 491 } 492 493 func TestExecInUserns(t *testing.T) { 494 if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { 495 t.Skip("Test requires userns.") 496 } 497 if testing.Short() { 498 return 499 } 500 config := newTemplateConfig(t, &tParam{userns: true}) 501 container, err := newContainer(t, config) 502 ok(t, err) 503 defer destroyContainer(container) 504 505 // Execute a first process in the container 506 stdinR, stdinW, err := os.Pipe() 507 ok(t, err) 508 509 process := &libcontainer.Process{ 510 Cwd: "/", 511 Args: []string{"cat"}, 512 Env: standardEnvironment, 513 Stdin: stdinR, 514 Init: true, 515 } 516 err = container.Run(process) 517 _ = stdinR.Close() 518 defer stdinW.Close() //nolint: errcheck 519 ok(t, err) 520 521 initPID, err := process.Pid() 522 ok(t, err) 523 initUserns, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/user", initPID)) 524 ok(t, err) 525 526 buffers := newStdBuffers() 527 process2 := &libcontainer.Process{ 528 Cwd: "/", 529 Args: []string{"readlink", "/proc/self/ns/user"}, 530 Env: []string{ 531 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 532 }, 533 Stdout: buffers.Stdout, 534 Stderr: os.Stderr, 535 } 536 err = container.Run(process2) 537 ok(t, err) 538 waitProcess(process2, t) 539 _ = stdinW.Close() 540 waitProcess(process, t) 541 542 if out := strings.TrimSpace(buffers.Stdout.String()); out != initUserns { 543 t.Errorf("execin userns(%s), wanted %s", out, initUserns) 544 } 545 }