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