github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/gadget/validate.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 gadget
    21  
    22  import (
    23  	"fmt"
    24  	"path/filepath"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/kernel"
    29  	"github.com/snapcore/snapd/osutil"
    30  	"github.com/snapcore/snapd/strutil"
    31  )
    32  
    33  // ValidationConstraints carries extra constraints on top of those
    34  // implied by the model to use for gadget validation.
    35  // They might be constraints that are determined only at runtime.
    36  type ValidationConstraints struct {
    37  	// EncryptedData when true indicates that the gadget will be used on a
    38  	// device where the data partition will be encrypted.
    39  	EncryptedData bool
    40  }
    41  
    42  // Validate checks that the given gadget metadata matches the
    43  // consistency rules for role usage, labels etc as implied by the
    44  // model and extra constraints that might be known only at runtime.
    45  func Validate(info *Info, model Model, extra *ValidationConstraints) error {
    46  	if err := ruleValidateVolumes(info.Volumes, model); err != nil {
    47  		return err
    48  	}
    49  	if extra != nil {
    50  		if extra.EncryptedData {
    51  			if err := validateEncryptionSupport(info); err != nil {
    52  				return fmt.Errorf("gadget does not support encrypted data: %v", err)
    53  			}
    54  		}
    55  	}
    56  	return nil
    57  }
    58  
    59  func validateEncryptionSupport(info *Info) error {
    60  	for name, vol := range info.Volumes {
    61  		var haveSave bool
    62  		for _, s := range vol.Structure {
    63  			if s.Role == SystemSave {
    64  				haveSave = true
    65  			}
    66  		}
    67  		if !haveSave {
    68  			return fmt.Errorf("volume %q has no structure with system-save role", name)
    69  		}
    70  		// TODO:UC20: shall we make sure that size of ubuntu-save is reasonable?
    71  	}
    72  	return nil
    73  }
    74  
    75  type roleInstance struct {
    76  	volName string
    77  	s       *VolumeStructure
    78  }
    79  
    80  func ruleValidateVolumes(vols map[string]*Volume, model Model) error {
    81  	roles := map[string]*roleInstance{
    82  		SystemSeed: nil,
    83  		SystemBoot: nil,
    84  		SystemData: nil,
    85  		SystemSave: nil,
    86  	}
    87  
    88  	xvols := ""
    89  	if len(vols) != 1 {
    90  		xvols = " across volumes"
    91  	}
    92  
    93  	// TODO: is this too strict for old gadgets?
    94  	for name, v := range vols {
    95  		for i := range v.Structure {
    96  			s := &v.Structure[i]
    97  			if inst, ok := roles[s.Role]; ok {
    98  				if inst != nil {
    99  					return fmt.Errorf("cannot have more than one partition with %s role%s", s.Role, xvols)
   100  				}
   101  				roles[s.Role] = &roleInstance{
   102  					volName: name,
   103  					s:       s,
   104  				}
   105  			}
   106  		}
   107  	}
   108  
   109  	expectedSeed := false
   110  	if model != nil {
   111  		expectedSeed = wantsSystemSeed(model)
   112  	} else {
   113  		// if system-seed role is mentioned assume the uc20
   114  		// consistency rules
   115  		expectedSeed = roles[SystemSeed] != nil
   116  	}
   117  
   118  	for name, v := range vols {
   119  		if err := ruleValidateVolume(name, v, expectedSeed); err != nil {
   120  			return fmt.Errorf("invalid volume %q: %v", name, err)
   121  		}
   122  	}
   123  
   124  	if err := ensureRolesConsistency(roles, expectedSeed); err != nil {
   125  		return err
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func ruleValidateVolume(name string, vol *Volume, expectedSeed bool) error {
   132  	for idx, s := range vol.Structure {
   133  		if err := ruleValidateVolumeStructure(&s, expectedSeed); err != nil {
   134  			return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err)
   135  		}
   136  	}
   137  
   138  	return nil
   139  }
   140  
   141  func ruleValidateVolumeStructure(vs *VolumeStructure, expectedSeed bool) error {
   142  	var reservedLabels []string
   143  	if expectedSeed {
   144  		reservedLabels = reservedLabelsWithSeed
   145  	} else {
   146  		reservedLabels = reservedLabelsWithoutSeed
   147  	}
   148  	if err := validateReservedLabels(vs, reservedLabels); err != nil {
   149  		return err
   150  	}
   151  	return nil
   152  }
   153  
   154  var (
   155  	reservedLabelsWithSeed = []string{
   156  		ubuntuBootLabel,
   157  		ubuntuSeedLabel,
   158  		ubuntuDataLabel,
   159  		ubuntuSaveLabel,
   160  	}
   161  
   162  	// labels that we don't expect to be used on a UC16/18 system:
   163  	//  * seed needs to be the ESP so there's a conflict
   164  	//  * ubuntu-data is the main data partition which on UC16/18
   165  	//    is expected to be named writable instead
   166  	reservedLabelsWithoutSeed = []string{
   167  		ubuntuSeedLabel,
   168  		ubuntuDataLabel,
   169  	}
   170  )
   171  
   172  func validateReservedLabels(vs *VolumeStructure, reservedLabels []string) error {
   173  	if vs.Role != "" {
   174  		// structure specifies a role, its labels will be checked later
   175  		return nil
   176  	}
   177  	if vs.Label == "" {
   178  		return nil
   179  	}
   180  	if strutil.ListContains(reservedLabels, vs.Label) {
   181  		// a structure without a role uses one of reserved labels
   182  		return fmt.Errorf("label %q is reserved", vs.Label)
   183  	}
   184  	return nil
   185  }
   186  
   187  func ensureRolesConsistency(roles map[string]*roleInstance, expectedSeed bool) error {
   188  	// TODO: should we validate usage of uc20 specific system-recovery-{image,select}
   189  	//       roles too? they should only be used on uc20 systems, so models that
   190  	//       have a grade set and are not classic
   191  
   192  	switch {
   193  	case roles[SystemSeed] == nil && roles[SystemData] == nil:
   194  		if expectedSeed {
   195  			return fmt.Errorf("model requires system-seed partition, but no system-seed or system-data partition found")
   196  		}
   197  	case roles[SystemSeed] != nil && roles[SystemData] == nil:
   198  		return fmt.Errorf("the system-seed role requires system-data to be defined")
   199  	case roles[SystemSeed] == nil && roles[SystemData] != nil:
   200  		// error if we have the SystemSeed constraint but no actual system-seed structure
   201  		if expectedSeed {
   202  			return fmt.Errorf("model requires system-seed structure, but none was found")
   203  		}
   204  		// without SystemSeed, system-data label must be implicit or writable
   205  		if err := checkImplicitLabel(SystemData, roles[SystemData].s, implicitSystemDataLabel); err != nil {
   206  			return err
   207  		}
   208  	case roles[SystemSeed] != nil && roles[SystemData] != nil:
   209  		// error if we don't have the SystemSeed constraint but we have a system-seed structure
   210  		if !expectedSeed {
   211  			return fmt.Errorf("model does not support the system-seed role")
   212  		}
   213  		if err := checkSeedDataImplicitLabels(roles); err != nil {
   214  			return err
   215  		}
   216  	}
   217  	if roles[SystemSave] != nil {
   218  		if !expectedSeed {
   219  			return fmt.Errorf("model does not support the system-save role")
   220  		}
   221  		if err := ensureSystemSaveRuleConsistency(roles); err != nil {
   222  			return err
   223  		}
   224  	}
   225  
   226  	if expectedSeed {
   227  		// make sure that all roles come from the same volume
   228  		// TODO:UC20: there is more to do in order to support multi-volume situations
   229  
   230  		// if SystemSeed is unset we must have failed earlier
   231  		seedVolName := roles[SystemSeed].volName
   232  
   233  		for _, otherRole := range []string{SystemBoot, SystemData, SystemSave} {
   234  			ri := roles[otherRole]
   235  			if ri != nil && ri.volName != seedVolName {
   236  				return fmt.Errorf("system-boot, system-data, and system-save are expected to share the same volume as system-seed")
   237  			}
   238  		}
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  func ensureSystemSaveRuleConsistency(roles map[string]*roleInstance) error {
   245  	if roles[SystemData] == nil || roles[SystemSeed] == nil {
   246  		// previous checks should stop reaching here
   247  		return fmt.Errorf("internal error: system-save requires system-seed and system-data structures")
   248  	}
   249  	if err := checkImplicitLabel(SystemSave, roles[SystemSave].s, ubuntuSaveLabel); err != nil {
   250  		return err
   251  	}
   252  	return nil
   253  }
   254  
   255  func checkSeedDataImplicitLabels(roles map[string]*roleInstance) error {
   256  	if err := checkImplicitLabel(SystemData, roles[SystemData].s, ubuntuDataLabel); err != nil {
   257  		return err
   258  	}
   259  	if err := checkImplicitLabel(SystemSeed, roles[SystemSeed].s, ubuntuSeedLabel); err != nil {
   260  		return err
   261  	}
   262  	return nil
   263  }
   264  
   265  func checkImplicitLabel(role string, vs *VolumeStructure, implicitLabel string) error {
   266  	if vs.Label != "" && vs.Label != implicitLabel {
   267  		return fmt.Errorf("%s structure must have an implicit label or %q, not %q", role, implicitLabel, vs.Label)
   268  
   269  	}
   270  	return nil
   271  }
   272  
   273  // content validation
   274  
   275  func splitKernelRef(kernelRef string) (asset, content string, err error) {
   276  	// kernel ref has format: $kernel:<asset-name>/<content-path> where
   277  	// asset name and content is listed in kernel.yaml, content looks like a
   278  	// sane path
   279  	if !strings.HasPrefix(kernelRef, "$kernel:") {
   280  		return "", "", fmt.Errorf("internal error: splitKernelRef called for non kernel ref %q", kernelRef)
   281  	}
   282  	assetAndContent := kernelRef[len("$kernel:"):]
   283  	l := strings.SplitN(assetAndContent, "/", 2)
   284  	if len(l) < 2 {
   285  		return "", "", fmt.Errorf("invalid asset and content in kernel ref %q", kernelRef)
   286  	}
   287  	asset = l[0]
   288  	content = l[1]
   289  	nonDirContent := content
   290  	if strings.HasSuffix(nonDirContent, "/") {
   291  		// a single trailing / is allowed to indicate all content under directory
   292  		nonDirContent = strings.TrimSuffix(nonDirContent, "/")
   293  	}
   294  	if len(asset) == 0 || len(content) == 0 {
   295  		return "", "", fmt.Errorf("missing asset name or content in kernel ref %q", kernelRef)
   296  	}
   297  	if filepath.Clean(nonDirContent) != nonDirContent || strings.Contains(content, "..") || nonDirContent == "/" {
   298  		return "", "", fmt.Errorf("invalid content in kernel ref %q", kernelRef)
   299  	}
   300  	if !kernel.ValidAssetName.MatchString(asset) {
   301  		return "", "", fmt.Errorf("invalid asset name in kernel ref %q", kernelRef)
   302  	}
   303  	return asset, content, nil
   304  }
   305  
   306  func validateVolumeContentsPresence(gadgetSnapRootDir string, vol *LaidOutVolume) error {
   307  	// bare structure content is checked to exist during layout
   308  	// make sure that filesystem content source paths exist as well
   309  	for _, s := range vol.LaidOutStructure {
   310  		if !s.HasFilesystem() {
   311  			continue
   312  		}
   313  		for _, c := range s.Content {
   314  			// TODO: detect and skip Content with "$kernel:" style
   315  			// refs if there is no kernelSnapRootDir passed in as
   316  			// well
   317  			if strings.HasPrefix(c.UnresolvedSource, "$kernel:") {
   318  				// This only validates that the ref is valid.
   319  				// Resolving happens with ResolveContentPaths()
   320  				if _, _, err := splitKernelRef(c.UnresolvedSource); err != nil {
   321  					return fmt.Errorf("cannot use kernel reference %q: %v", c.UnresolvedSource, err)
   322  				}
   323  				continue
   324  			}
   325  			realSource := filepath.Join(gadgetSnapRootDir, c.UnresolvedSource)
   326  			if !osutil.FileExists(realSource) {
   327  				return fmt.Errorf("structure %v, content %v: source path does not exist", s, c)
   328  			}
   329  			if strings.HasSuffix(c.UnresolvedSource, "/") {
   330  				// expecting a directory
   331  				if err := checkSourceIsDir(realSource + "/"); err != nil {
   332  					return fmt.Errorf("structure %v, content %v: %v", s, c, err)
   333  				}
   334  			}
   335  		}
   336  	}
   337  	return nil
   338  }
   339  
   340  // ValidateContent checks whether the given directory contains valid matching content with respect to the given pre-validated gadget metadata.
   341  func ValidateContent(info *Info, gadgetSnapRootDir, kernelSnapRootDir string) error {
   342  	// TODO: also validate that only one "<bl-name>.conf" file is
   343  	// in the root directory of the gadget snap, because the
   344  	// "<bl-name>.conf" file indicates precisely which bootloader
   345  	// the gadget uses and as such there cannot be more than one
   346  	// such bootloader
   347  	for name, vol := range info.Volumes {
   348  		constraints := DefaultConstraints
   349  		// At this point we may not know what kernel will be used
   350  		// with the gadget yet. Skip this check in this case.
   351  		if kernelSnapRootDir == "" {
   352  			constraints.SkipResolveContent = true
   353  		}
   354  		lv, err := LayoutVolume(gadgetSnapRootDir, kernelSnapRootDir, vol, constraints)
   355  		if err != nil {
   356  			return fmt.Errorf("invalid layout of volume %q: %v", name, err)
   357  		}
   358  		if err := validateVolumeContentsPresence(gadgetSnapRootDir, lv); err != nil {
   359  			return fmt.Errorf("invalid volume %q: %v", name, err)
   360  		}
   361  	}
   362  
   363  	// Ensure that at least one kernel.yaml reference can be resolved
   364  	// by the gadget
   365  	if kernelSnapRootDir != "" {
   366  		kinfo, err := kernel.ReadInfo(kernelSnapRootDir)
   367  		if err != nil {
   368  			return err
   369  		}
   370  		resolvedOnce := false
   371  		for _, vol := range info.Volumes {
   372  			err := gadgetVolumeConsumesOneKernelUpdateAsset(vol, kinfo)
   373  			if err == nil {
   374  				resolvedOnce = true
   375  			}
   376  		}
   377  		if !resolvedOnce {
   378  			return fmt.Errorf("no asset from the kernel.yaml needing synced update is consumed by the gadget at %q", gadgetSnapRootDir)
   379  		}
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  // gadgetVolumeConsumesOneKernelUpdateAsset ensures that at least one kernel
   386  // assets from the kernel.yaml has a reference in the given
   387  // LaidOutVolume.
   388  func gadgetVolumeConsumesOneKernelUpdateAsset(pNew *Volume, kernelInfo *kernel.Info) error {
   389  	notFoundAssets := make([]string, 0, len(kernelInfo.Assets))
   390  	for assetName, asset := range kernelInfo.Assets {
   391  		if !asset.Update {
   392  			continue
   393  		}
   394  		for _, ps := range pNew.Structure {
   395  			for _, rc := range ps.Content {
   396  				pathOrRef := rc.UnresolvedSource
   397  				if !strings.HasPrefix(pathOrRef, "$kernel:") {
   398  					// regular asset from the gadget snap
   399  					continue
   400  				}
   401  				wantedAsset, _, err := splitKernelRef(pathOrRef)
   402  				if err != nil {
   403  					return err
   404  				}
   405  				if assetName == wantedAsset {
   406  					// found a valid kernel asset,
   407  					// that is enough
   408  					return nil
   409  				}
   410  			}
   411  		}
   412  		notFoundAssets = append(notFoundAssets, assetName)
   413  	}
   414  	if len(notFoundAssets) > 0 {
   415  		sort.Strings(notFoundAssets)
   416  		return fmt.Errorf("gadget does not consume any of the kernel assets needing synced update %s", strutil.Quoted(notFoundAssets))
   417  	}
   418  	return nil
   419  }