github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/integration/container/mounts_linux_test.go (about) 1 package container // import "github.com/Prakhar-Agarwal-byte/moby/integration/container" 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "syscall" 8 "testing" 9 "time" 10 11 containertypes "github.com/Prakhar-Agarwal-byte/moby/api/types/container" 12 mounttypes "github.com/Prakhar-Agarwal-byte/moby/api/types/mount" 13 "github.com/Prakhar-Agarwal-byte/moby/api/types/network" 14 "github.com/Prakhar-Agarwal-byte/moby/api/types/versions" 15 "github.com/Prakhar-Agarwal-byte/moby/client" 16 "github.com/Prakhar-Agarwal-byte/moby/integration/internal/container" 17 "github.com/Prakhar-Agarwal-byte/moby/pkg/parsers/kernel" 18 "github.com/Prakhar-Agarwal-byte/moby/testutil" 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 ctx := setupTest(t) 33 34 tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0o755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0o644))) 35 defer tmpDir.Remove() 36 37 tmpNWFileMount := tmpDir.Join("nwfile") 38 39 config := containertypes.Config{ 40 Image: "busybox", 41 } 42 hostConfig := containertypes.HostConfig{ 43 Mounts: []mounttypes.Mount{ 44 { 45 Type: "bind", 46 Source: tmpNWFileMount, 47 Target: "/etc/resolv.conf", 48 }, 49 { 50 Type: "bind", 51 Source: tmpNWFileMount, 52 Target: "/etc/hostname", 53 }, 54 { 55 Type: "bind", 56 Source: tmpNWFileMount, 57 Target: "/etc/hosts", 58 }, 59 }, 60 } 61 62 cli, err := client.NewClientWithOpts(client.FromEnv) 63 assert.NilError(t, err) 64 defer cli.Close() 65 66 ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "") 67 assert.NilError(t, err) 68 // container will exit immediately because of no tty, but we only need the start sequence to test the condition 69 err = cli.ContainerStart(ctx, ctrCreate.ID, containertypes.StartOptions{}) 70 assert.NilError(t, err) 71 72 // Check that host-located bind mount network file did not change ownership when the container was started 73 // Note: If the user specifies a mountpath from the host, we should not be 74 // attempting to chown files outside the daemon's metadata directory 75 // (represented by `daemon.repository` at init time). 76 // This forces users who want to use user namespaces to handle the 77 // ownership needs of any external files mounted as network files 78 // (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the 79 // daemon. In all other volume/bind mount situations we have taken this 80 // same line--we don't chown host file content. 81 // See GitHub PR 34224 for details. 82 info, err := os.Stat(tmpNWFileMount) 83 assert.NilError(t, err) 84 fi := info.Sys().(*syscall.Stat_t) 85 assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root") 86 } 87 88 func TestMountDaemonRoot(t *testing.T) { 89 skip.If(t, testEnv.IsRemoteDaemon) 90 91 ctx := setupTest(t) 92 apiClient := testEnv.APIClient() 93 info, err := apiClient.Info(ctx) 94 if err != nil { 95 t.Fatal(err) 96 } 97 98 for _, test := range []struct { 99 desc string 100 propagation mounttypes.Propagation 101 expected mounttypes.Propagation 102 }{ 103 { 104 desc: "default", 105 propagation: "", 106 expected: mounttypes.PropagationRSlave, 107 }, 108 { 109 desc: "private", 110 propagation: mounttypes.PropagationPrivate, 111 }, 112 { 113 desc: "rprivate", 114 propagation: mounttypes.PropagationRPrivate, 115 }, 116 { 117 desc: "slave", 118 propagation: mounttypes.PropagationSlave, 119 }, 120 { 121 desc: "rslave", 122 propagation: mounttypes.PropagationRSlave, 123 expected: mounttypes.PropagationRSlave, 124 }, 125 { 126 desc: "shared", 127 propagation: mounttypes.PropagationShared, 128 }, 129 { 130 desc: "rshared", 131 propagation: mounttypes.PropagationRShared, 132 expected: mounttypes.PropagationRShared, 133 }, 134 } { 135 t.Run(test.desc, func(t *testing.T) { 136 test := test 137 t.Parallel() 138 139 ctx := testutil.StartSpan(ctx, t) 140 141 propagationSpec := fmt.Sprintf(":%s", test.propagation) 142 if test.propagation == "" { 143 propagationSpec = "" 144 } 145 bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec 146 bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec 147 148 for name, hc := range map[string]*containertypes.HostConfig{ 149 "bind root": {Binds: []string{bindSpecRoot}}, 150 "bind subpath": {Binds: []string{bindSpecSub}}, 151 "mount root": { 152 Mounts: []mounttypes.Mount{ 153 { 154 Type: mounttypes.TypeBind, 155 Source: info.DockerRootDir, 156 Target: "/foo", 157 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 158 }, 159 }, 160 }, 161 "mount subpath": { 162 Mounts: []mounttypes.Mount{ 163 { 164 Type: mounttypes.TypeBind, 165 Source: filepath.Join(info.DockerRootDir, "containers"), 166 Target: "/foo", 167 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 168 }, 169 }, 170 }, 171 } { 172 t.Run(name, func(t *testing.T) { 173 hc := hc 174 t.Parallel() 175 176 ctx := testutil.StartSpan(ctx, t) 177 178 c, err := apiClient.ContainerCreate(ctx, &containertypes.Config{ 179 Image: "busybox", 180 Cmd: []string{"true"}, 181 }, hc, nil, nil, "") 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 := apiClient.ContainerRemove(ctx, c.ID, containertypes.RemoveOptions{Force: true}); err != nil { 195 panic(err) 196 } 197 }() 198 199 inspect, err := apiClient.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 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 TestContainerBindMountRecursivelyReadOnly(t *testing.T) { 431 skip.If(t, testEnv.IsRemoteDaemon) 432 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44") 433 434 ctx := setupTest(t) 435 436 // 0o777 for allowing rootless containers to write to this directory 437 tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o777), 438 fs.WithDir("mnt", fs.WithMode(0o777))) 439 defer tmpDir1.Remove() 440 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") 441 tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o777), 442 fs.WithFile("file", "should not be writable when recursively read only", fs.WithMode(0o666))) 443 defer tmpDir2.Remove() 444 445 if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { 446 t.Fatal(err) 447 } 448 defer func() { 449 if err := mount.Unmount(tmpDir1Mnt); err != nil { 450 t.Fatal(err) 451 } 452 }() 453 454 rroSupported := kernel.CheckKernelVersion(5, 12, 0) 455 456 nonRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? = 0 ]`} 457 forceRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? != 0 ]`} 458 459 // ro (recursive if kernel >= 5.12) 460 ro := mounttypes.Mount{ 461 Type: mounttypes.TypeBind, 462 Source: tmpDir1.Path(), 463 Target: "/foo", 464 ReadOnly: true, 465 BindOptions: &mounttypes.BindOptions{ 466 Propagation: mounttypes.PropagationRPrivate, 467 }, 468 } 469 roAsStr := ro.Source + ":" + ro.Target + ":ro,rprivate" 470 roVerifier := nonRecursiveVerifier 471 if rroSupported { 472 roVerifier = forceRecursiveVerifier 473 } 474 475 // Non-recursive 476 nonRecursive := ro 477 nonRecursive.BindOptions = &mounttypes.BindOptions{ 478 ReadOnlyNonRecursive: true, 479 Propagation: mounttypes.PropagationRPrivate, 480 } 481 482 // Force recursive 483 forceRecursive := ro 484 forceRecursive.BindOptions = &mounttypes.BindOptions{ 485 ReadOnlyForceRecursive: true, 486 Propagation: mounttypes.PropagationRPrivate, 487 } 488 489 apiClient := testEnv.APIClient() 490 491 containers := []string{ 492 container.Run(ctx, t, apiClient, container.WithMount(ro), container.WithCmd(roVerifier...)), 493 container.Run(ctx, t, apiClient, container.WithBindRaw(roAsStr), container.WithCmd(roVerifier...)), 494 495 container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), 496 } 497 498 if rroSupported { 499 containers = append(containers, 500 container.Run(ctx, t, apiClient, container.WithMount(forceRecursive), container.WithCmd(forceRecursiveVerifier...)), 501 ) 502 } 503 504 for _, c := range containers { 505 poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c), poll.WithDelay(100*time.Millisecond)) 506 } 507 }