github.com/moby/docker@v26.1.3+incompatible/integration/container/mounts_linux_test.go (about) 1 package container // import "github.com/docker/docker/integration/container" 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "syscall" 8 "testing" 9 "time" 10 11 "github.com/docker/docker/api" 12 containertypes "github.com/docker/docker/api/types/container" 13 mounttypes "github.com/docker/docker/api/types/mount" 14 "github.com/docker/docker/api/types/network" 15 "github.com/docker/docker/api/types/versions" 16 "github.com/docker/docker/client" 17 "github.com/docker/docker/integration/internal/container" 18 "github.com/docker/docker/pkg/parsers/kernel" 19 "github.com/docker/docker/testutil" 20 "github.com/moby/sys/mount" 21 "github.com/moby/sys/mountinfo" 22 "gotest.tools/v3/assert" 23 is "gotest.tools/v3/assert/cmp" 24 "gotest.tools/v3/fs" 25 "gotest.tools/v3/poll" 26 "gotest.tools/v3/skip" 27 ) 28 29 func TestContainerNetworkMountsNoChown(t *testing.T) { 30 // chown only applies to Linux bind mounted volumes; must be same host to verify 31 skip.If(t, testEnv.IsRemoteDaemon) 32 33 ctx := setupTest(t) 34 35 tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0o755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0o644))) 36 defer tmpDir.Remove() 37 38 tmpNWFileMount := tmpDir.Join("nwfile") 39 40 config := containertypes.Config{ 41 Image: "busybox", 42 } 43 hostConfig := containertypes.HostConfig{ 44 Mounts: []mounttypes.Mount{ 45 { 46 Type: "bind", 47 Source: tmpNWFileMount, 48 Target: "/etc/resolv.conf", 49 }, 50 { 51 Type: "bind", 52 Source: tmpNWFileMount, 53 Target: "/etc/hostname", 54 }, 55 { 56 Type: "bind", 57 Source: tmpNWFileMount, 58 Target: "/etc/hosts", 59 }, 60 }, 61 } 62 63 cli, err := client.NewClientWithOpts(client.FromEnv) 64 assert.NilError(t, err) 65 defer cli.Close() 66 67 ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "") 68 assert.NilError(t, err) 69 // container will exit immediately because of no tty, but we only need the start sequence to test the condition 70 err = cli.ContainerStart(ctx, ctrCreate.ID, containertypes.StartOptions{}) 71 assert.NilError(t, err) 72 73 // Check that host-located bind mount network file did not change ownership when the container was started 74 // Note: If the user specifies a mountpath from the host, we should not be 75 // attempting to chown files outside the daemon's metadata directory 76 // (represented by `daemon.repository` at init time). 77 // This forces users who want to use user namespaces to handle the 78 // ownership needs of any external files mounted as network files 79 // (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the 80 // daemon. In all other volume/bind mount situations we have taken this 81 // same line--we don't chown host file content. 82 // See GitHub PR 34224 for details. 83 info, err := os.Stat(tmpNWFileMount) 84 assert.NilError(t, err) 85 fi := info.Sys().(*syscall.Stat_t) 86 assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root") 87 } 88 89 func TestMountDaemonRoot(t *testing.T) { 90 skip.If(t, testEnv.IsRemoteDaemon) 91 92 ctx := setupTest(t) 93 apiClient := testEnv.APIClient() 94 info, err := apiClient.Info(ctx) 95 if err != nil { 96 t.Fatal(err) 97 } 98 99 for _, test := range []struct { 100 desc string 101 propagation mounttypes.Propagation 102 expected mounttypes.Propagation 103 }{ 104 { 105 desc: "default", 106 propagation: "", 107 expected: mounttypes.PropagationRSlave, 108 }, 109 { 110 desc: "private", 111 propagation: mounttypes.PropagationPrivate, 112 }, 113 { 114 desc: "rprivate", 115 propagation: mounttypes.PropagationRPrivate, 116 }, 117 { 118 desc: "slave", 119 propagation: mounttypes.PropagationSlave, 120 }, 121 { 122 desc: "rslave", 123 propagation: mounttypes.PropagationRSlave, 124 expected: mounttypes.PropagationRSlave, 125 }, 126 { 127 desc: "shared", 128 propagation: mounttypes.PropagationShared, 129 }, 130 { 131 desc: "rshared", 132 propagation: mounttypes.PropagationRShared, 133 expected: mounttypes.PropagationRShared, 134 }, 135 } { 136 t.Run(test.desc, func(t *testing.T) { 137 test := test 138 t.Parallel() 139 140 ctx := testutil.StartSpan(ctx, t) 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 ctx := testutil.StartSpan(ctx, t) 178 179 c, err := apiClient.ContainerCreate(ctx, &containertypes.Config{ 180 Image: "busybox", 181 Cmd: []string{"true"}, 182 }, hc, nil, nil, "") 183 if err != nil { 184 if test.expected != "" { 185 t.Fatal(err) 186 } 187 // expected an error, so this is ok and should not continue 188 return 189 } 190 if test.expected == "" { 191 t.Fatal("expected create to fail") 192 } 193 194 defer func() { 195 if err := apiClient.ContainerRemove(ctx, c.ID, containertypes.RemoveOptions{Force: true}); err != nil { 196 panic(err) 197 } 198 }() 199 200 inspect, err := apiClient.ContainerInspect(ctx, c.ID) 201 if err != nil { 202 t.Fatal(err) 203 } 204 if len(inspect.Mounts) != 1 { 205 t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts) 206 } 207 208 m := inspect.Mounts[0] 209 if m.Propagation != test.expected { 210 t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation) 211 } 212 }) 213 } 214 }) 215 } 216 } 217 218 func TestContainerBindMountNonRecursive(t *testing.T) { 219 skip.If(t, testEnv.IsRemoteDaemon) 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 ctx := setupTest(t) 223 224 tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o755), 225 fs.WithDir("mnt", fs.WithMode(0o755))) 226 defer tmpDir1.Remove() 227 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") 228 tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o755), 229 fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0o644))) 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 apiClient := testEnv.APIClient() 261 containers := []string{ 262 container.Run(ctx, t, apiClient, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)), 263 container.Run(ctx, t, apiClient, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)), 264 container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), 265 } 266 267 for _, c := range containers { 268 poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c), poll.WithDelay(100*time.Millisecond)) 269 } 270 } 271 272 func TestContainerVolumesMountedAsShared(t *testing.T) { 273 // Volume propagation is linux only. Also it creates directories for 274 // bind mounting, so needs to be same host. 275 skip.If(t, testEnv.IsRemoteDaemon) 276 skip.If(t, testEnv.IsUserNamespace) 277 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)") 278 279 ctx := setupTest(t) 280 281 // Prepare a source directory to bind mount 282 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), 283 fs.WithDir("mnt1", fs.WithMode(0o755))) 284 defer tmpDir1.Remove() 285 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1") 286 287 // Convert this directory into a shared mount point so that we do 288 // not rely on propagation properties of parent mount. 289 if err := mount.MakePrivate(tmpDir1.Path()); err != nil { 290 t.Fatal(err) 291 } 292 defer func() { 293 if err := mount.Unmount(tmpDir1.Path()); err != nil { 294 t.Fatal(err) 295 } 296 }() 297 if err := mount.MakeShared(tmpDir1.Path()); err != nil { 298 t.Fatal(err) 299 } 300 301 sharedMount := mounttypes.Mount{ 302 Type: mounttypes.TypeBind, 303 Source: tmpDir1.Path(), 304 Target: "/volume-dest", 305 BindOptions: &mounttypes.BindOptions{ 306 Propagation: mounttypes.PropagationShared, 307 }, 308 } 309 310 bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"} 311 312 apiClient := testEnv.APIClient() 313 containerID := container.Run(ctx, t, apiClient, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...)) 314 poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, containerID), poll.WithDelay(100*time.Millisecond)) 315 316 // Make sure a bind mount under a shared volume propagated to host. 317 if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted { 318 t.Fatalf("Bind mount under shared volume did not propagate to host") 319 } 320 321 mount.Unmount(tmpDir1Mnt) 322 } 323 324 func TestContainerVolumesMountedAsSlave(t *testing.T) { 325 // Volume propagation is linux only. Also it creates directories for 326 // bind mounting, so needs to be same host. 327 skip.If(t, testEnv.IsRemoteDaemon) 328 skip.If(t, testEnv.IsUserNamespace) 329 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)") 330 331 ctx := testutil.StartSpan(baseContext, t) 332 333 // Prepare a source directory to bind mount 334 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), 335 fs.WithDir("mnt1", fs.WithMode(0o755))) 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(0o755), 342 fs.WithFile("slave-testfile", "Test", fs.WithMode(0o644))) 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 apiClient := testEnv.APIClient() 371 containerID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...)) 372 373 // Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside 374 // container then contents of tmpDir2/slave-testfile should become 375 // visible at "/volume-dest/mnt1/slave-testfile" 376 if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { 377 t.Fatal(err) 378 } 379 defer func() { 380 if err := mount.Unmount(tmpDir1Mnt); err != nil { 381 t.Fatal(err) 382 } 383 }() 384 385 mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"} 386 387 if result, err := container.Exec(ctx, apiClient, containerID, mountCmd); err == nil { 388 if result.Stdout() != "Test" { 389 t.Fatalf("Bind mount under slave volume did not propagate to container") 390 } 391 } else { 392 t.Fatal(err) 393 } 394 } 395 396 // Regression test for #38995 and #43390. 397 func TestContainerCopyLeaksMounts(t *testing.T) { 398 ctx := setupTest(t) 399 400 bindMount := mounttypes.Mount{ 401 Type: mounttypes.TypeBind, 402 Source: "/var", 403 Target: "/hostvar", 404 BindOptions: &mounttypes.BindOptions{ 405 Propagation: mounttypes.PropagationRSlave, 406 }, 407 } 408 409 apiClient := testEnv.APIClient() 410 cid := container.Run(ctx, t, apiClient, container.WithMount(bindMount), container.WithCmd("sleep", "120s")) 411 412 getMounts := func() string { 413 t.Helper() 414 res, err := container.Exec(ctx, apiClient, cid, []string{"cat", "/proc/self/mountinfo"}) 415 assert.NilError(t, err) 416 assert.Equal(t, res.ExitCode, 0) 417 return res.Stdout() 418 } 419 420 mountsBefore := getMounts() 421 422 _, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd") 423 assert.NilError(t, err) 424 425 mountsAfter := getMounts() 426 427 assert.Equal(t, mountsBefore, mountsAfter) 428 } 429 430 func TestContainerBindMountReadOnlyDefault(t *testing.T) { 431 skip.If(t, testEnv.IsRemoteDaemon) 432 skip.If(t, !isRROSupported(), "requires recursive read-only mounts") 433 434 ctx := setupTest(t) 435 436 // The test will run a container with a simple readonly /dev bind mount (-v /dev:/dev:ro) 437 // It will then check /proc/self/mountinfo for the mount type of /dev/shm (submount of /dev) 438 // If /dev/shm is rw, that will mean that the read-only mounts are NOT recursive by default. 439 const nonRecursive = " /dev/shm rw," 440 // If /dev/shm is ro, that will mean that the read-only mounts ARE recursive by default. 441 const recursive = " /dev/shm ro," 442 443 for _, tc := range []struct { 444 clientVersion string 445 expectedOut string 446 name string 447 }{ 448 {clientVersion: "", expectedOut: recursive, name: "latest should be the same as 1.44"}, 449 {clientVersion: "1.44", expectedOut: recursive, name: "submount should be recursive by default on 1.44"}, 450 451 {clientVersion: "1.43", expectedOut: nonRecursive, name: "older than 1.44 should be non-recursive by default"}, 452 453 // TODO: Remove when MinSupportedAPIVersion >= 1.44 454 {clientVersion: api.MinSupportedAPIVersion, expectedOut: nonRecursive, name: "minimum API should be non-recursive by default"}, 455 } { 456 t.Run(tc.name, func(t *testing.T) { 457 apiClient := testEnv.APIClient() 458 459 minDaemonVersion := tc.clientVersion 460 if minDaemonVersion == "" { 461 minDaemonVersion = "1.44" 462 } 463 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), minDaemonVersion), "requires API v"+minDaemonVersion) 464 465 if tc.clientVersion != "" { 466 c, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(tc.clientVersion)) 467 assert.NilError(t, err, "failed to create client with version v%s", tc.clientVersion) 468 apiClient = c 469 } 470 471 for _, tc2 := range []struct { 472 subname string 473 mountOpt func(*container.TestContainerConfig) 474 }{ 475 {"mount", container.WithMount(mounttypes.Mount{ 476 Type: mounttypes.TypeBind, 477 Source: "/dev", 478 Target: "/dev", 479 ReadOnly: true, 480 })}, 481 {"bind mount", container.WithBindRaw("/dev:/dev:ro")}, 482 } { 483 t.Run(tc2.subname, func(t *testing.T) { 484 cid := container.Run(ctx, t, apiClient, tc2.mountOpt, 485 container.WithCmd("sh", "-c", "grep /dev/shm /proc/self/mountinfo"), 486 ) 487 out, err := container.Output(ctx, apiClient, cid) 488 assert.NilError(t, err) 489 490 assert.Check(t, is.Equal(out.Stderr, "")) 491 // Output should be either: 492 // 545 526 0:160 / /dev/shm ro,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k 493 // or 494 // 545 526 0:160 / /dev/shm rw,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k 495 assert.Check(t, is.Contains(out.Stdout, tc.expectedOut)) 496 }) 497 } 498 }) 499 } 500 } 501 502 func TestContainerBindMountRecursivelyReadOnly(t *testing.T) { 503 skip.If(t, testEnv.IsRemoteDaemon) 504 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44") 505 506 ctx := setupTest(t) 507 508 // 0o777 for allowing rootless containers to write to this directory 509 tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o777), 510 fs.WithDir("mnt", fs.WithMode(0o777))) 511 defer tmpDir1.Remove() 512 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") 513 tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o777), 514 fs.WithFile("file", "should not be writable when recursively read only", fs.WithMode(0o666))) 515 defer tmpDir2.Remove() 516 517 if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { 518 t.Fatal(err) 519 } 520 defer func() { 521 if err := mount.Unmount(tmpDir1Mnt); err != nil { 522 t.Fatal(err) 523 } 524 }() 525 526 rroSupported := isRROSupported() 527 528 nonRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? = 0 ]`} 529 forceRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? != 0 ]`} 530 531 // ro (recursive if kernel >= 5.12) 532 ro := mounttypes.Mount{ 533 Type: mounttypes.TypeBind, 534 Source: tmpDir1.Path(), 535 Target: "/foo", 536 ReadOnly: true, 537 BindOptions: &mounttypes.BindOptions{ 538 Propagation: mounttypes.PropagationRPrivate, 539 }, 540 } 541 roAsStr := ro.Source + ":" + ro.Target + ":ro,rprivate" 542 roVerifier := nonRecursiveVerifier 543 if rroSupported { 544 roVerifier = forceRecursiveVerifier 545 } 546 547 // Non-recursive 548 nonRecursive := ro 549 nonRecursive.BindOptions = &mounttypes.BindOptions{ 550 ReadOnlyNonRecursive: true, 551 Propagation: mounttypes.PropagationRPrivate, 552 } 553 554 // Force recursive 555 forceRecursive := ro 556 forceRecursive.BindOptions = &mounttypes.BindOptions{ 557 ReadOnlyForceRecursive: true, 558 Propagation: mounttypes.PropagationRPrivate, 559 } 560 561 apiClient := testEnv.APIClient() 562 563 containers := []string{ 564 container.Run(ctx, t, apiClient, container.WithMount(ro), container.WithCmd(roVerifier...)), 565 container.Run(ctx, t, apiClient, container.WithBindRaw(roAsStr), container.WithCmd(roVerifier...)), 566 567 container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), 568 } 569 570 if rroSupported { 571 containers = append(containers, 572 container.Run(ctx, t, apiClient, container.WithMount(forceRecursive), container.WithCmd(forceRecursiveVerifier...)), 573 ) 574 } 575 576 for _, c := range containers { 577 poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c), poll.WithDelay(100*time.Millisecond)) 578 } 579 } 580 581 func isRROSupported() bool { 582 return kernel.CheckKernelVersion(5, 12, 0) 583 }