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