github.com/moby/docker@v26.1.3+incompatible/integration/container/ipcmode_linux_test.go (about) 1 package container // import "github.com/docker/docker/integration/container" 2 3 import ( 4 "bufio" 5 "os" 6 "regexp" 7 "strings" 8 "testing" 9 10 containertypes "github.com/docker/docker/api/types/container" 11 "github.com/docker/docker/api/types/versions" 12 "github.com/docker/docker/client" 13 "github.com/docker/docker/integration/internal/container" 14 "github.com/docker/docker/testutil" 15 "github.com/docker/docker/testutil/daemon" 16 "github.com/docker/docker/testutil/request" 17 "gotest.tools/v3/assert" 18 is "gotest.tools/v3/assert/cmp" 19 "gotest.tools/v3/fs" 20 "gotest.tools/v3/skip" 21 ) 22 23 // testIpcCheckDevExists checks whether a given mount (identified by its 24 // major:minor pair from /proc/self/mountinfo) exists on the host system. 25 // 26 // The format of /proc/self/mountinfo is like: 27 // 28 // 29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw 29 // ^^^^\ 30 // - this is the minor:major we look for 31 // 32 //nolint:dupword 33 func testIpcCheckDevExists(mm string) (bool, error) { 34 f, err := os.Open("/proc/self/mountinfo") 35 if err != nil { 36 return false, err 37 } 38 defer f.Close() 39 40 s := bufio.NewScanner(f) 41 for s.Scan() { 42 fields := strings.Fields(s.Text()) 43 if len(fields) < 7 { 44 continue 45 } 46 if fields[2] == mm { 47 return true, nil 48 } 49 } 50 51 return false, s.Err() 52 } 53 54 // testIpcNonePrivateShareable is a helper function to test "none", 55 // "private" and "shareable" modes. 56 func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool, mustBeShared bool) { 57 ctx := setupTest(t) 58 59 cfg := containertypes.Config{ 60 Image: "busybox", 61 Cmd: []string{"top"}, 62 } 63 hostCfg := containertypes.HostConfig{ 64 IpcMode: containertypes.IpcMode(mode), 65 } 66 apiClient := testEnv.APIClient() 67 68 resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") 69 assert.NilError(t, err) 70 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 71 72 err = apiClient.ContainerStart(ctx, resp.ID, containertypes.StartOptions{}) 73 assert.NilError(t, err) 74 75 // get major:minor pair for /dev/shm from container's /proc/self/mountinfo 76 cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo" 77 result, err := container.Exec(ctx, apiClient, resp.ID, []string{"sh", "-c", cmd}) 78 assert.NilError(t, err) 79 mm := result.Combined() 80 if !mustBeMounted { 81 assert.Check(t, is.Equal(mm, "")) 82 // no more checks to perform 83 return 84 } 85 assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm))) 86 87 shared, err := testIpcCheckDevExists(mm) 88 assert.NilError(t, err) 89 t.Logf("[testIpcPrivateShareable] ipcmode: %v, ipcdev: %v, shared: %v, mustBeShared: %v\n", mode, mm, shared, mustBeShared) 90 assert.Check(t, is.Equal(shared, mustBeShared)) 91 } 92 93 // TestIpcModeNone checks the container "none" IPC mode 94 // (--ipc none) works as expected. It makes sure there is no 95 // /dev/shm mount inside the container. 96 func TestIpcModeNone(t *testing.T) { 97 skip.If(t, testEnv.IsRemoteDaemon) 98 99 testIpcNonePrivateShareable(t, "none", false, false) 100 } 101 102 // TestAPIIpcModePrivate checks the container private IPC mode 103 // (--ipc private) works as expected. It gets the minor:major pair 104 // of /dev/shm mount from the container, and makes sure there is no 105 // such pair on the host. 106 func TestIpcModePrivate(t *testing.T) { 107 skip.If(t, testEnv.IsRemoteDaemon) 108 109 testIpcNonePrivateShareable(t, "private", true, false) 110 } 111 112 // TestAPIIpcModeShareable checks the container shareable IPC mode 113 // (--ipc shareable) works as expected. It gets the minor:major pair 114 // of /dev/shm mount from the container, and makes sure such pair 115 // also exists on the host. 116 func TestIpcModeShareable(t *testing.T) { 117 skip.If(t, testEnv.IsRemoteDaemon) 118 skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless") 119 120 testIpcNonePrivateShareable(t, "shareable", true, true) 121 } 122 123 // testIpcContainer is a helper function to test --ipc container:NNN mode in various scenarios 124 func testIpcContainer(t *testing.T, donorMode string, mustWork bool) { 125 t.Helper() 126 127 ctx := setupTest(t) 128 129 cfg := containertypes.Config{ 130 Image: "busybox", 131 Cmd: []string{"top"}, 132 } 133 hostCfg := containertypes.HostConfig{ 134 IpcMode: containertypes.IpcMode(donorMode), 135 } 136 apiClient := testEnv.APIClient() 137 138 // create and start the "donor" container 139 resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") 140 assert.NilError(t, err) 141 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 142 name1 := resp.ID 143 144 err = apiClient.ContainerStart(ctx, name1, containertypes.StartOptions{}) 145 assert.NilError(t, err) 146 147 // create and start the second container 148 hostCfg.IpcMode = containertypes.IpcMode("container:" + name1) 149 resp, err = apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") 150 assert.NilError(t, err) 151 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 152 name2 := resp.ID 153 154 err = apiClient.ContainerStart(ctx, name2, containertypes.StartOptions{}) 155 if !mustWork { 156 // start should fail with a specific error 157 assert.Check(t, is.ErrorContains(err, "non-shareable IPC")) 158 // no more checks to perform here 159 return 160 } 161 162 // start should succeed 163 assert.NilError(t, err) 164 165 // check that IPC is shared 166 // 1. create a file in the first container 167 _, err = container.Exec(ctx, apiClient, name1, []string{"sh", "-c", "printf covfefe > /dev/shm/bar"}) 168 assert.NilError(t, err) 169 // 2. check it's the same file in the second one 170 result, err := container.Exec(ctx, apiClient, name2, []string{"cat", "/dev/shm/bar"}) 171 assert.NilError(t, err) 172 out := result.Combined() 173 assert.Check(t, is.Equal(true, regexp.MustCompile("^covfefe$").MatchString(out))) 174 } 175 176 // TestAPIIpcModeShareableAndPrivate checks that 177 // 1) a container created with --ipc container:ID can use IPC of another shareable container. 178 // 2) a container created with --ipc container:ID can NOT use IPC of another private container. 179 func TestAPIIpcModeShareableAndContainer(t *testing.T) { 180 skip.If(t, testEnv.IsRemoteDaemon) 181 182 testIpcContainer(t, "shareable", true) 183 184 testIpcContainer(t, "private", false) 185 } 186 187 /* TestAPIIpcModeHost checks that a container created with --ipc host 188 * can use IPC of the host system. 189 */ 190 func TestAPIIpcModeHost(t *testing.T) { 191 skip.If(t, testEnv.IsRemoteDaemon) 192 skip.If(t, testEnv.IsUserNamespace) 193 194 cfg := containertypes.Config{ 195 Image: "busybox", 196 Cmd: []string{"top"}, 197 } 198 hostCfg := containertypes.HostConfig{ 199 IpcMode: containertypes.IPCModeHost, 200 } 201 202 ctx := testutil.StartSpan(baseContext, t) 203 204 apiClient := testEnv.APIClient() 205 resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "") 206 assert.NilError(t, err) 207 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 208 name := resp.ID 209 210 err = apiClient.ContainerStart(ctx, name, containertypes.StartOptions{}) 211 assert.NilError(t, err) 212 213 // check that IPC is shared 214 // 1. create a file inside container 215 _, err = container.Exec(ctx, apiClient, name, []string{"sh", "-c", "printf covfefe > /dev/shm/." + name}) 216 assert.NilError(t, err) 217 // 2. check it's the same on the host 218 bytes, err := os.ReadFile("/dev/shm/." + name) 219 assert.NilError(t, err) 220 assert.Check(t, is.Equal("covfefe", string(bytes))) 221 // 3. clean up 222 _, err = container.Exec(ctx, apiClient, name, []string{"rm", "-f", "/dev/shm/." + name}) 223 assert.NilError(t, err) 224 } 225 226 // testDaemonIpcPrivateShareable is a helper function to test "private" and "shareable" daemon default ipc modes. 227 func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...string) { 228 ctx := setupTest(t) 229 230 d := daemon.New(t) 231 d.StartWithBusybox(ctx, t, arg...) 232 defer d.Stop(t) 233 234 c := d.NewClientT(t) 235 236 cfg := containertypes.Config{ 237 Image: "busybox", 238 Cmd: []string{"top"}, 239 } 240 241 resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "") 242 assert.NilError(t, err) 243 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 244 245 err = c.ContainerStart(ctx, resp.ID, containertypes.StartOptions{}) 246 assert.NilError(t, err) 247 248 // get major:minor pair for /dev/shm from container's /proc/self/mountinfo 249 cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo" 250 result, err := container.Exec(ctx, c, resp.ID, []string{"sh", "-c", cmd}) 251 assert.NilError(t, err) 252 mm := result.Combined() 253 assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm))) 254 255 shared, err := testIpcCheckDevExists(mm) 256 assert.NilError(t, err) 257 t.Logf("[testDaemonIpcPrivateShareable] ipcdev: %v, shared: %v, mustBeShared: %v\n", mm, shared, mustBeShared) 258 assert.Check(t, is.Equal(shared, mustBeShared)) 259 } 260 261 // TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended. 262 func TestDaemonIpcModeShareable(t *testing.T) { 263 skip.If(t, testEnv.IsRemoteDaemon) 264 skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless") 265 266 testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable") 267 } 268 269 // TestDaemonIpcModePrivate checks that --default-ipc-mode private works as intended. 270 func TestDaemonIpcModePrivate(t *testing.T) { 271 skip.If(t, testEnv.IsRemoteDaemon) 272 273 testDaemonIpcPrivateShareable(t, false, "--default-ipc-mode", "private") 274 } 275 276 // used to check if an IpcMode given in config works as intended 277 func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) { 278 config := `{"default-ipc-mode": "` + mode + `"}` 279 // WithMode is needed for rootless 280 file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config), fs.WithMode(0o644)) 281 defer file.Remove() 282 283 testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path()) 284 } 285 286 // TestDaemonIpcModePrivateFromConfig checks that "default-ipc-mode: private" config works as intended. 287 func TestDaemonIpcModePrivateFromConfig(t *testing.T) { 288 skip.If(t, testEnv.IsRemoteDaemon) 289 290 testDaemonIpcFromConfig(t, "private", false) 291 } 292 293 // TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended. 294 func TestDaemonIpcModeShareableFromConfig(t *testing.T) { 295 skip.If(t, testEnv.IsRemoteDaemon) 296 skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless") 297 298 testDaemonIpcFromConfig(t, "shareable", true) 299 } 300 301 // TestIpcModeOlderClient checks that older client gets shareable IPC mode 302 // by default, even when the daemon default is private. 303 func TestIpcModeOlderClient(t *testing.T) { 304 apiClient := testEnv.APIClient() 305 skip.If(t, versions.LessThan(apiClient.ClientVersion(), "1.40"), "requires client API >= 1.40") 306 307 t.Parallel() 308 309 ctx := testutil.StartSpan(baseContext, t) 310 311 // pre-check: default ipc mode in daemon is private 312 cID := container.Create(ctx, t, apiClient, container.WithAutoRemove) 313 314 inspect, err := apiClient.ContainerInspect(ctx, cID) 315 assert.NilError(t, err) 316 assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "private")) 317 318 // main check: using older client creates "shareable" container 319 apiClient = request.NewAPIClient(t, client.WithVersion("1.39")) 320 cID = container.Create(ctx, t, apiClient, container.WithAutoRemove) 321 322 inspect, err = apiClient.ContainerInspect(ctx, cID) 323 assert.NilError(t, err) 324 assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable")) 325 }