gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/gadget/validate.go (about)

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