From 5045a2de24e62095278bd10b7abce4153d713b2a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 5 Apr 2023 20:32:03 +0900 Subject: [PATCH] Support recursively read-only (RRO) mounts `docker run -v /foo:/foo:ro` is now recursively read-only on kernel >= 5.12. Automatically falls back to the legacy non-recursively read-only mount mode on kernel < 5.12. Use `ro-non-recursive` to disable RRO. Use `ro-force-recursive` or `rro` to explicitly enable RRO. (Fails on kernel < 5.12) Fix issue 44978 Fix docker/for-linux issue 788 Signed-off-by: Akihiro Suda --- .../router/container/container_routes.go | 12 +++ api/swagger.yaml | 10 +++ api/types/mount/mount.go | 7 +- api/types/types.go | 4 +- container/mounts_unix.go | 14 +-- daemon/containerfs_linux.go | 40 +++++++++ daemon/daemon_linux.go | 75 ++++++++++++++++ daemon/daemon_unsupported.go | 4 + daemon/daemon_windows.go | 4 + daemon/oci_linux.go | 19 ++++- daemon/runtime_unix.go | 16 ++++ daemon/volumes_unix.go | 12 +++ docs/api/version-history.md | 4 + integration/container/mounts_linux_test.go | 85 +++++++++++++++++++ integration/internal/container/ops.go | 7 ++ volume/mounts/linux_parser.go | 23 ++++- volume/mounts/linux_parser_test.go | 48 +++++++---- volume/mounts/parser.go | 7 +- 18 files changed, 361 insertions(+), 30 deletions(-) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index b4aa0864fb..1d14de8b42 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -588,6 +588,18 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo hostConfig.PidsLimit = nil } + if hostConfig != nil && versions.LessThan(version, "1.44") { + for _, m := range hostConfig.Mounts { + if m.BindOptions != nil { + // Ignore ReadOnlyNonRecursive because it was added in API 1.44. + m.BindOptions.ReadOnlyNonRecursive = false + if m.BindOptions.ReadOnlyForceRecursive { + return errdefs.InvalidParameter(errors.New("BindOptions.ReadOnlyForceRecursive needs API v1.44 or newer")) + } + } + } + } + ccr, err := s.backend.ContainerCreate(ctx, types.ContainerCreateConfig{ Name: name, Config: config, diff --git a/api/swagger.yaml b/api/swagger.yaml index 8364e7bf4b..f761a30708 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -388,6 +388,16 @@ definitions: description: "Create mount point on host if missing" type: "boolean" default: false + ReadOnlyNonRecursive: + description: | + Make the mount non-recursively read-only, but still leave the mount recursive + (unless NonRecursive is set to true in conjunction). + type: "boolean" + default: false + ReadOnlyForceRecursive: + description: "Raise an error if the mount cannot be made recursively read-only." + type: "boolean" + default: false VolumeOptions: description: "Optional configuration for the `volume` type." type: "object" diff --git a/api/types/mount/mount.go b/api/types/mount/mount.go index ac4ce62231..57edf2ef18 100644 --- a/api/types/mount/mount.go +++ b/api/types/mount/mount.go @@ -29,7 +29,7 @@ type Mount struct { // Source is not supported for tmpfs (must be an empty value) Source string `json:",omitempty"` Target string `json:",omitempty"` - ReadOnly bool `json:",omitempty"` + ReadOnly bool `json:",omitempty"` // attempts recursive read-only if possible Consistency Consistency `json:",omitempty"` BindOptions *BindOptions `json:",omitempty"` @@ -85,6 +85,11 @@ type BindOptions struct { Propagation Propagation `json:",omitempty"` NonRecursive bool `json:",omitempty"` CreateMountpoint bool `json:",omitempty"` + // ReadOnlyNonRecursive makes the mount non-recursively read-only, but still leaves the mount recursive + // (unless NonRecursive is set to true in conjunction). + ReadOnlyNonRecursive bool `json:",omitempty"` + // ReadOnlyForceRecursive raises an error if the mount cannot be made recursively read-only. + ReadOnlyForceRecursive bool `json:",omitempty"` } // VolumeOptions represents the options for a mount of type volume. diff --git a/api/types/types.go b/api/types/types.go index d6aa3d6385..04be8e513b 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/volume" "github.com/docker/go-connections/nat" + "github.com/opencontainers/runtime-spec/specs-go/features" ) const ( @@ -658,7 +659,8 @@ type Runtime struct { Options map[string]interface{} `json:"options,omitempty"` // This is exposed here only for internal use - ShimConfig *ShimConfig `json:"-"` + ShimConfig *ShimConfig `json:"-"` + Features *features.Features `json:"-"` } // ShimConfig is used by runtime to configure containerd shims diff --git a/container/mounts_unix.go b/container/mounts_unix.go index 438b289cf4..14c8d36c12 100644 --- a/container/mounts_unix.go +++ b/container/mounts_unix.go @@ -4,10 +4,12 @@ package container // import "github.com/docker/docker/container" // Mount contains information for a mount operation. type Mount struct { - Source string `json:"source"` - Destination string `json:"destination"` - Writable bool `json:"writable"` - Data string `json:"data"` - Propagation string `json:"mountpropagation"` - NonRecursive bool `json:"nonrecursive"` + Source string `json:"source"` + Destination string `json:"destination"` + Writable bool `json:"writable"` + Data string `json:"data"` + Propagation string `json:"mountpropagation"` + NonRecursive bool `json:"nonrecursive"` + ReadOnlyNonRecursive bool `json:"readonlynonrecursive"` + ReadOnlyForceRecursive bool `json:"readonlyforcerecursive"` } diff --git a/daemon/containerfs_linux.go b/daemon/containerfs_linux.go index b7420b9244..747112fd15 100644 --- a/daemon/containerfs_linux.go +++ b/daemon/containerfs_linux.go @@ -2,6 +2,8 @@ package daemon // import "github.com/docker/docker/daemon" import ( "context" + "errors" + "fmt" "os" "path/filepath" "runtime" @@ -10,6 +12,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/moby/sys/mount" "github.com/moby/sys/symlink" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" "github.com/docker/docker/api/types" @@ -102,6 +105,15 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai writeMode := "ro" if m.Writable { writeMode = "rw" + if m.ReadOnlyNonRecursive { + return errors.New("options conflict: Writable && ReadOnlyNonRecursive") + } + if m.ReadOnlyForceRecursive { + return errors.New("options conflict: Writable && ReadOnlyForceRecursive") + } + } + if m.ReadOnlyNonRecursive && m.ReadOnlyForceRecursive { + return errors.New("options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive") } // openContainerFS() is called for temporary mounts @@ -118,6 +130,16 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai if err := mount.Mount(m.Source, dest, "", opts); err != nil { return err } + + if !m.Writable && !m.ReadOnlyNonRecursive { + if err := makeMountRRO(dest); err != nil { + if m.ReadOnlyForceRecursive { + return err + } else { + logrus.WithError(err).Debugf("Failed to make %q recursively read-only", dest) + } + } + } } return mounttree.SwitchRoot(container.BaseFS) @@ -219,3 +241,21 @@ func (vw *containerFSView) Stat(ctx context.Context, path string) (*types.Contai }) return stat, err } + +// makeMountRRO makes the mount recursively read-only. +func makeMountRRO(dest string) error { + attr := &unix.MountAttr{ + Attr_set: unix.MOUNT_ATTR_RDONLY, + } + var err error + for { + err = unix.MountSetattr(-1, dest, unix.AT_RECURSIVE, attr) + if !errors.Is(err, unix.EINTR) { + break + } + } + if err != nil { + err = fmt.Errorf("failed to apply MOUNT_ATTR_RDONLY with AT_RECURSIVE to %q: %w", dest, err) + } + return err +} diff --git a/daemon/daemon_linux.go b/daemon/daemon_linux.go index 80dff4ac95..4a94f1064e 100644 --- a/daemon/daemon_linux.go +++ b/daemon/daemon_linux.go @@ -8,6 +8,7 @@ import ( "os" "regexp" "strings" + "sync" "github.com/docker/docker/daemon/config" "github.com/docker/docker/libnetwork/ns" @@ -17,6 +18,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" ) // On Linux, plugins use a static path for storing execution state, @@ -182,3 +184,76 @@ func ifaceAddrs(linkName string) (v4, v6 []*net.IPNet, err error) { } return v4, v6, nil } + +var ( + kernelSupportsRROOnce sync.Once + kernelSupportsRROErr error +) + +func kernelSupportsRecursivelyReadOnly() error { + fn := func() error { + tmpMnt, err := os.MkdirTemp("", "moby-detect-rro") + if err != nil { + return fmt.Errorf("failed to create a temp directory: %w", err) + } + for { + err = unix.Mount("", tmpMnt, "tmpfs", 0, "") + if !errors.Is(err, unix.EINTR) { + break + } + } + if err != nil { + return fmt.Errorf("failed to mount tmpfs on %q: %w", tmpMnt, err) + } + defer func() { + var umErr error + for { + umErr = unix.Unmount(tmpMnt, 0) + if !errors.Is(umErr, unix.EINTR) { + break + } + } + if umErr != nil { + logrus.WithError(umErr).Warnf("Failed to unmount %q", tmpMnt) + } + }() + attr := &unix.MountAttr{ + Attr_set: unix.MOUNT_ATTR_RDONLY, + } + for { + err = unix.MountSetattr(-1, tmpMnt, unix.AT_RECURSIVE, attr) + if !errors.Is(err, unix.EINTR) { + break + } + } + // ENOSYS on kernel < 5.12 + if err != nil { + return fmt.Errorf("failed to call mount_setattr: %w", err) + } + return nil + } + + kernelSupportsRROOnce.Do(func() { + kernelSupportsRROErr = fn() + }) + return kernelSupportsRROErr +} + +func (daemon *Daemon) supportsRecursivelyReadOnly(runtime string) error { + if err := kernelSupportsRecursivelyReadOnly(); err != nil { + return fmt.Errorf("rro is not supported: %w (kernel is older than 5.12?)", err) + } + if runtime == "" { + runtime = daemon.configStore.GetDefaultRuntimeName() + } + rt := daemon.configStore.GetRuntime(runtime) + if rt.Features == nil { + return fmt.Errorf("rro is not supported by runtime %q: OCI features struct is not available", runtime) + } + for _, s := range rt.Features.MountOptions { + if s == "rro" { + return nil + } + } + return fmt.Errorf("rro is not supported by runtime %q", runtime) +} diff --git a/daemon/daemon_unsupported.go b/daemon/daemon_unsupported.go index c3d419306c..ac776257c4 100644 --- a/daemon/daemon_unsupported.go +++ b/daemon/daemon_unsupported.go @@ -17,3 +17,7 @@ func setupResolvConf(_ *interface{}) {} func getSysInfo(_ *Daemon) *sysinfo.SysInfo { return sysinfo.New() } + +func (daemon *Daemon) supportsRecursivelyReadOnly(_ string) error { + return nil +} diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go index b6ced4af02..928a36b008 100644 --- a/daemon/daemon_windows.go +++ b/daemon/daemon_windows.go @@ -604,3 +604,7 @@ func (daemon *Daemon) initLibcontainerd(ctx context.Context) error { return err } + +func (daemon *Daemon) supportsRecursivelyReadOnly(_ string) error { + return nil +} diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 015a429944..37cdd6b7a2 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -645,7 +645,24 @@ func WithMounts(daemon *Daemon, c *container.Container) coci.SpecOpts { } opts := []string{bindMode} if !m.Writable { - opts = append(opts, "ro") + rro := true + if m.ReadOnlyNonRecursive { + rro = false + if m.ReadOnlyForceRecursive { + return errors.New("mount options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive") + } + } + if rroErr := daemon.supportsRecursivelyReadOnly(c.HostConfig.Runtime); rroErr != nil { + rro = false + if m.ReadOnlyForceRecursive { + return rroErr + } + } + if rro { + opts = append(opts, "rro") + } else { + opts = append(opts, "ro") + } } if pFlag != 0 { opts = append(opts, mountPropagationReverseMap[pFlag]) diff --git a/daemon/runtime_unix.go b/daemon/runtime_unix.go index be42d29f34..1dd37e5dc4 100644 --- a/daemon/runtime_unix.go +++ b/daemon/runtime_unix.go @@ -3,6 +3,8 @@ package daemon import ( + "bytes" + "encoding/json" "fmt" "os" "os/exec" @@ -14,6 +16,7 @@ import ( "github.com/docker/docker/daemon/config" "github.com/docker/docker/errdefs" "github.com/docker/docker/libcontainerd/shimopts" + "github.com/opencontainers/runtime-spec/specs-go/features" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -109,6 +112,19 @@ func (daemon *Daemon) initRuntimes(runtimes map[string]types.Runtime) (err error } } rt.ShimConfig = defaultV2ShimConfig(daemon.configStore, daemon.rewriteRuntimePath(name, rt.Path, rt.Args)) + var featuresStderr bytes.Buffer + featuresCmd := exec.Command(rt.Path, append(rt.Args, "features")...) + featuresCmd.Stderr = &featuresStderr + if featuresB, err := featuresCmd.Output(); err != nil { + logrus.WithError(err).Warnf("Failed to run %v: %q", featuresCmd.Args, featuresStderr.String()) + } else { + var features features.Features + if jsonErr := json.Unmarshal(featuresB, &features); jsonErr != nil { + logrus.WithError(err).Warnf("Failed to unmarshal the output of %v as a JSON", featuresCmd.Args) + } else { + rt.Features = &features + } + } } else { if len(rt.Args) > 0 { return errors.Errorf("runtime %s: args cannot be used with a runtimeType runtime", name) diff --git a/daemon/volumes_unix.go b/daemon/volumes_unix.go index 466f8bcbad..abfa070fd1 100644 --- a/daemon/volumes_unix.go +++ b/daemon/volumes_unix.go @@ -12,6 +12,7 @@ import ( mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/container" volumemounts "github.com/docker/docker/volume/mounts" + "github.com/pkg/errors" ) // setupMounts iterates through each of the mount points for a container and @@ -58,7 +59,18 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er Propagation: string(m.Propagation), } if m.Spec.Type == mounttypes.TypeBind && m.Spec.BindOptions != nil { + if !m.Spec.ReadOnly && m.Spec.BindOptions.ReadOnlyNonRecursive { + return nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyNonRecursive") + } + if !m.Spec.ReadOnly && m.Spec.BindOptions.ReadOnlyForceRecursive { + return nil, errors.New("mount options conflict: !ReadOnly && BindOptions.ReadOnlyForceRecursive") + } + if m.Spec.BindOptions.ReadOnlyNonRecursive && m.Spec.BindOptions.ReadOnlyForceRecursive { + return nil, errors.New("mount options conflict: ReadOnlyNonRecursive && BindOptions.ReadOnlyForceRecursive") + } mnt.NonRecursive = m.Spec.BindOptions.NonRecursive + mnt.ReadOnlyNonRecursive = m.Spec.BindOptions.ReadOnlyNonRecursive + mnt.ReadOnlyForceRecursive = m.Spec.BindOptions.ReadOnlyForceRecursive } if m.Volume != nil { attributes := map[string]string{ diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 61d2545118..91795d7659 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -20,6 +20,10 @@ keywords: "API, Docker, rcli, REST, documentation" * The `VirtualSize` field in the `GET /images/{name}/json`, `GET /images/json`, and `GET /system/df` responses is now omitted. Use the `Size` field instead, which contains the same information. +* Read-only bind mounts are now made recursively read-only on kernel >= 5.12 + with runtimes which support the feature. + `POST /containers/create`, `GET /containers/{id}/json`, and `GET /containers/json` now supports + `BindOptions.ReadOnlyNonRecursive` and `BindOptions.ReadOnlyForceRecursive` to customize the behavior. ## v1.43 API changes diff --git a/integration/container/mounts_linux_test.go b/integration/container/mounts_linux_test.go index 405e16c1db..1627d76bc6 100644 --- a/integration/container/mounts_linux_test.go +++ b/integration/container/mounts_linux_test.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/container" + "github.com/docker/docker/pkg/parsers/kernel" "github.com/moby/sys/mount" "github.com/moby/sys/mountinfo" "gotest.tools/v3/assert" @@ -428,3 +429,87 @@ func TestContainerCopyLeaksMounts(t *testing.T) { assert.Equal(t, mountsBefore, mountsAfter) } + +func TestContainerBindMountRecursivelyReadOnly(t *testing.T) { + skip.If(t, testEnv.IsRemoteDaemon) + skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44") + + defer setupTest(t)() + + // 0o777 for allowing rootless containers to write to this directory + tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o777), + fs.WithDir("mnt", fs.WithMode(0o777))) + defer tmpDir1.Remove() + tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") + tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o777), + fs.WithFile("file", "should not be writable when recursively read only", fs.WithMode(0o666))) + defer tmpDir2.Remove() + + if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { + t.Fatal(err) + } + defer func() { + if err := mount.Unmount(tmpDir1Mnt); err != nil { + t.Fatal(err) + } + }() + + rroSupported := kernel.CheckKernelVersion(5, 12, 0) + + nonRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? = 0 ]`} + forceRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? != 0 ]`} + + // ro (recursive if kernel >= 5.12) + ro := mounttypes.Mount{ + Type: mounttypes.TypeBind, + Source: tmpDir1.Path(), + Target: "/foo", + ReadOnly: true, + BindOptions: &mounttypes.BindOptions{ + Propagation: mounttypes.PropagationRPrivate, + }, + } + roAsStr := ro.Source + ":" + ro.Target + ":ro,rprivate" + roVerifier := nonRecursiveVerifier + if rroSupported { + roVerifier = forceRecursiveVerifier + } + + // Non-recursive + nonRecursive := ro + nonRecursive.BindOptions = &mounttypes.BindOptions{ + ReadOnlyNonRecursive: true, + Propagation: mounttypes.PropagationRPrivate, + } + nonRecursiveAsStr := nonRecursive.Source + ":" + nonRecursive.Target + ":ro-non-recursive,rprivate" + + // Force recursive + forceRecursive := ro + forceRecursive.BindOptions = &mounttypes.BindOptions{ + ReadOnlyForceRecursive: true, + Propagation: mounttypes.PropagationRPrivate, + } + forceRecursiveAsStr := forceRecursive.Source + ":" + forceRecursive.Target + ":ro-force-recursive,rprivate" + + ctx := context.Background() + client := testEnv.APIClient() + + containers := []string{ + container.Run(ctx, t, client, container.WithMount(ro), container.WithCmd(roVerifier...)), + container.Run(ctx, t, client, container.WithBindRaw(roAsStr), container.WithCmd(roVerifier...)), + + container.Run(ctx, t, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), + container.Run(ctx, t, client, container.WithBindRaw(nonRecursiveAsStr), container.WithCmd(nonRecursiveVerifier...)), + } + + if rroSupported { + containers = append(containers, + container.Run(ctx, t, client, container.WithMount(forceRecursive), container.WithCmd(forceRecursiveVerifier...)), + container.Run(ctx, t, client, container.WithBindRaw(forceRecursiveAsStr), container.WithCmd(forceRecursiveVerifier...)), + ) + } + + for _, c := range containers { + poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond)) + } +} diff --git a/integration/internal/container/ops.go b/integration/internal/container/ops.go index a38bc81198..fbc3b6b33b 100644 --- a/integration/internal/container/ops.go +++ b/integration/internal/container/ops.go @@ -94,6 +94,13 @@ func WithBind(src, target string) func(*TestContainerConfig) { } } +// WithBindRaw sets the bind mount of the container +func WithBindRaw(s string) func(*TestContainerConfig) { + return func(c *TestContainerConfig) { + c.HostConfig.Binds = append(c.HostConfig.Binds, s) + } +} + // WithTmpfs sets a target path in the container to a tmpfs, with optional options // (separated with a colon). func WithTmpfs(targetAndOpts string) func(config *TestContainerConfig) { diff --git a/volume/mounts/linux_parser.go b/volume/mounts/linux_parser.go index 17bc3c9878..be90157f94 100644 --- a/volume/mounts/linux_parser.go +++ b/volume/mounts/linux_parser.go @@ -194,7 +194,7 @@ func (p *linuxParser) ReadWrite(mode string) bool { } for _, o := range strings.Split(mode, ",") { - if o == "ro" { + if o == "ro" || strings.HasPrefix(o, "ro-") || o == "rro" { return false } } @@ -262,6 +262,24 @@ func (p *linuxParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, erro } } + for _, m := range strings.Split(mode, ",") { + m = strings.TrimSpace(m) + if strings.HasPrefix(m, "ro-") || m == "rro" { + if spec.Type != mount.TypeBind { + return nil, fmt.Errorf("mount mode %q requires a bind mount: %w", mode, errInvalidSpec(raw)) + } + if spec.BindOptions == nil { + spec.BindOptions = &mount.BindOptions{} + } + switch m { + case "ro-non-recursive": + spec.BindOptions.ReadOnlyNonRecursive = true + case "ro-force-recursive", "rro": + spec.BindOptions.ReadOnlyForceRecursive = true + } + } + } + mp, err := p.parseMountSpec(spec, false) if mp != nil { mp.Mode = mode @@ -329,6 +347,9 @@ func (p *linuxParser) ParseVolumesFrom(spec string) (string, string, error) { if !linuxValidMountMode(mode) { return "", "", errInvalidMode(mode) } + if strings.HasPrefix(mode, "ro-") || mode == "rro" { + return "", "", fmt.Errorf("mount mode %q is not supported for volumes-from mounts: %w", mode, errInvalidMode(mode)) + } // For now don't allow propagation properties while importing // volumes from data container. These volumes will inherit // the same propagation property as of the original volume diff --git a/volume/mounts/linux_parser_test.go b/volume/mounts/linux_parser_test.go index bcfca72b25..12d57235b9 100644 --- a/volume/mounts/linux_parser_test.go +++ b/volume/mounts/linux_parser_test.go @@ -99,25 +99,30 @@ func TestLinuxParseMountRaw(t *testing.T) { func TestLinuxParseMountRawSplit(t *testing.T) { cases := []struct { - bind string - driver string - expType mount.Type - expDest string - expSource string - expName string - expDriver string - expRW bool - fail bool + bind string + driver string + expType mount.Type + expDest string + expSource string + expName string + expDriver string + expRW bool + expNonRRO bool + expForceRRO bool + fail bool }{ - {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false}, - {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false}, - {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false}, - {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true}, - {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false}, - {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false}, - {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false}, - {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false}, - {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true}, + {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false, false, false}, + {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false, false, false}, + {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false, false, false}, + {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, false, false, true}, + {"/tmp:/tmp5:ro-non-recursive", "", mount.TypeBind, "/tmp5", "/tmp", "", "", false, true, false, false}, + {"/tmp:/tmp6:ro-force-recursive,rprivate", "", mount.TypeBind, "/tmp6", "/tmp", "", "", false, false, true, false}, + {"/tmp:/tmp7:rro", "", mount.TypeBind, "/tmp7", "/tmp", "", "", false, false, true, false}, + {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false, false, false}, + {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false, false, false}, + {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false, false, false}, + {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false, false, false}, + {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, false, false, true}, } parser := NewLinuxParser() @@ -141,6 +146,13 @@ func TestLinuxParseMountRawSplit(t *testing.T) { assert.Equal(t, m.Driver, c.expDriver) assert.Equal(t, m.RW, c.expRW) assert.Equal(t, m.Type, c.expType) + var nonRRO, forceRRO bool + if m.Spec.BindOptions != nil { + nonRRO = m.Spec.BindOptions.ReadOnlyNonRecursive + forceRRO = m.Spec.BindOptions.ReadOnlyForceRecursive + } + assert.Equal(t, nonRRO, c.expNonRRO) + assert.Equal(t, forceRRO, c.expForceRRO) }) } } diff --git a/volume/mounts/parser.go b/volume/mounts/parser.go index 58107f490c..19e2c64689 100644 --- a/volume/mounts/parser.go +++ b/volume/mounts/parser.go @@ -13,8 +13,11 @@ var ErrVolumeTargetIsRoot = errors.New("invalid specification: destination can't // read-write modes var rwModes = map[string]bool{ - "rw": true, - "ro": true, + "rw": true, + "ro": true, // attempts recursive read-only if possible + "ro-non-recursive": true, // makes the mount non-recursively read-only, but still leaves the mount recursive + "ro-force-recursive": true, // raises an error if the mount cannot be made recursively read-only + "rro": true, // alias for ro-force-recursive } // Parser represents a platform specific parser for mount expressions