| package integration |
| |
| import ( |
| "fmt" |
| "io" |
| "os" |
| "slices" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/sys/unix" |
| |
| "github.com/containerd/console" |
| "github.com/opencontainers/runc/internal/cmsg" |
| "github.com/opencontainers/runc/libcontainer" |
| "github.com/opencontainers/runc/libcontainer/configs" |
| "github.com/opencontainers/runc/libcontainer/utils" |
| ) |
| |
| func TestExecIn(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| buffers := newStdBuffers() |
| ps := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"ps"}, |
| Env: standardEnvironment, |
| Stdout: buffers.Stdout, |
| Stderr: buffers.Stderr, |
| } |
| |
| err = container.Run(ps) |
| ok(t, err) |
| waitProcess(ps, t) |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| out := buffers.Stdout.String() |
| if !strings.Contains(out, "cat") || !strings.Contains(out, "ps") { |
| t.Fatalf("unexpected running process, output %q", out) |
| } |
| if strings.Contains(out, "\r") { |
| t.Fatalf("unexpected carriage-return in output %q", out) |
| } |
| } |
| |
| func TestExecInUsernsRlimit(t *testing.T) { |
| needUserNS(t) |
| testExecInRlimit(t, true) |
| } |
| |
| func TestExecInRlimit(t *testing.T) { |
| testExecInRlimit(t, false) |
| } |
| |
| func testExecInRlimit(t *testing.T, userns bool) { |
| if testing.Short() { |
| return |
| } |
| |
| config := newTemplateConfig(t, &tParam{userns: userns}) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| buffers := newStdBuffers() |
| ps := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"/bin/sh", "-c", "ulimit -n"}, |
| Env: standardEnvironment, |
| Stdout: buffers.Stdout, |
| Stderr: buffers.Stderr, |
| Rlimits: []configs.Rlimit{ |
| // increase process rlimit higher than container rlimit to test per-process limit |
| {Type: unix.RLIMIT_NOFILE, Hard: 1026, Soft: 1026}, |
| }, |
| } |
| err = container.Run(ps) |
| ok(t, err) |
| waitProcess(ps, t) |
| |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| out := buffers.Stdout.String() |
| if limit := strings.TrimSpace(out); limit != "1026" { |
| t.Fatalf("expected rlimit to be 1026, got %s", limit) |
| } |
| } |
| |
| func TestExecInAdditionalGroups(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| var stdout strings.Builder |
| pconfig := libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"sh", "-c", "id", "-Gn"}, |
| Env: standardEnvironment, |
| Stdin: nil, |
| Stdout: &stdout, |
| Stderr: new(strings.Builder), |
| AdditionalGroups: []int{4444, 87654}, |
| } |
| err = container.Run(&pconfig) |
| ok(t, err) |
| |
| // Wait for process |
| waitProcess(&pconfig, t) |
| |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| outputGroups := stdout.String() |
| |
| // Check that the groups output has the groups that we specified. |
| for _, gid := range pconfig.AdditionalGroups { |
| if !strings.Contains(outputGroups, strconv.Itoa(gid)) { |
| t.Errorf("Listed groups do not contain gid %d as expected: %v", gid, outputGroups) |
| } |
| } |
| } |
| |
| func TestExecInError(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer func() { |
| _ = stdinW.Close() |
| if _, err := process.Wait(); err != nil { |
| t.Log(err) |
| } |
| }() |
| ok(t, err) |
| |
| for range 42 { |
| unexistent := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"unexistent"}, |
| Env: standardEnvironment, |
| } |
| err = container.Run(unexistent) |
| if err == nil { |
| t.Fatal("Should be an error") |
| } |
| if !strings.Contains(err.Error(), "executable file not found") { |
| t.Fatalf("Should be error about not found executable, got %s", err) |
| } |
| } |
| } |
| |
| func TestExecInTTY(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| t.Skip("racy; see https://github.com/opencontainers/runc/issues/2425") |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer func() { |
| _ = stdinW.Close() |
| if _, err := process.Wait(); err != nil { |
| t.Log(err) |
| } |
| }() |
| ok(t, err) |
| |
| ps := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"ps"}, |
| Env: standardEnvironment, |
| } |
| |
| // Repeat to increase chances to catch a race; see |
| // https://github.com/opencontainers/runc/issues/2425. |
| for range 300 { |
| var stdout strings.Builder |
| |
| parent, child, err := utils.NewSockPair("console") |
| ok(t, err) |
| ps.ConsoleSocket = child |
| |
| done := make(chan (error)) |
| go func() { |
| f, err := cmsg.RecvFile(parent) |
| if err != nil { |
| done <- fmt.Errorf("RecvFile: %w", err) |
| return |
| } |
| c, err := console.ConsoleFromFile(f) |
| if err != nil { |
| done <- fmt.Errorf("ConsoleFromFile: %w", err) |
| return |
| } |
| err = console.ClearONLCR(c.Fd()) |
| if err != nil { |
| done <- fmt.Errorf("ClearONLCR: %w", err) |
| return |
| } |
| // An error from io.Copy is expected once the terminal |
| // is gone, so we deliberately ignore it. |
| _, _ = io.Copy(&stdout, c) |
| done <- nil |
| }() |
| |
| err = container.Run(ps) |
| ok(t, err) |
| |
| select { |
| case <-time.After(5 * time.Second): |
| t.Fatal("Waiting for copy timed out") |
| case err := <-done: |
| ok(t, err) |
| } |
| |
| waitProcess(ps, t) |
| _ = parent.Close() |
| _ = child.Close() |
| |
| out := stdout.String() |
| if !strings.Contains(out, "cat") || !strings.Contains(out, "ps") { |
| t.Fatalf("unexpected running process, output %q", out) |
| } |
| if strings.Contains(out, "\r") { |
| t.Fatalf("unexpected carriage-return in output %q", out) |
| } |
| } |
| } |
| |
| func TestExecInEnvironment(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| execEnv := []string{ |
| "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", |
| // The below line is added to check the deduplication feature: |
| // if a variable with the same name appears more than once, |
| // only the last value should be added to the environment. |
| "DEBUG=true", |
| "DEBUG=false", |
| "ENV=test", |
| } |
| buffers := newStdBuffers() |
| process2 := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"/bin/env"}, |
| Env: execEnv, |
| Stdout: buffers.Stdout, |
| Stderr: buffers.Stderr, |
| } |
| err = container.Run(process2) |
| ok(t, err) |
| waitProcess(process2, t) |
| |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| // Check exec process environment. |
| t.Logf("exec output:\n%s", buffers.Stdout.String()) |
| out := strings.Fields(buffers.Stdout.String()) |
| // If not present in the Process.Env, runc should add $HOME, |
| // which is deduced by parsing container's /etc/passwd. |
| // We use a known image in the test, so we know its value. |
| for _, e := range append(execEnv, "HOME=/root") { |
| if e == "DEBUG=true" { // This one should be dedup'ed out. |
| continue |
| } |
| if !slices.Contains(out, e) { |
| t.Errorf("Expected env %s not found in exec output", e) |
| } |
| } |
| // Check that there are no extra variables. We have 1 env ("DEBUG=true") |
| // removed and 1 env ("HOME=root") added, resulting in the same number. |
| if got, want := len(out), len(execEnv); want != got { |
| t.Errorf("Mismatched number of variables: want %d, got %d", want, got) |
| } |
| } |
| |
| func TestExecinPassExtraFiles(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, nil) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| var stdout strings.Builder |
| pipeout1, pipein1, err := os.Pipe() |
| ok(t, err) |
| pipeout2, pipein2, err := os.Pipe() |
| ok(t, err) |
| inprocess := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"sh", "-c", "cd /proc/$$/fd; echo -n *; echo -n 1 >3; echo -n 2 >4"}, |
| Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, |
| ExtraFiles: []*os.File{pipein1, pipein2}, |
| Stdin: nil, |
| Stdout: &stdout, |
| Stderr: new(strings.Builder), |
| } |
| err = container.Run(inprocess) |
| ok(t, err) |
| |
| waitProcess(inprocess, t) |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| out := stdout.String() |
| // fd 5 is the directory handle for /proc/$$/fd |
| if out != "0 1 2 3 4 5" { |
| t.Fatalf("expected to have the file descriptors '0 1 2 3 4 5' passed to exec, got '%s'", out) |
| } |
| buf := []byte{0} |
| _, err = pipeout1.Read(buf) |
| ok(t, err) |
| out1 := string(buf) |
| if out1 != "1" { |
| t.Fatalf("expected first pipe to receive '1', got '%s'", out1) |
| } |
| |
| _, err = pipeout2.Read(buf) |
| ok(t, err) |
| out2 := string(buf) |
| if out2 != "2" { |
| t.Fatalf("expected second pipe to receive '2', got '%s'", out2) |
| } |
| } |
| |
| func TestExecInOomScoreAdj(t *testing.T) { |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, nil) |
| config.OomScoreAdj = ptrInt(200) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| buffers := newStdBuffers() |
| ps := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"/bin/sh", "-c", "cat /proc/self/oom_score_adj"}, |
| Env: standardEnvironment, |
| Stdout: buffers.Stdout, |
| Stderr: buffers.Stderr, |
| } |
| err = container.Run(ps) |
| ok(t, err) |
| waitProcess(ps, t) |
| |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| out := buffers.Stdout.String() |
| if oomScoreAdj := strings.TrimSpace(out); oomScoreAdj != strconv.Itoa(*config.OomScoreAdj) { |
| t.Fatalf("expected oomScoreAdj to be %d, got %s", *config.OomScoreAdj, oomScoreAdj) |
| } |
| } |
| |
| func TestExecInUserns(t *testing.T) { |
| needUserNS(t) |
| if testing.Short() { |
| return |
| } |
| config := newTemplateConfig(t, &tParam{userns: true}) |
| container, err := newContainer(t, config) |
| ok(t, err) |
| defer destroyContainer(container) |
| |
| // Execute a first process in the container |
| stdinR, stdinW, err := os.Pipe() |
| ok(t, err) |
| |
| process := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"cat"}, |
| Env: standardEnvironment, |
| Stdin: stdinR, |
| Init: true, |
| } |
| err = container.Run(process) |
| _ = stdinR.Close() |
| defer stdinW.Close() |
| ok(t, err) |
| |
| initPID, err := process.Pid() |
| ok(t, err) |
| initUserns, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/user", initPID)) |
| ok(t, err) |
| |
| buffers := newStdBuffers() |
| process2 := &libcontainer.Process{ |
| Cwd: "/", |
| Args: []string{"readlink", "-v", "/proc/self/ns/user"}, |
| Env: []string{ |
| "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", |
| }, |
| Stdout: buffers.Stdout, |
| Stderr: new(strings.Builder), |
| } |
| err = container.Run(process2) |
| ok(t, err) |
| waitProcess(process2, t) |
| _ = stdinW.Close() |
| waitProcess(process, t) |
| |
| if out := strings.TrimSpace(buffers.Stdout.String()); out != initUserns { |
| t.Errorf("execin userns(%s), wanted %s", out, initUserns) |
| } |
| } |