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