github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/integration/container/mounts_linux_test.go (about) 1 package container // import "github.com/docker/docker/integration/container" 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "syscall" 9 "testing" 10 "time" 11 12 "github.com/docker/docker/api/types" 13 containertypes "github.com/docker/docker/api/types/container" 14 mounttypes "github.com/docker/docker/api/types/mount" 15 "github.com/docker/docker/api/types/network" 16 "github.com/docker/docker/api/types/versions" 17 "github.com/docker/docker/client" 18 "github.com/docker/docker/integration/internal/container" 19 "github.com/moby/sys/mount" 20 "github.com/moby/sys/mountinfo" 21 "gotest.tools/v3/assert" 22 is "gotest.tools/v3/assert/cmp" 23 "gotest.tools/v3/fs" 24 "gotest.tools/v3/poll" 25 "gotest.tools/v3/skip" 26 ) 27 28 func TestContainerNetworkMountsNoChown(t *testing.T) { 29 // chown only applies to Linux bind mounted volumes; must be same host to verify 30 skip.If(t, testEnv.IsRemoteDaemon) 31 32 defer setupTest(t)() 33 34 ctx := context.Background() 35 36 tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0644))) 37 defer tmpDir.Remove() 38 39 tmpNWFileMount := tmpDir.Join("nwfile") 40 41 config := containertypes.Config{ 42 Image: "busybox", 43 } 44 hostConfig := containertypes.HostConfig{ 45 Mounts: []mounttypes.Mount{ 46 { 47 Type: "bind", 48 Source: tmpNWFileMount, 49 Target: "/etc/resolv.conf", 50 }, 51 { 52 Type: "bind", 53 Source: tmpNWFileMount, 54 Target: "/etc/hostname", 55 }, 56 { 57 Type: "bind", 58 Source: tmpNWFileMount, 59 Target: "/etc/hosts", 60 }, 61 }, 62 } 63 64 cli, err := client.NewClientWithOpts(client.FromEnv) 65 assert.NilError(t, err) 66 defer cli.Close() 67 68 ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "") 69 assert.NilError(t, err) 70 // container will exit immediately because of no tty, but we only need the start sequence to test the condition 71 err = cli.ContainerStart(ctx, ctrCreate.ID, types.ContainerStartOptions{}) 72 assert.NilError(t, err) 73 74 // Check that host-located bind mount network file did not change ownership when the container was started 75 // Note: If the user specifies a mountpath from the host, we should not be 76 // attempting to chown files outside the daemon's metadata directory 77 // (represented by `daemon.repository` at init time). 78 // This forces users who want to use user namespaces to handle the 79 // ownership needs of any external files mounted as network files 80 // (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the 81 // daemon. In all other volume/bind mount situations we have taken this 82 // same line--we don't chown host file content. 83 // See GitHub PR 34224 for details. 84 info, err := os.Stat(tmpNWFileMount) 85 assert.NilError(t, err) 86 fi := info.Sys().(*syscall.Stat_t) 87 assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root") 88 } 89 90 func TestMountDaemonRoot(t *testing.T) { 91 skip.If(t, testEnv.IsRemoteDaemon) 92 93 defer setupTest(t)() 94 client := testEnv.APIClient() 95 ctx := context.Background() 96 info, err := client.Info(ctx) 97 if err != nil { 98 t.Fatal(err) 99 } 100 101 for _, test := range []struct { 102 desc string 103 propagation mounttypes.Propagation 104 expected mounttypes.Propagation 105 }{ 106 { 107 desc: "default", 108 propagation: "", 109 expected: mounttypes.PropagationRSlave, 110 }, 111 { 112 desc: "private", 113 propagation: mounttypes.PropagationPrivate, 114 }, 115 { 116 desc: "rprivate", 117 propagation: mounttypes.PropagationRPrivate, 118 }, 119 { 120 desc: "slave", 121 propagation: mounttypes.PropagationSlave, 122 }, 123 { 124 desc: "rslave", 125 propagation: mounttypes.PropagationRSlave, 126 expected: mounttypes.PropagationRSlave, 127 }, 128 { 129 desc: "shared", 130 propagation: mounttypes.PropagationShared, 131 }, 132 { 133 desc: "rshared", 134 propagation: mounttypes.PropagationRShared, 135 expected: mounttypes.PropagationRShared, 136 }, 137 } { 138 t.Run(test.desc, func(t *testing.T) { 139 test := test 140 t.Parallel() 141 142 propagationSpec := fmt.Sprintf(":%s", test.propagation) 143 if test.propagation == "" { 144 propagationSpec = "" 145 } 146 bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec 147 bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec 148 149 for name, hc := range map[string]*containertypes.HostConfig{ 150 "bind root": {Binds: []string{bindSpecRoot}}, 151 "bind subpath": {Binds: []string{bindSpecSub}}, 152 "mount root": { 153 Mounts: []mounttypes.Mount{ 154 { 155 Type: mounttypes.TypeBind, 156 Source: info.DockerRootDir, 157 Target: "/foo", 158 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 159 }, 160 }, 161 }, 162 "mount subpath": { 163 Mounts: []mounttypes.Mount{ 164 { 165 Type: mounttypes.TypeBind, 166 Source: filepath.Join(info.DockerRootDir, "containers"), 167 Target: "/foo", 168 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 169 }, 170 }, 171 }, 172 } { 173 t.Run(name, func(t *testing.T) { 174 hc := hc 175 t.Parallel() 176 177 c, err := client.ContainerCreate(ctx, &containertypes.Config{ 178 Image: "busybox", 179 Cmd: []string{"true"}, 180 }, hc, nil, nil, "") 181 182 if err != nil { 183 if test.expected != "" { 184 t.Fatal(err) 185 } 186 // expected an error, so this is ok and should not continue 187 return 188 } 189 if test.expected == "" { 190 t.Fatal("expected create to fail") 191 } 192 193 defer func() { 194 if err := client.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true}); err != nil { 195 panic(err) 196 } 197 }() 198 199 inspect, err := client.ContainerInspect(ctx, c.ID) 200 if err != nil { 201 t.Fatal(err) 202 } 203 if len(inspect.Mounts) != 1 { 204 t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts) 205 } 206 207 m := inspect.Mounts[0] 208 if m.Propagation != test.expected { 209 t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation) 210 } 211 }) 212 } 213 }) 214 } 215 } 216 217 func TestContainerBindMountNonRecursive(t *testing.T) { 218 skip.If(t, testEnv.IsRemoteDaemon) 219 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "BindOptions.NonRecursive requires API v1.40") 220 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 221 222 defer setupTest(t)() 223 224 tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0755), 225 fs.WithDir("mnt", fs.WithMode(0755))) 226 defer tmpDir1.Remove() 227 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") 228 tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0755), 229 fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0644))) 230 defer tmpDir2.Remove() 231 232 err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro") 233 if err != nil { 234 t.Fatal(err) 235 } 236 defer func() { 237 if err := mount.Unmount(tmpDir1Mnt); err != nil { 238 t.Fatal(err) 239 } 240 }() 241 242 // implicit is recursive (NonRecursive: false) 243 implicit := mounttypes.Mount{ 244 Type: "bind", 245 Source: tmpDir1.Path(), 246 Target: "/foo", 247 ReadOnly: true, 248 } 249 recursive := implicit 250 recursive.BindOptions = &mounttypes.BindOptions{ 251 NonRecursive: false, 252 } 253 recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"} 254 nonRecursive := implicit 255 nonRecursive.BindOptions = &mounttypes.BindOptions{ 256 NonRecursive: true, 257 } 258 nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"} 259 260 ctx := context.Background() 261 client := testEnv.APIClient() 262 containers := []string{ 263 container.Run(ctx, t, client, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)), 264 container.Run(ctx, t, client, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)), 265 container.Run(ctx, t, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), 266 } 267 268 for _, c := range containers { 269 poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond)) 270 } 271 } 272 273 func TestContainerVolumesMountedAsShared(t *testing.T) { 274 // Volume propagation is linux only. Also it creates directories for 275 // bind mounting, so needs to be same host. 276 skip.If(t, testEnv.IsRemoteDaemon) 277 skip.If(t, testEnv.IsUserNamespace) 278 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 279 280 defer setupTest(t)() 281 282 // Prepare a source directory to bind mount 283 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755), 284 fs.WithDir("mnt1", fs.WithMode(0755))) 285 defer tmpDir1.Remove() 286 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1") 287 288 // Convert this directory into a shared mount point so that we do 289 // not rely on propagation properties of parent mount. 290 if err := mount.MakePrivate(tmpDir1.Path()); err != nil { 291 t.Fatal(err) 292 } 293 defer func() { 294 if err := mount.Unmount(tmpDir1.Path()); err != nil { 295 t.Fatal(err) 296 } 297 }() 298 if err := mount.MakeShared(tmpDir1.Path()); err != nil { 299 t.Fatal(err) 300 } 301 302 sharedMount := mounttypes.Mount{ 303 Type: mounttypes.TypeBind, 304 Source: tmpDir1.Path(), 305 Target: "/volume-dest", 306 BindOptions: &mounttypes.BindOptions{ 307 Propagation: mounttypes.PropagationShared, 308 }, 309 } 310 311 bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"} 312 313 ctx := context.Background() 314 client := testEnv.APIClient() 315 containerID := container.Run(ctx, t, client, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...)) 316 poll.WaitOn(t, container.IsSuccessful(ctx, client, containerID), poll.WithDelay(100*time.Millisecond)) 317 318 // Make sure a bind mount under a shared volume propagated to host. 319 if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted { 320 t.Fatalf("Bind mount under shared volume did not propagate to host") 321 } 322 323 mount.Unmount(tmpDir1Mnt) 324 } 325 326 func TestContainerVolumesMountedAsSlave(t *testing.T) { 327 // Volume propagation is linux only. Also it creates directories for 328 // bind mounting, so needs to be same host. 329 skip.If(t, testEnv.IsRemoteDaemon) 330 skip.If(t, testEnv.IsUserNamespace) 331 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 332 333 // Prepare a source directory to bind mount 334 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755), 335 fs.WithDir("mnt1", fs.WithMode(0755))) 336 defer tmpDir1.Remove() 337 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1") 338 339 // Prepare a source directory with file in it. We will bind mount this 340 // directory and see if file shows up. 341 tmpDir2 := fs.NewDir(t, "volume-source2", fs.WithMode(0755), 342 fs.WithFile("slave-testfile", "Test", fs.WithMode(0644))) 343 defer tmpDir2.Remove() 344 345 // Convert this directory into a shared mount point so that we do 346 // not rely on propagation properties of parent mount. 347 if err := mount.MakePrivate(tmpDir1.Path()); err != nil { 348 t.Fatal(err) 349 } 350 defer func() { 351 if err := mount.Unmount(tmpDir1.Path()); err != nil { 352 t.Fatal(err) 353 } 354 }() 355 if err := mount.MakeShared(tmpDir1.Path()); err != nil { 356 t.Fatal(err) 357 } 358 359 slaveMount := mounttypes.Mount{ 360 Type: mounttypes.TypeBind, 361 Source: tmpDir1.Path(), 362 Target: "/volume-dest", 363 BindOptions: &mounttypes.BindOptions{ 364 Propagation: mounttypes.PropagationSlave, 365 }, 366 } 367 368 topCmd := []string{"top"} 369 370 ctx := context.Background() 371 client := testEnv.APIClient() 372 containerID := container.Run(ctx, t, client, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...)) 373 374 // Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside 375 // container then contents of tmpDir2/slave-testfile should become 376 // visible at "/volume-dest/mnt1/slave-testfile" 377 if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { 378 t.Fatal(err) 379 } 380 defer func() { 381 if err := mount.Unmount(tmpDir1Mnt); err != nil { 382 t.Fatal(err) 383 } 384 }() 385 386 mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"} 387 388 if result, err := container.Exec(ctx, client, containerID, mountCmd); err == nil { 389 if result.Stdout() != "Test" { 390 t.Fatalf("Bind mount under slave volume did not propagate to container") 391 } 392 } else { 393 t.Fatal(err) 394 } 395 } 396 397 // Regression test for #38995 and #43390. 398 func TestContainerCopyLeaksMounts(t *testing.T) { 399 defer setupTest(t)() 400 401 bindMount := mounttypes.Mount{ 402 Type: mounttypes.TypeBind, 403 Source: "/var", 404 Target: "/hostvar", 405 BindOptions: &mounttypes.BindOptions{ 406 Propagation: mounttypes.PropagationRSlave, 407 }, 408 } 409 410 ctx := context.Background() 411 client := testEnv.APIClient() 412 cid := container.Run(ctx, t, client, container.WithMount(bindMount), container.WithCmd("sleep", "120s")) 413 414 getMounts := func() string { 415 t.Helper() 416 res, err := container.Exec(ctx, client, cid, []string{"cat", "/proc/self/mountinfo"}) 417 assert.NilError(t, err) 418 assert.Equal(t, res.ExitCode, 0) 419 return res.Stdout() 420 } 421 422 mountsBefore := getMounts() 423 424 _, _, err := client.CopyFromContainer(ctx, cid, "/etc/passwd") 425 assert.NilError(t, err) 426 427 mountsAfter := getMounts() 428 429 assert.Equal(t, mountsBefore, mountsAfter) 430 }