gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/mount_control.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package builtin
    21  
    22  import (
    23  	"bytes"
    24  	"errors"
    25  	"fmt"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"gitee.com/mysnapcore/mysnapd/interfaces"
    30  	"gitee.com/mysnapcore/mysnapd/interfaces/apparmor"
    31  	"gitee.com/mysnapcore/mysnapd/interfaces/utils"
    32  	"gitee.com/mysnapcore/mysnapd/snap"
    33  	"gitee.com/mysnapcore/mysnapd/strutil"
    34  	"gitee.com/mysnapcore/mysnapd/systemd"
    35  )
    36  
    37  const mountControlSummary = `allows creating transient and persistent mounts`
    38  
    39  const mountControlBaseDeclarationPlugs = `
    40    mount-control:
    41      allow-installation: false
    42      deny-auto-connection: true
    43  `
    44  
    45  const mountControlBaseDeclarationSlots = `
    46    mount-control:
    47      allow-installation:
    48        slot-snap-type:
    49          - core
    50      deny-connection: true
    51  `
    52  
    53  var mountAttrTypeError = errors.New(`mount-control "mount" attribute must be a list of dictionaries`)
    54  
    55  const mountControlConnectedPlugSecComp = `
    56  # Description: Allow mount and umount syscall access. No filtering here, as we
    57  # rely on AppArmor to filter the mount operations.
    58  mount
    59  umount
    60  umount2
    61  `
    62  
    63  // The reason why this list is not shared with osutil.MountOptsToCommonFlags or
    64  // other parts of the codebase is that this one only contains the options which
    65  // have been deemed safe and have been vetted by the security team.
    66  var allowedMountOptions = []string{
    67  	"async",
    68  	"atime",
    69  	"bind",
    70  	"diratime",
    71  	"dirsync",
    72  	"iversion",
    73  	"lazytime",
    74  	"nofail",
    75  	"noiversion",
    76  	"nomand",
    77  	"noatime",
    78  	"nodev",
    79  	"nodiratime",
    80  	"noexec",
    81  	"nolazytime",
    82  	"norelatime",
    83  	"nosuid",
    84  	"nostrictatime",
    85  	"nouser",
    86  	"relatime",
    87  	"strictatime",
    88  	"sync",
    89  	"ro",
    90  	"rw",
    91  }
    92  
    93  // A few mount flags are special in that if they are specified, the filesystem
    94  // type is ignored. We list them here, and we will ensure that the plug
    95  // declaration does not specify a type, if any of them is present among the
    96  // options.
    97  var optionsWithoutFsType = []string{
    98  	"bind",
    99  	// Note: the following flags should also fall into this list, but we are
   100  	// not currently allowing them (and don't plan to):
   101  	// - "make-private"
   102  	// - "make-shared"
   103  	// - "make-slave"
   104  	// - "make-unbindable"
   105  	// - "move"
   106  	// - "remount"
   107  }
   108  
   109  // List of filesystem types to allow if the plug declaration does not
   110  // explicitly specify a filesystem type.
   111  var defaultFSTypes = []string{
   112  	"aufs",
   113  	"autofs",
   114  	"btrfs",
   115  	"ext2",
   116  	"ext3",
   117  	"ext4",
   118  	"hfs",
   119  	"iso9660",
   120  	"jfs",
   121  	"msdos",
   122  	"ntfs",
   123  	"ramfs",
   124  	"reiserfs",
   125  	"squashfs",
   126  	"tmpfs",
   127  	"ubifs",
   128  	"udf",
   129  	"ufs",
   130  	"vfat",
   131  	"zfs",
   132  	"xfs",
   133  }
   134  
   135  // The filesystems in the following list were considered either dangerous or
   136  // not relevant for this interface:
   137  var disallowedFSTypes = []string{
   138  	"bpf",
   139  	"cgroup",
   140  	"cgroup2",
   141  	"debugfs",
   142  	"devpts",
   143  	"ecryptfs",
   144  	"hugetlbfs",
   145  	"overlayfs",
   146  	"proc",
   147  	"securityfs",
   148  	"sysfs",
   149  	"tracefs",
   150  }
   151  
   152  // mountControlInterface allows creating transient and persistent mounts
   153  type mountControlInterface struct {
   154  	commonInterface
   155  }
   156  
   157  // The "what" and "where" attributes end up in the AppArmor profile, surrounded
   158  // by double quotes; to ensure that a malicious snap cannot inject arbitrary
   159  // rules by specifying something like
   160  //   where: $SNAP_DATA/foo", /** rw, #
   161  // which would generate a profile line like:
   162  //   mount options=() "$SNAP_DATA/foo", /** rw, #"
   163  // (which would grant read-write access to the whole filesystem), it's enough
   164  // to exclude the `"` character: without it, whatever is written in the
   165  // attribute will not be able to escape being treated like a pattern.
   166  //
   167  // To be safe, there's more to be done: the pattern also needs to be valid, as
   168  // a malformed one (for example, a pattern having an unmatched `}`) would cause
   169  // apparmor_parser to fail loading the profile. For this situation, we use the
   170  // PathPattern interface to validate the pattern.
   171  //
   172  // Besides that, we are also excluding the `@` character, which is used to mark
   173  // AppArmor variables (tunables): when generating the profile we lack the
   174  // knowledge of which variables have been defined, so it's safer to exclude
   175  // them.
   176  // The what attribute regular expression here is intentionally permissive of
   177  // nearly any path, and due to the super-privileged nature of this interface it
   178  // is expected that sensible values of what are enforced by the store manual
   179  // review queue and security teams.
   180  var (
   181  	whatRegexp  = regexp.MustCompile(`^(none|/[^"@]*)$`)
   182  	whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`)
   183  )
   184  
   185  // Excluding spaces and other characters which might allow constructing a
   186  // malicious string like
   187  //   auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=(
   188  var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`)
   189  
   190  type MountInfo struct {
   191  	what       string
   192  	where      string
   193  	persistent bool
   194  	types      []string
   195  	options    []string
   196  }
   197  
   198  func parseStringList(mountEntry map[string]interface{}, fieldName string) ([]string, error) {
   199  	var list []string
   200  	value, ok := mountEntry[fieldName]
   201  	if ok {
   202  		interfaceList, ok := value.([]interface{})
   203  		if !ok {
   204  			return nil, fmt.Errorf(`mount-control "%s" must be an array of strings (got %q)`, fieldName, value)
   205  		}
   206  		for i, iface := range interfaceList {
   207  			valueString, ok := iface.(string)
   208  			if !ok {
   209  				return nil, fmt.Errorf(`mount-control "%s" element %d not a string (%q)`, fieldName, i+1, iface)
   210  			}
   211  			list = append(list, valueString)
   212  		}
   213  	}
   214  	return list, nil
   215  }
   216  
   217  func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error) error {
   218  	var mounts []map[string]interface{}
   219  	err := plug.Attr("mount", &mounts)
   220  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   221  		return mountAttrTypeError
   222  	}
   223  
   224  	for _, mount := range mounts {
   225  		what, ok := mount["what"].(string)
   226  		if !ok {
   227  			return fmt.Errorf(`mount-control "what" must be a string`)
   228  		}
   229  
   230  		where, ok := mount["where"].(string)
   231  		if !ok {
   232  			return fmt.Errorf(`mount-control "where" must be a string`)
   233  		}
   234  
   235  		persistent := false
   236  		persistentValue, ok := mount["persistent"]
   237  		if ok {
   238  			if persistent, ok = persistentValue.(bool); !ok {
   239  				return fmt.Errorf(`mount-control "persistent" must be a boolean`)
   240  			}
   241  		}
   242  
   243  		types, err := parseStringList(mount, "type")
   244  		if err != nil {
   245  			return err
   246  		}
   247  
   248  		options, err := parseStringList(mount, "options")
   249  		if err != nil {
   250  			return err
   251  		}
   252  
   253  		mountInfo := &MountInfo{
   254  			what:       what,
   255  			where:      where,
   256  			persistent: persistent,
   257  			types:      types,
   258  			options:    options,
   259  		}
   260  
   261  		if err := fn(mountInfo); err != nil {
   262  			return err
   263  		}
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func validateWhatAttr(what string) error {
   270  	if !whatRegexp.MatchString(what) {
   271  		return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`)
   272  	}
   273  
   274  	if !cleanSubPath(what) {
   275  		return fmt.Errorf(`mount-control "what" pattern is not clean: %q`, what)
   276  	}
   277  
   278  	if _, err := utils.NewPathPattern(what); err != nil {
   279  		return fmt.Errorf(`mount-control "what" setting cannot be used: %v`, err)
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func validateWhereAttr(where string) error {
   286  	if !whereRegexp.MatchString(where) {
   287  		return fmt.Errorf(`mount-control "where" attribute must start with $SNAP_COMMON, $SNAP_DATA or / and not contain special characters`)
   288  	}
   289  
   290  	if !cleanSubPath(where) {
   291  		return fmt.Errorf(`mount-control "where" pattern is not clean: %q`, where)
   292  	}
   293  
   294  	if _, err := utils.NewPathPattern(where); err != nil {
   295  		return fmt.Errorf(`mount-control "where" setting cannot be used: %v`, err)
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  func validateMountTypes(types []string) error {
   302  	includesTmpfs := false
   303  	for _, t := range types {
   304  		if !typeRegexp.MatchString(t) {
   305  			return fmt.Errorf(`mount-control filesystem type invalid: %q`, t)
   306  		}
   307  		if strutil.ListContains(disallowedFSTypes, t) {
   308  			return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t)
   309  		}
   310  		if t == "tmpfs" {
   311  			includesTmpfs = true
   312  		}
   313  	}
   314  
   315  	if includesTmpfs && len(types) > 1 {
   316  		return errors.New(`mount-control filesystem type "tmpfs" cannot be listed with other types`)
   317  	}
   318  	return nil
   319  }
   320  
   321  func validateMountOptions(options []string) error {
   322  	if len(options) == 0 {
   323  		return errors.New(`mount-control "options" cannot be empty`)
   324  	}
   325  	for _, o := range options {
   326  		if !strutil.ListContains(allowedMountOptions, o) {
   327  			return fmt.Errorf(`mount-control option unrecognized or forbidden: %q`, o)
   328  		}
   329  	}
   330  	return nil
   331  }
   332  
   333  // Find the first option which is incompatible with a FS type declaration
   334  func optionIncompatibleWithFsType(options []string) string {
   335  	for _, o := range options {
   336  		if strutil.ListContains(optionsWithoutFsType, o) {
   337  			return o
   338  		}
   339  	}
   340  	return ""
   341  }
   342  
   343  func validateMountInfo(mountInfo *MountInfo) error {
   344  	if err := validateWhatAttr(mountInfo.what); err != nil {
   345  		return err
   346  	}
   347  
   348  	if err := validateWhereAttr(mountInfo.where); err != nil {
   349  		return err
   350  	}
   351  
   352  	if err := validateMountTypes(mountInfo.types); err != nil {
   353  		return err
   354  	}
   355  
   356  	if err := validateMountOptions(mountInfo.options); err != nil {
   357  		return err
   358  	}
   359  
   360  	// Check if any options are incompatible with specifying a FS type
   361  	fsExclusiveOption := optionIncompatibleWithFsType(mountInfo.options)
   362  	if fsExclusiveOption != "" && len(mountInfo.types) > 0 {
   363  		return fmt.Errorf(`mount-control option %q is incompatible with specifying filesystem type`, fsExclusiveOption)
   364  	}
   365  
   366  	// "what" must be set to "none" iff the type is "tmpfs"
   367  	isTmpfs := len(mountInfo.types) == 1 && mountInfo.types[0] == "tmpfs"
   368  	if mountInfo.what == "none" {
   369  		if !isTmpfs {
   370  			return errors.New(`mount-control "what" attribute can be "none" only with "tmpfs"`)
   371  		}
   372  	} else if isTmpfs {
   373  		return fmt.Errorf(`mount-control "what" attribute must be "none" with "tmpfs"; found %q instead`, mountInfo.what)
   374  	}
   375  
   376  	// Until we have a clear picture of how this should work, disallow creating
   377  	// persistent mounts into $SNAP_DATA
   378  	if mountInfo.persistent && strings.HasPrefix(mountInfo.where, "$SNAP_DATA") {
   379  		return errors.New(`mount-control "persistent" attribute cannot be used to mount onto $SNAP_DATA`)
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  func (iface *mountControlInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error {
   386  	// The systemd.ListMountUnits() method works by issuing the command
   387  	// "systemctl show *.mount", but globbing was only added in systemd v209.
   388  	if err := systemd.EnsureAtLeast(209); err != nil {
   389  		return err
   390  	}
   391  
   392  	hasMountEntries := false
   393  	err := enumerateMounts(plug, func(mountInfo *MountInfo) error {
   394  		hasMountEntries = true
   395  		return validateMountInfo(mountInfo)
   396  	})
   397  	if err != nil {
   398  		return err
   399  	}
   400  
   401  	if !hasMountEntries {
   402  		return mountAttrTypeError
   403  	}
   404  
   405  	return nil
   406  }
   407  
   408  func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   409  	mountControlSnippet := bytes.NewBuffer(nil)
   410  	emit := func(f string, args ...interface{}) {
   411  		fmt.Fprintf(mountControlSnippet, f, args...)
   412  	}
   413  	snapInfo := plug.Snap()
   414  
   415  	emit(`
   416    # Rules added by the mount-control interface
   417    capability sys_admin,  # for mount
   418  
   419    owner @{PROC}/@{pid}/mounts r,
   420    owner @{PROC}/@{pid}/mountinfo r,
   421    owner @{PROC}/self/mountinfo r,
   422  
   423    /{,usr/}bin/mount ixr,
   424    /{,usr/}bin/umount ixr,
   425    # mount/umount (via libmount) track some mount info in these files
   426    /run/mount/utab* wrlk,
   427  `)
   428  
   429  	// No validation is occurring here, as it was already performed in
   430  	// BeforeConnectPlug()
   431  	enumerateMounts(plug, func(mountInfo *MountInfo) error {
   432  
   433  		source := mountInfo.what
   434  		target := mountInfo.where
   435  		if target[0] == '$' {
   436  			matches := whereRegexp.FindStringSubmatchIndex(target)
   437  			if matches == nil || len(matches) < 4 {
   438  				// This cannot really happen, as the string wouldn't pass the validation
   439  				return fmt.Errorf(`internal error: "where" fails to match regexp: %q`, mountInfo.where)
   440  			}
   441  			// the first two elements in "matches" are the boundaries of the whole
   442  			// string; the next two are the boundaries of the first match, which is
   443  			// what we care about as it contains the environment variable we want
   444  			// to expand:
   445  			variableStart, variableEnd := matches[2], matches[3]
   446  			variable := target[variableStart:variableEnd]
   447  			expanded := snapInfo.ExpandSnapVariables(variable)
   448  			target = expanded + target[variableEnd:]
   449  		}
   450  
   451  		var typeRule string
   452  		if optionIncompatibleWithFsType(mountInfo.options) != "" {
   453  			// In this rule the FS type will not match unless it's empty
   454  			typeRule = ""
   455  		} else {
   456  			var types []string
   457  			if len(mountInfo.types) > 0 {
   458  				types = mountInfo.types
   459  			} else {
   460  				types = defaultFSTypes
   461  			}
   462  			typeRule = "fstype=(" + strings.Join(types, ",") + ")"
   463  		}
   464  
   465  		options := strings.Join(mountInfo.options, ",")
   466  
   467  		emit("  mount %s options=(%s) \"%s\" -> \"%s{,/}\",\n", typeRule, options, source, target)
   468  		emit("  umount \"%s{,/}\",\n", target)
   469  		return nil
   470  	})
   471  
   472  	spec.AddSnippet(mountControlSnippet.String())
   473  	return nil
   474  }
   475  
   476  func (iface *mountControlInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
   477  	return true
   478  }
   479  
   480  func init() {
   481  	registerIface(&mountControlInterface{
   482  		commonInterface: commonInterface{
   483  			name:                 "mount-control",
   484  			summary:              mountControlSummary,
   485  			baseDeclarationPlugs: mountControlBaseDeclarationPlugs,
   486  			baseDeclarationSlots: mountControlBaseDeclarationSlots,
   487  			implicitOnCore:       true,
   488  			implicitOnClassic:    true,
   489  			connectedPlugSecComp: mountControlConnectedPlugSecComp,
   490  		},
   491  	})
   492  }