From 02264d597faa034c14393ab6c98591e85a2642ee Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 22 Aug 2019 11:21:20 -0400 Subject: Add support for 'exec', 'suid', 'dev' mount flags Previously, we explicitly set noexec/nosuid/nodev on every mount, with no ability to disable them. The 'mount' command on Linux will accept their inverses without complaint, though - 'noexec' is counteracted by 'exec', 'nosuid' by 'suid', etc. Add support for passing these options at the command line to disable our explicit forcing of security options. This also cleans up mount option handling significantly. We are still parsing options in more than one place, which isn't good, but option parsing for bind and tmpfs mounts has been unified. Fixes: #3819 Fixes: #3803 Signed-off-by: Matthew Heon --- pkg/util/mountOpts.go | 111 ++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 49 deletions(-) (limited to 'pkg/util') diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index 9b2c734c0..81dc4cefd 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -13,88 +13,101 @@ var ( ErrDupeMntOption = errors.Errorf("duplicate option passed") ) -// ProcessOptions parses the options for a bind mount and ensures that they are -// sensible and follow convention. -func ProcessOptions(options []string) []string { +// ProcessOptions parses the options for a bind or tmpfs mount and ensures that +// they are sensible and follow convention. The isTmpfs variable controls +// whether extra, tmpfs-specific options will be allowed. +func ProcessOptions(options []string, isTmpfs bool) ([]string, error) { var ( - foundbind, foundrw, foundro bool - rootProp string + foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind bool ) - for _, opt := range options { - switch opt { - case "bind", "rbind": - foundbind = true - case "ro": - foundro = true - case "rw": - foundrw = true - case "private", "rprivate", "slave", "rslave", "shared", "rshared": - rootProp = opt - } - } - if !foundbind { - options = append(options, "rbind") - } - if !foundrw && !foundro { - options = append(options, "rw") - } - if rootProp == "" { - options = append(options, "rprivate") - } - return options -} - -// ProcessTmpfsOptions parses the options for a tmpfs mountpoint and ensures -// that they are sensible and follow convention. -func ProcessTmpfsOptions(options []string) ([]string, error) { - var ( - foundWrite, foundSize, foundProp, foundMode bool - ) - - baseOpts := []string{"noexec", "nosuid", "nodev"} for _, opt := range options { // Some options have parameters - size, mode splitOpt := strings.SplitN(opt, "=", 2) switch splitOpt[0] { + case "exec", "noexec": + if foundExec { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'noexec' and 'exec' can be used") + } + foundExec = true + case "suid", "nosuid": + if foundSuid { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'nosuid' and 'suid' can be used") + } + foundSuid = true + case "nodev", "dev": + if foundDev { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'nodev' and 'dev' can be used") + } + foundDev = true case "rw", "ro": if foundWrite { - return nil, errors.Wrapf(ErrDupeMntOption, "only one of rw and ro can be used") + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'rw' and 'ro' can be used") } foundWrite = true - baseOpts = append(baseOpts, opt) case "private", "rprivate", "slave", "rslave", "shared", "rshared": if foundProp { return nil, errors.Wrapf(ErrDupeMntOption, "only one root propagation mode can be used") } foundProp = true - baseOpts = append(baseOpts, opt) case "size": + if !isTmpfs { + return nil, errors.Wrapf(ErrBadMntOption, "the 'size' option is only allowed with tmpfs mounts") + } if foundSize { return nil, errors.Wrapf(ErrDupeMntOption, "only one tmpfs size can be specified") } foundSize = true - baseOpts = append(baseOpts, opt) case "mode": + if !isTmpfs { + return nil, errors.Wrapf(ErrBadMntOption, "the 'mode' option is only allowed with tmpfs mounts") + } if foundMode { return nil, errors.Wrapf(ErrDupeMntOption, "only one tmpfs mode can be specified") } foundMode = true - baseOpts = append(baseOpts, opt) - case "noexec", "nodev", "nosuid": - // Do nothing. We always include these even if they are - // not explicitly requested. + case "tmpcopyup": + if !isTmpfs { + return nil, errors.Wrapf(ErrBadMntOption, "the 'tmpcopyup' option is only allowed with tmpfs mounts") + } + if foundCopyUp { + return nil, errors.Wrapf(ErrDupeMntOption, "the 'tmpcopyup' option can only be set once") + } + foundCopyUp = true + case "bind", "rbind": + if isTmpfs { + return nil, errors.Wrapf(ErrBadMntOption, "the 'bind' and 'rbind' options are not allowed with tmpfs mounts") + } + if foundBind { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'rbind' and 'bind' can be used") + } + foundBind = true default: - return nil, errors.Wrapf(ErrBadMntOption, "unknown tmpfs option %q", opt) + return nil, errors.Wrapf(ErrBadMntOption, "unknown mount option %q", opt) } } if !foundWrite { - baseOpts = append(baseOpts, "rw") + options = append(options, "rw") } if !foundProp { - baseOpts = append(baseOpts, "rprivate") + options = append(options, "rprivate") + } + if !foundExec { + options = append(options, "noexec") + } + if !foundSuid { + options = append(options, "nosuid") + } + if !foundDev { + options = append(options, "nodev") + } + if isTmpfs && !foundCopyUp { + options = append(options, "tmpcopyup") + } + if !isTmpfs && !foundBind { + options = append(options, "rbind") } - return baseOpts, nil + return options, nil } -- cgit v1.2.3-54-g00ecf From 5bdd97f77fc3100c6338928d4d54af32273d36fb Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 23 Aug 2019 13:24:06 -0400 Subject: Set base mount options for bind mounts from base system If I mount, say, /usr/bin into my container - I expect to be able to run the executables in that mount. Unconditionally applying noexec would be a bad idea. Before my patches to change mount options and allow exec/dev/suid being set explicitly, we inferred the mount options from where on the base system the mount originated, and the options it had there. Implement the same functionality for the new option handling. There's a lot of performance left on the table here, but I don't know that this is ever going to take enough time to make it worth optimizing. Signed-off-by: Matthew Heon --- libpod/options.go | 2 +- pkg/spec/spec.go | 21 -------------------- pkg/spec/storage.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++-- pkg/util/mountOpts.go | 17 ++++++++++++---- 4 files changed, 67 insertions(+), 28 deletions(-) (limited to 'pkg/util') diff --git a/libpod/options.go b/libpod/options.go index f4bf536b3..6df1ca5be 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1360,7 +1360,7 @@ func WithNamedVolumes(volumes []*ContainerNamedVolume) CtrCreateOption { } destinations[vol.Dest] = true - mountOpts, err := util.ProcessOptions(vol.Options, false) + mountOpts, err := util.ProcessOptions(vol.Options, false, nil) if err != nil { return errors.Wrapf(err, "error processing options for named volume %q mounted at %q", vol.Name, vol.Dest) } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 779fb5290..44bbda885 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -2,13 +2,11 @@ package createconfig import ( "os" - "path/filepath" "strings" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/rootless" - pmount "github.com/containers/storage/pkg/mount" "github.com/docker/docker/oci/caps" "github.com/docker/go-units" "github.com/opencontainers/runc/libcontainer/user" @@ -457,25 +455,6 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM return configSpec, nil } -func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) { - var err error - target, err = filepath.Abs(target) - if err != nil { - return nil, errors.Wrapf(err, "cannot resolve %s", target) - } - var bestSoFar *pmount.Info - for _, i := range mounts { - if bestSoFar != nil && len(bestSoFar.Mountpoint) > len(i.Mountpoint) { - // Won't be better than what we have already found - continue - } - if strings.HasPrefix(target, i.Mountpoint) { - bestSoFar = i - } - } - return bestSoFar, nil -} - func blockAccessToKernelFilesystems(config *CreateConfig, g *generate.Generator) { if !config.Privileged { for _, mp := range []string{ diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index 9f4248924..7ed21a055 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -10,6 +10,7 @@ import ( "github.com/containers/buildah/pkg/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/util" + pmount "github.com/containers/storage/pkg/mount" "github.com/containers/storage/pkg/stringid" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -816,17 +817,46 @@ func supercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.M // Ensure mount options on all mounts are correct func initFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { + // We need to look up mounts so we can figure out the proper mount flags + // to apply. + systemMounts, err := pmount.GetMounts() + if err != nil { + return nil, errors.Wrapf(err, "error retrieving system mounts to look up mount options") + } + + // TODO: We probably don't need to re-build the mounts array var mounts []spec.Mount for _, m := range inputMounts { if m.Type == TypeBind { - opts, err := util.ProcessOptions(m.Options, false) + baseMnt, err := findMount(m.Destination, systemMounts) + if err != nil { + return nil, errors.Wrapf(err, "error looking up mountpoint for mount %s", m.Destination) + } + var noexec, nosuid, nodev bool + for _, baseOpt := range strings.Split(baseMnt.Opts, ",") { + switch baseOpt { + case "noexec": + noexec = true + case "nosuid": + nosuid = true + case "nodev": + nodev = true + } + } + + defaultMountOpts := new(util.DefaultMountOptions) + defaultMountOpts.Noexec = noexec + defaultMountOpts.Nosuid = nosuid + defaultMountOpts.Nodev = nodev + + opts, err := util.ProcessOptions(m.Options, false, defaultMountOpts) if err != nil { return nil, err } m.Options = opts } if m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev" { - opts, err := util.ProcessOptions(m.Options, true) + opts, err := util.ProcessOptions(m.Options, true, nil) if err != nil { return nil, err } @@ -837,3 +867,24 @@ func initFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { } return mounts, nil } + +// TODO: We could make this a bit faster by building a tree of the mountpoints +// and traversing it to identify the correct mount. +func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) { + var err error + target, err = filepath.Abs(target) + if err != nil { + return nil, errors.Wrapf(err, "cannot resolve %s", target) + } + var bestSoFar *pmount.Info + for _, i := range mounts { + if bestSoFar != nil && len(bestSoFar.Mountpoint) > len(i.Mountpoint) { + // Won't be better than what we have already found + continue + } + if strings.HasPrefix(target, i.Mountpoint) { + bestSoFar = i + } + } + return bestSoFar, nil +} diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index 81dc4cefd..8accd4697 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -13,10 +13,19 @@ var ( ErrDupeMntOption = errors.Errorf("duplicate option passed") ) +// DefaultMountOptions sets default mount options for ProcessOptions. +type DefaultMountOptions struct { + Noexec bool + Nosuid bool + Nodev bool +} + // ProcessOptions parses the options for a bind or tmpfs mount and ensures that // they are sensible and follow convention. The isTmpfs variable controls // whether extra, tmpfs-specific options will be allowed. -func ProcessOptions(options []string, isTmpfs bool) ([]string, error) { +// The defaults variable controls default mount options that will be set. If it +// is not included, they will be set unconditionally. +func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOptions) ([]string, error) { var ( foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind bool ) @@ -93,13 +102,13 @@ func ProcessOptions(options []string, isTmpfs bool) ([]string, error) { if !foundProp { options = append(options, "rprivate") } - if !foundExec { + if !foundExec && (defaults == nil || defaults.Noexec) { options = append(options, "noexec") } - if !foundSuid { + if !foundSuid && (defaults == nil || defaults.Nosuid) { options = append(options, "nosuid") } - if !foundDev { + if !foundDev && (defaults == nil || defaults.Nodev) { options = append(options, "nodev") } if isTmpfs && !foundCopyUp { -- cgit v1.2.3-54-g00ecf From 820e242e821efda218031b75fce01625a20baa54 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 23 Aug 2019 15:10:57 -0400 Subject: Allow :z and :Z with ProcessOptions Signed-off-by: Matthew Heon --- pkg/util/mountOpts.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'pkg/util') diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index 8accd4697..9e387ce95 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -27,7 +27,7 @@ type DefaultMountOptions struct { // is not included, they will be set unconditionally. func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOptions) ([]string, error) { var ( - foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind bool + foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ bool ) for _, opt := range options { @@ -91,6 +91,13 @@ func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOption return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'rbind' and 'bind' can be used") } foundBind = true + case "z", "Z": + if isTmpfs { + return nil, errors.Wrapf(ErrBadMntOption, "the 'z' and 'Z' options are not allowed with tmpfs mounts") + } + if foundZ { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'z' and 'Z' can be used") + } default: return nil, errors.Wrapf(ErrBadMntOption, "unknown mount option %q", opt) } -- cgit v1.2.3-54-g00ecf From 96812dc490dbd00b0ec6280353a4e78ba79b44b8 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 23 Aug 2019 19:05:16 -0400 Subject: Fix addition of mount options when using RO tmpfs For read-only containers set to create tmpfs filesystems over /run and other common destinations, we were incorrectly setting mount options, resulting in duplicate mount options. Signed-off-by: Matthew Heon --- pkg/spec/storage.go | 6 ++++-- pkg/util/mountOpts.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'pkg/util') diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index 7ed21a055..bc0eaad6d 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -163,14 +163,16 @@ func (config *CreateConfig) parseVolumes(runtime *libpod.Runtime) ([]spec.Mount, // If requested, add tmpfs filesystems for read-only containers. if config.ReadOnlyRootfs && config.ReadOnlyTmpfs { readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"} - options := []string{"rw", "rprivate", "exec", "nosuid", "nodev", "tmpcopyup"} + options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} for _, dest := range readonlyTmpfs { if _, ok := baseMounts[dest]; ok { continue } localOpts := options if dest == "/run" { - localOpts = append(localOpts, "dev", "suid", "noexec", "size=65536k") + localOpts = append(localOpts, "noexec", "size=65536k") + } else { + localOpts = append(localOpts, "exec") } baseMounts[dest] = spec.Mount{ Destination: dest, diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index 9e387ce95..670daeaf9 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -10,7 +10,7 @@ var ( // ErrBadMntOption indicates that an invalid mount option was passed. ErrBadMntOption = errors.Errorf("invalid mount option") // ErrDupeMntOption indicates that a duplicate mount option was passed. - ErrDupeMntOption = errors.Errorf("duplicate option passed") + ErrDupeMntOption = errors.Errorf("duplicate mount option passed") ) // DefaultMountOptions sets default mount options for ProcessOptions. -- cgit v1.2.3-54-g00ecf