github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/snap/validate.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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 snap
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  	"unicode/utf8"
    32  
    33  	"github.com/snapcore/snapd/osutil"
    34  	"github.com/snapcore/snapd/snap/naming"
    35  	"github.com/snapcore/snapd/spdx"
    36  	"github.com/snapcore/snapd/strutil"
    37  	"github.com/snapcore/snapd/timeout"
    38  	"github.com/snapcore/snapd/timeutil"
    39  )
    40  
    41  // ValidateInstanceName checks if a string can be used as a snap instance name.
    42  func ValidateInstanceName(instanceName string) error {
    43  	return naming.ValidateInstance(instanceName)
    44  }
    45  
    46  // ValidateName checks if a string can be used as a snap name.
    47  func ValidateName(name string) error {
    48  	return naming.ValidateSnap(name)
    49  }
    50  
    51  // ValidateDesktopPrefix checks if a string can be used as a desktop file
    52  // prefix. A desktop prefix should be of the form 'snapname' or
    53  // 'snapname+instance'.
    54  func ValidateDesktopPrefix(prefix string) bool {
    55  	tokens := strings.Split(prefix, "+")
    56  	if len(tokens) == 0 || len(tokens) > 2 {
    57  		return false
    58  	}
    59  	if err := ValidateName(tokens[0]); err != nil {
    60  		return false
    61  	}
    62  	if len(tokens) == 2 {
    63  		if err := ValidateInstanceName(tokens[1]); err != nil {
    64  			return false
    65  		}
    66  	}
    67  	return true
    68  }
    69  
    70  // ValidatePlugName checks if a string can be used as a slot name.
    71  //
    72  // Slot names and plug names within one snap must have unique names.
    73  // This is not enforced by this function but is enforced by snap-level
    74  // validation.
    75  func ValidatePlugName(name string) error {
    76  	return naming.ValidatePlug(name)
    77  }
    78  
    79  // ValidateSlotName checks if a string can be used as a slot name.
    80  //
    81  // Slot names and plug names within one snap must have unique names.
    82  // This is not enforced by this function but is enforced by snap-level
    83  // validation.
    84  func ValidateSlotName(name string) error {
    85  	return naming.ValidateSlot(name)
    86  }
    87  
    88  // ValidateInterfaceName checks if a string can be used as an interface name.
    89  func ValidateInterfaceName(name string) error {
    90  	return naming.ValidateInterface(name)
    91  }
    92  
    93  // NB keep this in sync with snapcraft and the review tools :-)
    94  var isValidVersion = regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]{0,30}[a-zA-Z0-9+~])?$").MatchString
    95  
    96  var isNonGraphicalASCII = regexp.MustCompile("[^[:graph:]]").MatchString
    97  var isInvalidFirstVersionChar = regexp.MustCompile("^[^a-zA-Z0-9]").MatchString
    98  var isInvalidLastVersionChar = regexp.MustCompile("[^a-zA-Z0-9+~]$").MatchString
    99  var invalidMiddleVersionChars = regexp.MustCompile("[^a-zA-Z0-9:.+~-]+").FindAllString
   100  
   101  // ValidateVersion checks if a string is a valid snap version.
   102  func ValidateVersion(version string) error {
   103  	if !isValidVersion(version) {
   104  		// maybe it was too short?
   105  		if len(version) == 0 {
   106  			return errors.New("invalid snap version: cannot be empty")
   107  		}
   108  		if isNonGraphicalASCII(version) {
   109  			// note that while this way of quoting the version can produce ugly
   110  			// output in some cases (e.g. if you're trying to set a version to
   111  			// "hello😁", seeing “invalid version "hello😁"” could be clearer than
   112  			// “invalid snap version "hello\U0001f601"”), in a lot of more
   113  			// interesting cases you _need_ to have the thing that's not ASCII
   114  			// pointed out: homoglyphs and near-homoglyphs are too hard to spot
   115  			// otherwise. Take for example a version of "аерс". Or "v1.0‑x".
   116  			return fmt.Errorf("invalid snap version %s: must be printable, non-whitespace ASCII",
   117  				strconv.QuoteToASCII(version))
   118  		}
   119  		// now we know it's a non-empty ASCII string, we can get serious
   120  		var reasons []string
   121  		// ... too long?
   122  		if len(version) > 32 {
   123  			reasons = append(reasons, fmt.Sprintf("cannot be longer than 32 characters (got: %d)", len(version)))
   124  		}
   125  		// started with a symbol?
   126  		if isInvalidFirstVersionChar(version) {
   127  			// note that we can only say version[0] because we know it's ASCII :-)
   128  			reasons = append(reasons, fmt.Sprintf("must start with an ASCII alphanumeric (and not %q)", version[0]))
   129  		}
   130  		if len(version) > 1 {
   131  			if isInvalidLastVersionChar(version) {
   132  				tpl := "must end with an ASCII alphanumeric or one of '+' or '~' (and not %q)"
   133  				reasons = append(reasons, fmt.Sprintf(tpl, version[len(version)-1]))
   134  			}
   135  			if len(version) > 2 {
   136  				if all := invalidMiddleVersionChars(version[1:len(version)-1], -1); len(all) > 0 {
   137  					reasons = append(reasons, fmt.Sprintf("contains invalid characters: %s", strutil.Quoted(all)))
   138  				}
   139  			}
   140  		}
   141  		switch len(reasons) {
   142  		case 0:
   143  			// huh
   144  			return fmt.Errorf("invalid snap version %q", version)
   145  		case 1:
   146  			return fmt.Errorf("invalid snap version %q: %s", version, reasons[0])
   147  		default:
   148  			reasons, last := reasons[:len(reasons)-1], reasons[len(reasons)-1]
   149  			return fmt.Errorf("invalid snap version %q: %s, and %s", version, strings.Join(reasons, ", "), last)
   150  		}
   151  	}
   152  	return nil
   153  }
   154  
   155  // ValidateLicense checks if a string is a valid SPDX expression.
   156  func ValidateLicense(license string) error {
   157  	if err := spdx.ValidateLicense(license); err != nil {
   158  		return fmt.Errorf("cannot validate license %q: %s", license, err)
   159  	}
   160  	return nil
   161  }
   162  
   163  // ValidateHook validates the content of the given HookInfo
   164  func ValidateHook(hook *HookInfo) error {
   165  	if err := naming.ValidateHook(hook.Name); err != nil {
   166  		return err
   167  	}
   168  
   169  	// Also validate the command chain
   170  	for _, value := range hook.CommandChain {
   171  		if !commandChainContentWhitelist.MatchString(value) {
   172  			return fmt.Errorf("hook command-chain contains illegal %q (legal: '%s')", value, commandChainContentWhitelist)
   173  		}
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  // ValidateAlias checks if a string can be used as an alias name.
   180  func ValidateAlias(alias string) error {
   181  	return naming.ValidateAlias(alias)
   182  }
   183  
   184  // validateSocketName checks if a string ca be used as a name for a socket (for
   185  // socket activation).
   186  func validateSocketName(name string) error {
   187  	return naming.ValidateSocket(name)
   188  }
   189  
   190  // validateSocketmode checks that the socket mode is a valid file mode.
   191  func validateSocketMode(mode os.FileMode) error {
   192  	if mode > 0777 {
   193  		return fmt.Errorf("cannot use mode: %04o", mode)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // validateSocketAddr checks that the value of socket addresses.
   200  func validateSocketAddr(socket *SocketInfo, fieldName string, address string) error {
   201  	if address == "" {
   202  		return fmt.Errorf("%q is not defined", fieldName)
   203  	}
   204  
   205  	switch address[0] {
   206  	case '/', '$':
   207  		return validateSocketAddrPath(socket, fieldName, address)
   208  	case '@':
   209  		return validateSocketAddrAbstract(socket, fieldName, address)
   210  	default:
   211  		return validateSocketAddrNet(socket, fieldName, address)
   212  	}
   213  }
   214  
   215  func validateSocketAddrPath(socket *SocketInfo, fieldName string, path string) error {
   216  	if clean := filepath.Clean(path); clean != path {
   217  		return fmt.Errorf("invalid %q: %q should be written as %q", fieldName, path, clean)
   218  	}
   219  
   220  	switch socket.App.DaemonScope {
   221  	case SystemDaemon:
   222  		if !(strings.HasPrefix(path, "$SNAP_DATA/") || strings.HasPrefix(path, "$SNAP_COMMON/") || strings.HasPrefix(path, "$XDG_RUNTIME_DIR/")) {
   223  			return fmt.Errorf(
   224  				"invalid %q: system daemon sockets must have a prefix of $SNAP_DATA, $SNAP_COMMON or $XDG_RUNTIME_DIR", fieldName)
   225  		}
   226  	case UserDaemon:
   227  		if !(strings.HasPrefix(path, "$SNAP_USER_DATA/") || strings.HasPrefix(path, "$SNAP_USER_COMMON/") || strings.HasPrefix(path, "$XDG_RUNTIME_DIR/")) {
   228  			return fmt.Errorf(
   229  				"invalid %q: user daemon sockets must have a prefix of $SNAP_USER_DATA, $SNAP_USER_COMMON, or $XDG_RUNTIME_DIR", fieldName)
   230  		}
   231  	default:
   232  		return fmt.Errorf("invalid %q: cannot validate sockets for daemon-scope %q", fieldName, socket.App.DaemonScope)
   233  	}
   234  
   235  	return nil
   236  }
   237  
   238  func validateSocketAddrAbstract(socket *SocketInfo, fieldName string, path string) error {
   239  	// this comes from snap declaration, so the prefix can only be the snap
   240  	// name at this point
   241  	prefix := fmt.Sprintf("@snap.%s.", socket.App.Snap.SnapName())
   242  	if !strings.HasPrefix(path, prefix) {
   243  		return fmt.Errorf("path for %q must be prefixed with %q", fieldName, prefix)
   244  	}
   245  	return nil
   246  }
   247  
   248  func validateSocketAddrNet(socket *SocketInfo, fieldName string, address string) error {
   249  	lastIndex := strings.LastIndex(address, ":")
   250  	if lastIndex >= 0 {
   251  		if err := validateSocketAddrNetHost(socket, fieldName, address[:lastIndex]); err != nil {
   252  			return err
   253  		}
   254  		return validateSocketAddrNetPort(socket, fieldName, address[lastIndex+1:])
   255  	}
   256  
   257  	// Address only contains a port
   258  	return validateSocketAddrNetPort(socket, fieldName, address)
   259  }
   260  
   261  func validateSocketAddrNetHost(socket *SocketInfo, fieldName string, address string) error {
   262  	validAddresses := []string{"127.0.0.1", "[::1]", "[::]"}
   263  	for _, valid := range validAddresses {
   264  		if address == valid {
   265  			return nil
   266  		}
   267  	}
   268  
   269  	return fmt.Errorf("invalid %q address %q, must be one of: %s", fieldName, address, strings.Join(validAddresses, ", "))
   270  }
   271  
   272  func validateSocketAddrNetPort(socket *SocketInfo, fieldName string, port string) error {
   273  	var val uint64
   274  	var err error
   275  	retErr := fmt.Errorf("invalid %q port number %q", fieldName, port)
   276  	if val, err = strconv.ParseUint(port, 10, 16); err != nil {
   277  		return retErr
   278  	}
   279  	if val < 1 || val > 65535 {
   280  		return retErr
   281  	}
   282  	return nil
   283  }
   284  
   285  func validateDescription(descr string) error {
   286  	if count := utf8.RuneCountInString(descr); count > 4096 {
   287  		return fmt.Errorf("description can have up to 4096 codepoints, got %d", count)
   288  	}
   289  	return nil
   290  }
   291  
   292  func validateTitle(title string) error {
   293  	if count := utf8.RuneCountInString(title); count > 40 {
   294  		return fmt.Errorf("title can have up to 40 codepoints, got %d", count)
   295  	}
   296  	return nil
   297  }
   298  
   299  // Validate verifies the content in the info.
   300  func Validate(info *Info) error {
   301  	name := info.InstanceName()
   302  	if name == "" {
   303  		return errors.New("snap name cannot be empty")
   304  	}
   305  
   306  	if err := ValidateName(info.SnapName()); err != nil {
   307  		return err
   308  	}
   309  	if err := ValidateInstanceName(name); err != nil {
   310  		return err
   311  	}
   312  
   313  	if err := validateTitle(info.Title()); err != nil {
   314  		return err
   315  	}
   316  
   317  	if err := validateDescription(info.Description()); err != nil {
   318  		return err
   319  	}
   320  
   321  	if err := ValidateVersion(info.Version); err != nil {
   322  		return err
   323  	}
   324  
   325  	if err := info.Epoch.Validate(); err != nil {
   326  		return err
   327  	}
   328  
   329  	if license := info.License; license != "" {
   330  		if err := ValidateLicense(license); err != nil {
   331  			return err
   332  		}
   333  	}
   334  
   335  	// validate app entries
   336  	for _, app := range info.Apps {
   337  		if err := ValidateApp(app); err != nil {
   338  			return fmt.Errorf("invalid definition of application %q: %v", app.Name, err)
   339  		}
   340  	}
   341  
   342  	// validate apps ordering according to after/before
   343  	if err := validateAppOrderCycles(info.Services()); err != nil {
   344  		return err
   345  	}
   346  
   347  	// validate aliases
   348  	for alias, app := range info.LegacyAliases {
   349  		if err := naming.ValidateAlias(alias); err != nil {
   350  			return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name)
   351  		}
   352  	}
   353  
   354  	// validate hook entries
   355  	for _, hook := range info.Hooks {
   356  		if err := ValidateHook(hook); err != nil {
   357  			return err
   358  		}
   359  	}
   360  
   361  	// Ensure that plugs and slots have appropriate names and interface names.
   362  	if err := plugsSlotsInterfacesNames(info); err != nil {
   363  		return err
   364  	}
   365  
   366  	// Ensure that plug and slot have unique names.
   367  	if err := plugsSlotsUniqueNames(info); err != nil {
   368  		return err
   369  	}
   370  
   371  	// Ensure that base field is valid
   372  	if err := ValidateBase(info); err != nil {
   373  		return err
   374  	}
   375  
   376  	// Ensure system usernames are valid
   377  	if err := ValidateSystemUsernames(info); err != nil {
   378  		return err
   379  	}
   380  
   381  	// ensure that common-id(s) are unique
   382  	if err := ValidateCommonIDs(info); err != nil {
   383  		return err
   384  	}
   385  
   386  	return ValidateLayoutAll(info)
   387  }
   388  
   389  // ValidateBase validates the base field.
   390  func ValidateBase(info *Info) error {
   391  	// validate that bases do not have base fields
   392  	if info.Type() == TypeOS || info.Type() == TypeBase {
   393  		if info.Base != "" && info.Base != "none" {
   394  			return fmt.Errorf(`cannot have "base" field on %q snap %q`, info.Type(), info.InstanceName())
   395  		}
   396  	}
   397  
   398  	if info.Base == "none" && (len(info.Hooks) > 0 || len(info.Apps) > 0) {
   399  		return fmt.Errorf(`cannot have apps or hooks with base "none"`)
   400  	}
   401  
   402  	if info.Base != "" {
   403  		baseSnapName, instanceKey := SplitInstanceName(info.Base)
   404  		if instanceKey != "" {
   405  			return fmt.Errorf("base cannot specify a snap instance name: %q", info.Base)
   406  		}
   407  		if err := ValidateName(baseSnapName); err != nil {
   408  			return fmt.Errorf("invalid base name: %s", err)
   409  		}
   410  	}
   411  	return nil
   412  }
   413  
   414  // ValidateLayoutAll validates the consistency of all the layout elements in a snap.
   415  func ValidateLayoutAll(info *Info) error {
   416  	paths := make([]string, 0, len(info.Layout))
   417  	for _, layout := range info.Layout {
   418  		paths = append(paths, layout.Path)
   419  	}
   420  	sort.Strings(paths)
   421  
   422  	// Validate that each source path is not a new top-level directory
   423  	for _, layout := range info.Layout {
   424  		cleanPathSrc := info.ExpandSnapVariables(filepath.Clean(layout.Path))
   425  		elems := strings.SplitN(cleanPathSrc, string(os.PathSeparator), 3)
   426  		switch len(elems) {
   427  		// len(1) is either relative path or empty string, will be validated
   428  		// elsewhere
   429  		case 2, 3:
   430  			// if the first string is the empty string, then we have a top-level
   431  			// directory to check
   432  			if elems[0] != "" {
   433  				// not the empty string which means this was a relative
   434  				// specification, i.e. usr/src/doc
   435  				return fmt.Errorf("layout %q is a relative filename", layout.Path)
   436  			}
   437  			if elems[1] != "" {
   438  				// verify that the top-level directory is a supported one
   439  				// we can't create new top-level directories because that would
   440  				// require creating a mimic on top of "/" which we don't
   441  				// currently support
   442  				switch elems[1] {
   443  				// this list was produced by taking all of the top level
   444  				// directories in the core snap and removing the explicitly
   445  				// denied top-level directories
   446  				case "bin", "etc", "lib", "lib64", "meta", "mnt", "opt", "root", "sbin", "snap", "srv", "usr", "var", "writable":
   447  				default:
   448  					return fmt.Errorf("layout %q defines a new top-level directory %q", layout.Path, "/"+elems[1])
   449  				}
   450  			}
   451  		}
   452  	}
   453  
   454  	// Validate that each source path is used consistently as a file or as a directory.
   455  	sourceKindMap := make(map[string]string)
   456  	for _, path := range paths {
   457  		layout := info.Layout[path]
   458  		if layout.Bind != "" {
   459  			// Layout refers to a directory.
   460  			sourcePath := info.ExpandSnapVariables(layout.Bind)
   461  			if kind, ok := sourceKindMap[sourcePath]; ok {
   462  				if kind != "dir" {
   463  					return fmt.Errorf("layout %q refers to directory %q but another layout treats it as file", layout.Path, layout.Bind)
   464  				}
   465  			}
   466  			sourceKindMap[sourcePath] = "dir"
   467  		}
   468  		if layout.BindFile != "" {
   469  			// Layout refers to a file.
   470  			sourcePath := info.ExpandSnapVariables(layout.BindFile)
   471  			if kind, ok := sourceKindMap[sourcePath]; ok {
   472  				if kind != "file" {
   473  					return fmt.Errorf("layout %q refers to file %q but another layout treats it as a directory", layout.Path, layout.BindFile)
   474  				}
   475  			}
   476  			sourceKindMap[sourcePath] = "file"
   477  		}
   478  	}
   479  
   480  	// Validate that layout are not attempting to define elements that normally
   481  	// come from other snaps. This is separate from the ValidateLayout below to
   482  	// simplify argument passing.
   483  	thisSnapMntDir := filepath.Join("/snap/", info.SnapName())
   484  	for _, path := range paths {
   485  		if strings.HasPrefix(path, "/snap/") && !strings.HasPrefix(path, thisSnapMntDir) {
   486  			return fmt.Errorf("layout %q defines a layout in space belonging to another snap", path)
   487  		}
   488  	}
   489  
   490  	// Validate each layout item and collect resulting constraints.
   491  	constraints := make([]LayoutConstraint, 0, len(info.Layout))
   492  	for _, path := range paths {
   493  		layout := info.Layout[path]
   494  		if err := ValidateLayout(layout, constraints); err != nil {
   495  			return err
   496  		}
   497  		constraints = append(constraints, layout.constraint())
   498  	}
   499  	return nil
   500  }
   501  
   502  func plugsSlotsInterfacesNames(info *Info) error {
   503  	for plugName, plug := range info.Plugs {
   504  		if err := ValidatePlugName(plugName); err != nil {
   505  			return err
   506  		}
   507  		if err := ValidateInterfaceName(plug.Interface); err != nil {
   508  			return fmt.Errorf("invalid interface name %q for plug %q", plug.Interface, plugName)
   509  		}
   510  	}
   511  	for slotName, slot := range info.Slots {
   512  		if err := ValidateSlotName(slotName); err != nil {
   513  			return err
   514  		}
   515  		if err := ValidateInterfaceName(slot.Interface); err != nil {
   516  			return fmt.Errorf("invalid interface name %q for slot %q", slot.Interface, slotName)
   517  		}
   518  	}
   519  	return nil
   520  }
   521  func plugsSlotsUniqueNames(info *Info) error {
   522  	// we could choose the smaller collection if we wanted to optimize this check
   523  	for plugName := range info.Plugs {
   524  		if info.Slots[plugName] != nil {
   525  			return fmt.Errorf("cannot have plug and slot with the same name: %q", plugName)
   526  		}
   527  	}
   528  	return nil
   529  }
   530  
   531  func validateField(name, cont string, whitelist *regexp.Regexp) error {
   532  	if !whitelist.MatchString(cont) {
   533  		return fmt.Errorf("app description field '%s' contains illegal %q (legal: '%s')", name, cont, whitelist)
   534  
   535  	}
   536  	return nil
   537  }
   538  
   539  func validateAppSocket(socket *SocketInfo) error {
   540  	if err := validateSocketName(socket.Name); err != nil {
   541  		return err
   542  	}
   543  
   544  	if err := validateSocketMode(socket.SocketMode); err != nil {
   545  		return err
   546  	}
   547  	return validateSocketAddr(socket, "listen-stream", socket.ListenStream)
   548  }
   549  
   550  // validateAppOrderCycles checks for cycles in app ordering dependencies
   551  func validateAppOrderCycles(apps []*AppInfo) error {
   552  	if _, err := SortServices(apps); err != nil {
   553  		return err
   554  	}
   555  	return nil
   556  }
   557  
   558  func validateAppOrderNames(app *AppInfo, dependencies []string) error {
   559  	// we must be a service to request ordering
   560  	if len(dependencies) > 0 && !app.IsService() {
   561  		return errors.New("must be a service to define before/after ordering")
   562  	}
   563  
   564  	for _, dep := range dependencies {
   565  		// dependency is not defined
   566  		other, ok := app.Snap.Apps[dep]
   567  		if !ok {
   568  			return fmt.Errorf("before/after references a missing application %q", dep)
   569  		}
   570  
   571  		if !other.IsService() {
   572  			return fmt.Errorf("before/after references a non-service application %q", dep)
   573  		}
   574  
   575  		if app.DaemonScope != other.DaemonScope {
   576  			return fmt.Errorf("before/after references service with different daemon-scope %q", dep)
   577  		}
   578  	}
   579  	return nil
   580  }
   581  
   582  func validateAppTimeouts(app *AppInfo) error {
   583  	type T struct {
   584  		desc    string
   585  		timeout timeout.Timeout
   586  	}
   587  	for _, t := range []T{
   588  		{"start-timeout", app.StartTimeout},
   589  		{"stop-timeout", app.StopTimeout},
   590  		{"watchdog-timeout", app.WatchdogTimeout},
   591  	} {
   592  		if t.timeout == 0 {
   593  			continue
   594  		}
   595  		if !app.IsService() {
   596  			return fmt.Errorf("%s is only applicable to services", t.desc)
   597  		}
   598  		if t.timeout < 0 {
   599  			return fmt.Errorf("%s cannot be negative", t.desc)
   600  		}
   601  	}
   602  	return nil
   603  }
   604  
   605  func validateAppTimer(app *AppInfo) error {
   606  	if app.Timer == nil {
   607  		return nil
   608  	}
   609  
   610  	if !app.IsService() {
   611  		return errors.New("timer is only applicable to services")
   612  	}
   613  
   614  	if _, err := timeutil.ParseSchedule(app.Timer.Timer); err != nil {
   615  		return fmt.Errorf("timer has invalid format: %v", err)
   616  	}
   617  
   618  	return nil
   619  }
   620  
   621  func validateAppRestart(app *AppInfo) error {
   622  	// app.RestartCond value is validated when unmarshalling
   623  
   624  	if app.RestartDelay == 0 && app.RestartCond == "" {
   625  		return nil
   626  	}
   627  
   628  	if app.RestartDelay != 0 {
   629  		if !app.IsService() {
   630  			return errors.New("restart-delay is only applicable to services")
   631  		}
   632  
   633  		if app.RestartDelay < 0 {
   634  			return errors.New("restart-delay cannot be negative")
   635  		}
   636  	}
   637  
   638  	if app.RestartCond != "" {
   639  		if !app.IsService() {
   640  			return errors.New("restart-condition is only applicable to services")
   641  		}
   642  	}
   643  	return nil
   644  }
   645  
   646  func validateAppActivatesOn(app *AppInfo) error {
   647  	if len(app.ActivatesOn) == 0 {
   648  		return nil
   649  	}
   650  
   651  	if !app.IsService() {
   652  		return errors.New("activates-on is only applicable to services")
   653  	}
   654  
   655  	for _, slot := range app.ActivatesOn {
   656  		// ActivatesOn slots must use the "dbus" interface
   657  		if slot.Interface != "dbus" {
   658  			return fmt.Errorf("invalid activates-on value %q: slot does not use dbus interface", slot.Name)
   659  		}
   660  
   661  		// D-Bus slots must match the daemon scope
   662  		bus := slot.Attrs["bus"]
   663  		if app.DaemonScope == SystemDaemon && bus != "system" || app.DaemonScope == UserDaemon && bus != "session" {
   664  			return fmt.Errorf("invalid activates-on value %q: bus %q does not match daemon-scope %q", slot.Name, bus, app.DaemonScope)
   665  		}
   666  
   667  		// Slots must only be activatable on a single app
   668  		for _, otherApp := range slot.Apps {
   669  			if otherApp == app {
   670  				continue
   671  			}
   672  			for _, otherSlot := range otherApp.ActivatesOn {
   673  				if otherSlot == slot {
   674  					return fmt.Errorf("invalid activates-on value %q: slot is also activatable on app %q", slot.Name, otherApp.Name)
   675  				}
   676  			}
   677  		}
   678  	}
   679  
   680  	return nil
   681  }
   682  
   683  // appContentWhitelist is the whitelist of legal chars in the "apps"
   684  // section of snap.yaml. Do not allow any of [',",`] here or snap-exec
   685  // will get confused. chainContentWhitelist is the same, but for the
   686  // command-chain, which also doesn't allow whitespace.
   687  var appContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/. _#:$-]*$`)
   688  var commandChainContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/._#:$-]*$`)
   689  
   690  // ValidAppName tells whether a string is a valid application name.
   691  func ValidAppName(n string) bool {
   692  	return naming.ValidateApp(n) == nil
   693  }
   694  
   695  // ValidateApp verifies the content in the app info.
   696  func ValidateApp(app *AppInfo) error {
   697  	switch app.Daemon {
   698  	case "", "simple", "forking", "oneshot", "dbus", "notify":
   699  		// valid
   700  	default:
   701  		return fmt.Errorf(`"daemon" field contains invalid value %q`, app.Daemon)
   702  	}
   703  
   704  	switch app.DaemonScope {
   705  	case "":
   706  		if app.Daemon != "" {
   707  			return fmt.Errorf(`"daemon-scope" must be set for daemons`)
   708  		}
   709  	case SystemDaemon, UserDaemon:
   710  		if app.Daemon == "" {
   711  			return fmt.Errorf(`"daemon-scope" can only be set for daemons`)
   712  		}
   713  	default:
   714  		return fmt.Errorf(`invalid "daemon-scope": %q`, app.DaemonScope)
   715  	}
   716  
   717  	// Validate app name
   718  	if !ValidAppName(app.Name) {
   719  		return fmt.Errorf("cannot have %q as app name - use letters, digits, and dash as separator", app.Name)
   720  	}
   721  
   722  	// Validate the rest of the app info
   723  	checks := map[string]string{
   724  		"command":           app.Command,
   725  		"stop-command":      app.StopCommand,
   726  		"reload-command":    app.ReloadCommand,
   727  		"post-stop-command": app.PostStopCommand,
   728  		"bus-name":          app.BusName,
   729  	}
   730  
   731  	for name, value := range checks {
   732  		if err := validateField(name, value, appContentWhitelist); err != nil {
   733  			return err
   734  		}
   735  	}
   736  
   737  	// Also validate the command chain
   738  	for _, value := range app.CommandChain {
   739  		if err := validateField("command-chain", value, commandChainContentWhitelist); err != nil {
   740  			return err
   741  		}
   742  	}
   743  
   744  	// Socket activation requires the "network-bind" plug
   745  	if len(app.Sockets) > 0 {
   746  		if _, ok := app.Plugs["network-bind"]; !ok {
   747  			return fmt.Errorf(`"network-bind" interface plug is required when sockets are used`)
   748  		}
   749  	}
   750  
   751  	for _, socket := range app.Sockets {
   752  		if err := validateAppSocket(socket); err != nil {
   753  			return fmt.Errorf("invalid definition of socket %q: %v", socket.Name, err)
   754  		}
   755  	}
   756  
   757  	if err := validateAppActivatesOn(app); err != nil {
   758  		return err
   759  	}
   760  
   761  	if err := validateAppRestart(app); err != nil {
   762  		return err
   763  	}
   764  	if err := validateAppOrderNames(app, app.Before); err != nil {
   765  		return err
   766  	}
   767  	if err := validateAppOrderNames(app, app.After); err != nil {
   768  		return err
   769  	}
   770  
   771  	if err := validateAppTimeouts(app); err != nil {
   772  		return err
   773  	}
   774  
   775  	// validate stop-mode
   776  	if err := app.StopMode.Validate(); err != nil {
   777  		return err
   778  	}
   779  	// validate refresh-mode
   780  	switch app.RefreshMode {
   781  	case "", "endure", "restart":
   782  		// valid
   783  	default:
   784  		return fmt.Errorf(`"refresh-mode" field contains invalid value %q`, app.RefreshMode)
   785  	}
   786  	// validate install-mode
   787  	switch app.InstallMode {
   788  	case "", "enable", "disable":
   789  		// valid
   790  	default:
   791  		return fmt.Errorf(`"install-mode" field contains invalid value %q`, app.InstallMode)
   792  	}
   793  	if app.StopMode != "" && app.Daemon == "" {
   794  		return fmt.Errorf(`"stop-mode" cannot be used for %q, only for services`, app.Name)
   795  	}
   796  	if app.RefreshMode != "" && app.Daemon == "" {
   797  		return fmt.Errorf(`"refresh-mode" cannot be used for %q, only for services`, app.Name)
   798  	}
   799  	if app.InstallMode != "" && app.Daemon == "" {
   800  		return fmt.Errorf(`"install-mode" cannot be used for %q, only for services`, app.Name)
   801  	}
   802  
   803  	return validateAppTimer(app)
   804  }
   805  
   806  // ValidatePathVariables ensures that given path contains only $SNAP, $SNAP_DATA or $SNAP_COMMON.
   807  func ValidatePathVariables(path string) error {
   808  	for path != "" {
   809  		start := strings.IndexRune(path, '$')
   810  		if start < 0 {
   811  			break
   812  		}
   813  		path = path[start+1:]
   814  		end := strings.IndexFunc(path, func(c rune) bool {
   815  			return (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_'
   816  		})
   817  		if end < 0 {
   818  			end = len(path)
   819  		}
   820  		v := path[:end]
   821  		if v != "SNAP" && v != "SNAP_DATA" && v != "SNAP_COMMON" {
   822  			return fmt.Errorf("reference to unknown variable %q", "$"+v)
   823  		}
   824  		path = path[end:]
   825  	}
   826  	return nil
   827  }
   828  
   829  func isAbsAndClean(path string) bool {
   830  	return (filepath.IsAbs(path) || strings.HasPrefix(path, "$")) && filepath.Clean(path) == path
   831  }
   832  
   833  // LayoutConstraint abstracts validation of conflicting layout elements.
   834  type LayoutConstraint interface {
   835  	IsOffLimits(path string) bool
   836  }
   837  
   838  // mountedTree represents a mounted file-system tree or a bind-mounted directory.
   839  type mountedTree string
   840  
   841  // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path.
   842  func (mountPoint mountedTree) IsOffLimits(path string) bool {
   843  	return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
   844  }
   845  
   846  // mountedFile represents a bind-mounted file.
   847  type mountedFile string
   848  
   849  // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path.
   850  func (mountPoint mountedFile) IsOffLimits(path string) bool {
   851  	return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
   852  }
   853  
   854  // symlinkFile represents a layout using symbolic link.
   855  type symlinkFile string
   856  
   857  // IsOffLimits returns true for mounted files  if a path is identical to the path of the mount point.
   858  func (mountPoint symlinkFile) IsOffLimits(path string) bool {
   859  	return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
   860  }
   861  
   862  func (layout *Layout) constraint() LayoutConstraint {
   863  	path := layout.Snap.ExpandSnapVariables(layout.Path)
   864  	if layout.Symlink != "" {
   865  		return symlinkFile(path)
   866  	} else if layout.BindFile != "" {
   867  		return mountedFile(path)
   868  	}
   869  	return mountedTree(path)
   870  }
   871  
   872  // layoutRejectionList contains directories that cannot be used as layout
   873  // targets. Nothing there, or underneath can be replaced with $SNAP or
   874  // $SNAP_DATA, or $SNAP_COMMON content, even from the point of view of a single
   875  // snap.
   876  var layoutRejectionList = []string{
   877  	// Special locations that need to retain their properties:
   878  
   879  	// The /dev directory contains essential device nodes and there's no valid
   880  	// reason to allow snaps to replace it.
   881  	"/dev",
   882  	// The /proc directory contains essential process meta-data and
   883  	// miscellaneous kernel configuration parameters and there is no valid
   884  	// reason to allow snaps to replace it.
   885  	"/proc",
   886  	// The /sys directory exposes many kernel internals, similar to /proc and
   887  	// there is no known reason to allow snaps to replace it.
   888  	"/sys",
   889  	// The media directory is mounted with bi-directional mount event sharing.
   890  	// Any mount operations there are reflected in the host's view of /media,
   891  	// which may be either itself or /run/media.
   892  	"/media",
   893  	// The /run directory contains various ephemeral information files or
   894  	// sockets used by various programs. Providing view of the true /run allows
   895  	// snap applications to be integrated with the rest of the system and
   896  	// therefore snaps should not be allowed to replace it.
   897  	"/run",
   898  	// The /tmp directory contains a private, per-snap, view of /tmp and
   899  	// there's no valid reason to allow snaps to replace it.
   900  	"/tmp",
   901  	// The /var/lib/snapd directory contains essential snapd state and is
   902  	// sometimes consulted from inside the mount namespace.
   903  	"/var/lib/snapd",
   904  
   905  	// Locations that may be used to attack the host:
   906  
   907  	// The firmware is sometimes loaded on demand by the kernel, in response to
   908  	// a process performing generic I/O to a specific device. In that case the
   909  	// mount namespace of the process is searched, by the kernel, for the
   910  	// firmware. Therefore firmware must not be replaceable to prevent
   911  	// malicious firmware from attacking the host.
   912  	"/lib/firmware",
   913  	// Similarly the kernel will load modules and the modules should not be
   914  	// something that snaps can tamper with.
   915  	"/lib/modules",
   916  
   917  	// Locations that store essential data:
   918  
   919  	// The /var/snap directory contains system-wide state of particular snaps
   920  	// and should not be replaced as it would break content interface
   921  	// connections that use $SNAP_DATA or $SNAP_COMMON.
   922  	"/var/snap",
   923  	// The /home directory contains user data, including $SNAP_USER_DATA,
   924  	// $SNAP_USER_COMMON and should be disallowed for the same reasons as
   925  	// /var/snap.
   926  	"/home",
   927  
   928  	// Locations that should be pristine to avoid confusion.
   929  
   930  	// There's no known reason to allow snaps to replace things there.
   931  	"/boot",
   932  	// The lost+found directory is used by fsck tools to link lost blocks back
   933  	// into the filesystem tree. Using layouts for this element is just
   934  	// confusing and there is no valid reason to allow it.
   935  	"/lost+found",
   936  }
   937  
   938  // ValidateLayout ensures that the given layout contains only valid subset of constructs.
   939  func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error {
   940  	si := layout.Snap
   941  	// Rules for validating layouts:
   942  	//
   943  	// * source of mount --bind must be in on of $SNAP, $SNAP_DATA or $SNAP_COMMON
   944  	// * target of symlink must in in one of $SNAP, $SNAP_DATA, or $SNAP_COMMON
   945  	// * may not mount on top of an existing layout mountpoint
   946  
   947  	mountPoint := layout.Path
   948  
   949  	if mountPoint == "" {
   950  		return errors.New("layout cannot use an empty path")
   951  	}
   952  
   953  	if err := ValidatePathVariables(mountPoint); err != nil {
   954  		return fmt.Errorf("layout %q uses invalid mount point: %s", layout.Path, err)
   955  	}
   956  	mountPoint = si.ExpandSnapVariables(mountPoint)
   957  	if !isAbsAndClean(mountPoint) {
   958  		return fmt.Errorf("layout %q uses invalid mount point: must be absolute and clean", layout.Path)
   959  	}
   960  
   961  	for _, path := range layoutRejectionList {
   962  		// We use the mountedTree constraint as this has the right semantics.
   963  		if mountedTree(path).IsOffLimits(mountPoint) {
   964  			return fmt.Errorf("layout %q in an off-limits area", layout.Path)
   965  		}
   966  	}
   967  
   968  	for _, constraint := range constraints {
   969  		if constraint.IsOffLimits(mountPoint) {
   970  			return fmt.Errorf("layout %q underneath prior layout item %q", layout.Path, constraint)
   971  		}
   972  	}
   973  
   974  	var nused int
   975  	if layout.Bind != "" {
   976  		nused++
   977  	}
   978  	if layout.BindFile != "" {
   979  		nused++
   980  	}
   981  	if layout.Type != "" {
   982  		nused++
   983  	}
   984  	if layout.Symlink != "" {
   985  		nused++
   986  	}
   987  	if nused != 1 {
   988  		return fmt.Errorf("layout %q must define a bind mount, a filesystem mount or a symlink", layout.Path)
   989  	}
   990  
   991  	if layout.Bind != "" || layout.BindFile != "" {
   992  		mountSource := layout.Bind + layout.BindFile
   993  		if err := ValidatePathVariables(mountSource); err != nil {
   994  			return fmt.Errorf("layout %q uses invalid bind mount source %q: %s", layout.Path, mountSource, err)
   995  		}
   996  		mountSource = si.ExpandSnapVariables(mountSource)
   997  		if !isAbsAndClean(mountSource) {
   998  			return fmt.Errorf("layout %q uses invalid bind mount source %q: must be absolute and clean", layout.Path, mountSource)
   999  		}
  1000  		// Bind mounts *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as bind
  1001  		// mount source. This is done so that snaps cannot bypass restrictions
  1002  		// by mounting something outside into their own space.
  1003  		if !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP")) &&
  1004  			!strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_DATA")) &&
  1005  			!strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_COMMON")) {
  1006  			return fmt.Errorf("layout %q uses invalid bind mount source %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, mountSource)
  1007  		}
  1008  	}
  1009  
  1010  	switch layout.Type {
  1011  	case "tmpfs":
  1012  	case "":
  1013  		// nothing to do
  1014  	default:
  1015  		return fmt.Errorf("layout %q uses invalid filesystem %q", layout.Path, layout.Type)
  1016  	}
  1017  
  1018  	if layout.Symlink != "" {
  1019  		oldname := layout.Symlink
  1020  		if err := ValidatePathVariables(oldname); err != nil {
  1021  			return fmt.Errorf("layout %q uses invalid symlink old name %q: %s", layout.Path, oldname, err)
  1022  		}
  1023  		oldname = si.ExpandSnapVariables(oldname)
  1024  		if !isAbsAndClean(oldname) {
  1025  			return fmt.Errorf("layout %q uses invalid symlink old name %q: must be absolute and clean", layout.Path, oldname)
  1026  		}
  1027  		// Symlinks *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as oldname.
  1028  		// This is done so that snaps cannot attempt to bypass restrictions
  1029  		// by mounting something outside into their own space.
  1030  		if !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP")) &&
  1031  			!strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_DATA")) &&
  1032  			!strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_COMMON")) {
  1033  			return fmt.Errorf("layout %q uses invalid symlink old name %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, oldname)
  1034  		}
  1035  	}
  1036  
  1037  	// When new users and groups are supported those must be added to interfaces/mount/spec.go as well.
  1038  	// For now only "root" is allowed (and default).
  1039  
  1040  	switch layout.User {
  1041  	case "root", "":
  1042  	// TODO: allow declared snap user and group names.
  1043  	default:
  1044  		return fmt.Errorf("layout %q uses invalid user %q", layout.Path, layout.User)
  1045  	}
  1046  	switch layout.Group {
  1047  	case "root", "":
  1048  	default:
  1049  		return fmt.Errorf("layout %q uses invalid group %q", layout.Path, layout.Group)
  1050  	}
  1051  
  1052  	if layout.Mode&01777 != layout.Mode {
  1053  		return fmt.Errorf("layout %q uses invalid mode %#o", layout.Path, layout.Mode)
  1054  	}
  1055  	return nil
  1056  }
  1057  
  1058  func ValidateCommonIDs(info *Info) error {
  1059  	seen := make(map[string]string, len(info.Apps))
  1060  	for _, app := range info.Apps {
  1061  		if app.CommonID != "" {
  1062  			if other, was := seen[app.CommonID]; was {
  1063  				return fmt.Errorf("application %q common-id %q must be unique, already used by application %q",
  1064  					app.Name, app.CommonID, other)
  1065  			}
  1066  			seen[app.CommonID] = app.Name
  1067  		}
  1068  	}
  1069  	return nil
  1070  }
  1071  
  1072  func ValidateSystemUsernames(info *Info) error {
  1073  	for username := range info.SystemUsernames {
  1074  		if !osutil.IsValidUsername(username) {
  1075  			return fmt.Errorf("invalid system username %q", username)
  1076  		}
  1077  	}
  1078  	return nil
  1079  }
  1080  
  1081  // NeededDefaultProviders returns a map keyed by the names of all
  1082  // default-providers for the content plugs that the given snap.Info
  1083  // needs. The map values are the corresponding content tags.
  1084  func NeededDefaultProviders(info *Info) (providerSnapsToContentTag map[string][]string) {
  1085  	providerSnapsToContentTag = make(map[string][]string)
  1086  	for _, plug := range info.Plugs {
  1087  		gatherDefaultContentProvider(providerSnapsToContentTag, plug)
  1088  	}
  1089  	return providerSnapsToContentTag
  1090  }
  1091  
  1092  // ValidateBasesAndProviders checks that all bases/default-providers are part of the seed
  1093  func ValidateBasesAndProviders(snapInfos []*Info) []error {
  1094  	all := naming.NewSnapSet(nil)
  1095  	for _, info := range snapInfos {
  1096  		all.Add(info)
  1097  	}
  1098  
  1099  	var errs []error
  1100  	for _, info := range snapInfos {
  1101  		// ensure base is available
  1102  		if info.Base != "" && info.Base != "none" {
  1103  			if !all.Contains(naming.Snap(info.Base)) {
  1104  				errs = append(errs, fmt.Errorf("cannot use snap %q: base %q is missing", info.InstanceName(), info.Base))
  1105  			}
  1106  		}
  1107  		// ensure core is available
  1108  		if info.Base == "" && info.SnapType == TypeApp && info.InstanceName() != "snapd" {
  1109  			if !all.Contains(naming.Snap("core")) {
  1110  				errs = append(errs, fmt.Errorf(`cannot use snap %q: required snap "core" missing`, info.InstanceName()))
  1111  			}
  1112  		}
  1113  		// ensure default-providers are available
  1114  		for dp := range NeededDefaultProviders(info) {
  1115  			if !all.Contains(naming.Snap(dp)) {
  1116  				errs = append(errs, fmt.Errorf("cannot use snap %q: default provider %q is missing", info.InstanceName(), dp))
  1117  			}
  1118  		}
  1119  	}
  1120  	return errs
  1121  }