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  }