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