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  }