package container // import "github.com/docker/docker/integration/container" import ( "bufio" "context" "os" "regexp" "strings" "testing" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/testutil/daemon" "github.com/docker/docker/testutil/request" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" "gotest.tools/v3/skip" ) // testIpcCheckDevExists checks whether a given mount (identified by its // major:minor pair from /proc/self/mountinfo) exists on the host system. // // The format of /proc/self/mountinfo is like: // // 29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw // ^^^^\ // - this is the minor:major we look for func testIpcCheckDevExists(mm string) (bool, error) { f, err := os.Open("/proc/self/mountinfo") if err != nil { return false, err } defer f.Close() s := bufio.NewScanner(f) for s.Scan() { fields := strings.Fields(s.Text()) if len(fields) < 7 { continue } if fields[2] == mm { return true, nil } } return false, s.Err() } // testIpcNonePrivateShareable is a helper function to test "none", // "private" and "shareable" modes. func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool, mustBeShared bool) { defer setupTest(t)() cfg := containertypes.Config{ Image: "busybox", Cmd: []string{"top"}, } hostCfg := containertypes.HostConfig{ IpcMode: containertypes.IpcMode(mode), } client := testEnv.APIClient() ctx := context.Background() resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") assert.NilError(t, err) assert.Check(t, is.Equal(len(resp.Warnings), 0)) err = client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) assert.NilError(t, err) // get major:minor pair for /dev/shm from container's /proc/self/mountinfo cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo" result, err := container.Exec(ctx, client, resp.ID, []string{"sh", "-c", cmd}) assert.NilError(t, err) mm := result.Combined() if !mustBeMounted { assert.Check(t, is.Equal(mm, "")) // no more checks to perform return } assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm))) shared, err := testIpcCheckDevExists(mm) assert.NilError(t, err) t.Logf("[testIpcPrivateShareable] ipcmode: %v, ipcdev: %v, shared: %v, mustBeShared: %v\n", mode, mm, shared, mustBeShared) assert.Check(t, is.Equal(shared, mustBeShared)) } // TestIpcModeNone checks the container "none" IPC mode // (--ipc none) works as expected. It makes sure there is no // /dev/shm mount inside the container. func TestIpcModeNone(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testIpcNonePrivateShareable(t, "none", false, false) } // TestAPIIpcModePrivate checks the container private IPC mode // (--ipc private) works as expected. It gets the minor:major pair // of /dev/shm mount from the container, and makes sure there is no // such pair on the host. func TestIpcModePrivate(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testIpcNonePrivateShareable(t, "private", true, false) } // TestAPIIpcModeShareable checks the container shareable IPC mode // (--ipc shareable) works as expected. It gets the minor:major pair // of /dev/shm mount from the container, and makes sure such pair // also exists on the host. func TestIpcModeShareable(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless") testIpcNonePrivateShareable(t, "shareable", true, true) } // testIpcContainer is a helper function to test --ipc container:NNN mode in various scenarios func testIpcContainer(t *testing.T, donorMode string, mustWork bool) { t.Helper() defer setupTest(t)() cfg := containertypes.Config{ Image: "busybox", Cmd: []string{"top"}, } hostCfg := containertypes.HostConfig{ IpcMode: containertypes.IpcMode(donorMode), } ctx := context.Background() client := testEnv.APIClient() // create and start the "donor" container resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") assert.NilError(t, err) assert.Check(t, is.Equal(len(resp.Warnings), 0)) name1 := resp.ID err = client.ContainerStart(ctx, name1, types.ContainerStartOptions{}) assert.NilError(t, err) // create and start the second container hostCfg.IpcMode = containertypes.IpcMode("container:" + name1) resp, err = client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") assert.NilError(t, err) assert.Check(t, is.Equal(len(resp.Warnings), 0)) name2 := resp.ID err = client.ContainerStart(ctx, name2, types.ContainerStartOptions{}) if !mustWork { // start should fail with a specific error assert.Check(t, is.ErrorContains(err, "non-shareable IPC")) // no more checks to perform here return } // start should succeed assert.NilError(t, err) // check that IPC is shared // 1. create a file in the first container _, err = container.Exec(ctx, client, name1, []string{"sh", "-c", "printf covfefe > /dev/shm/bar"}) assert.NilError(t, err) // 2. check it's the same file in the second one result, err := container.Exec(ctx, client, name2, []string{"cat", "/dev/shm/bar"}) assert.NilError(t, err) out := result.Combined() assert.Check(t, is.Equal(true, regexp.MustCompile("^covfefe$").MatchString(out))) } // TestAPIIpcModeShareableAndPrivate checks that // 1) a container created with --ipc container:ID can use IPC of another shareable container. // 2) a container created with --ipc container:ID can NOT use IPC of another private container. func TestAPIIpcModeShareableAndContainer(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testIpcContainer(t, "shareable", true) testIpcContainer(t, "private", false) } /* TestAPIIpcModeHost checks that a container created with --ipc host * can use IPC of the host system. */ func TestAPIIpcModeHost(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsUserNamespace) skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless") cfg := containertypes.Config{ Image: "busybox", Cmd: []string{"top"}, } hostCfg := containertypes.HostConfig{ IpcMode: containertypes.IPCModeHost, } ctx := context.Background() client := testEnv.APIClient() resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") assert.NilError(t, err) assert.Check(t, is.Equal(len(resp.Warnings), 0)) name := resp.ID err = client.ContainerStart(ctx, name, types.ContainerStartOptions{}) assert.NilError(t, err) // check that IPC is shared // 1. create a file inside container _, err = container.Exec(ctx, client, name, []string{"sh", "-c", "printf covfefe > /dev/shm/." + name}) assert.NilError(t, err) // 2. check it's the same on the host bytes, err := os.ReadFile("/dev/shm/." + name) assert.NilError(t, err) assert.Check(t, is.Equal("covfefe", string(bytes))) // 3. clean up _, err = container.Exec(ctx, client, name, []string{"rm", "-f", "/dev/shm/." + name}) assert.NilError(t, err) } // testDaemonIpcPrivateShareable is a helper function to test "private" and "shareable" daemon default ipc modes. func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...string) { defer setupTest(t)() d := daemon.New(t) d.StartWithBusybox(t, arg...) defer d.Stop(t) c := d.NewClientT(t) cfg := containertypes.Config{ Image: "busybox", Cmd: []string{"top"}, } ctx := context.Background() resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "") assert.NilError(t, err) assert.Check(t, is.Equal(len(resp.Warnings), 0)) err = c.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) assert.NilError(t, err) // get major:minor pair for /dev/shm from container's /proc/self/mountinfo cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo" result, err := container.Exec(ctx, c, resp.ID, []string{"sh", "-c", cmd}) assert.NilError(t, err) mm := result.Combined() assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm))) shared, err := testIpcCheckDevExists(mm) assert.NilError(t, err) t.Logf("[testDaemonIpcPrivateShareable] ipcdev: %v, shared: %v, mustBeShared: %v\n", mm, shared, mustBeShared) assert.Check(t, is.Equal(shared, mustBeShared)) } // TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended. func TestDaemonIpcModeShareable(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless") testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable") } // TestDaemonIpcModePrivate checks that --default-ipc-mode private works as intended. func TestDaemonIpcModePrivate(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testDaemonIpcPrivateShareable(t, false, "--default-ipc-mode", "private") } // used to check if an IpcMode given in config works as intended func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) { skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless") config := `{"default-ipc-mode": "` + mode + `"}` file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config)) defer file.Remove() testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path()) } // TestDaemonIpcModePrivateFromConfig checks that "default-ipc-mode: private" config works as intended. func TestDaemonIpcModePrivateFromConfig(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testDaemonIpcFromConfig(t, "private", false) } // TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended. func TestDaemonIpcModeShareableFromConfig(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) testDaemonIpcFromConfig(t, "shareable", true) } // TestIpcModeOlderClient checks that older client gets shareable IPC mode // by default, even when the daemon default is private. func TestIpcModeOlderClient(t *testing.T) { skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "requires a daemon with DefaultIpcMode: private") c := testEnv.APIClient() skip.If(t, versions.LessThan(c.ClientVersion(), "1.40"), "requires client API >= 1.40") t.Parallel() ctx := context.Background() // pre-check: default ipc mode in daemon is private cID := container.Create(ctx, t, c, container.WithAutoRemove) inspect, err := c.ContainerInspect(ctx, cID) assert.NilError(t, err) assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "private")) // main check: using older client creates "shareable" container c = request.NewAPIClient(t, client.WithVersion("1.39")) cID = container.Create(ctx, t, c, container.WithAutoRemove) inspect, err = c.ContainerInspect(ctx, cID) assert.NilError(t, err) assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable")) }