github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/gadget/gadget.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  	"bufio"
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"os"
    29  	"path/filepath"
    30  	"regexp"
    31  	"sort"
    32  	"strings"
    33  
    34  	"gopkg.in/yaml.v2"
    35  
    36  	"github.com/snapcore/snapd/asserts"
    37  	"github.com/snapcore/snapd/gadget/edition"
    38  	"github.com/snapcore/snapd/gadget/quantity"
    39  	"github.com/snapcore/snapd/metautil"
    40  	"github.com/snapcore/snapd/osutil"
    41  	"github.com/snapcore/snapd/snap"
    42  	"github.com/snapcore/snapd/snap/naming"
    43  	"github.com/snapcore/snapd/snap/snapfile"
    44  	"github.com/snapcore/snapd/strutil"
    45  )
    46  
    47  const (
    48  	// schemaMBR identifies a Master Boot Record partitioning schema, or an
    49  	// MBR like role
    50  	schemaMBR = "mbr"
    51  	// schemaGPT identifies a GUID Partition Table partitioning schema
    52  	schemaGPT = "gpt"
    53  
    54  	SystemBoot = "system-boot"
    55  	SystemData = "system-data"
    56  	SystemSeed = "system-seed"
    57  	SystemSave = "system-save"
    58  
    59  	// extracted kernels for all uc systems
    60  	bootImage = "system-boot-image"
    61  
    62  	// extracted kernels for recovery kernels for uc20 specifically
    63  	seedBootImage = "system-seed-image"
    64  
    65  	// bootloader specific partition which stores bootloader environment vars
    66  	// for purposes of booting normal run mode on uc20 and all modes on
    67  	// uc16 and uc18
    68  	bootSelect = "system-boot-select"
    69  
    70  	// bootloader specific partition which stores bootloader environment vars
    71  	// for purposes of booting recovery systems on uc20, i.e. recover or install
    72  	seedBootSelect = "system-seed-select"
    73  
    74  	// implicitSystemDataLabel is the implicit filesystem label of structure
    75  	// of system-data role
    76  	implicitSystemDataLabel = "writable"
    77  
    78  	// UC20 filesystem labels for roles
    79  	ubuntuBootLabel = "ubuntu-boot"
    80  	ubuntuSeedLabel = "ubuntu-seed"
    81  	ubuntuDataLabel = "ubuntu-data"
    82  	ubuntuSaveLabel = "ubuntu-save"
    83  
    84  	// only supported for legacy reasons
    85  	legacyBootImage  = "bootimg"
    86  	legacyBootSelect = "bootselect"
    87  )
    88  
    89  var (
    90  	validVolumeName = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]+$")
    91  	validTypeID     = regexp.MustCompile("^[0-9A-F]{2}$")
    92  	validGUUID      = regexp.MustCompile("^(?i)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$")
    93  )
    94  
    95  type Info struct {
    96  	Volumes map[string]*Volume `yaml:"volumes,omitempty"`
    97  
    98  	// Default configuration for snaps (snap-id => key => value).
    99  	Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"`
   100  
   101  	Connections []Connection `yaml:"connections"`
   102  }
   103  
   104  // Volume defines the structure and content for the image to be written into a
   105  // block device.
   106  type Volume struct {
   107  	// Schema describes the schema used for the volume
   108  	Schema string `yaml:"schema"`
   109  	// Bootloader names the bootloader used by the volume
   110  	Bootloader string `yaml:"bootloader"`
   111  	//  ID is a 2-hex digit disk ID or GPT GUID
   112  	ID string `yaml:"id"`
   113  	// Structure describes the structures that are part of the volume
   114  	Structure []VolumeStructure `yaml:"structure"`
   115  }
   116  
   117  // VolumeStructure describes a single structure inside a volume. A structure can
   118  // represent a partition, Master Boot Record, or any other contiguous range
   119  // within the volume.
   120  type VolumeStructure struct {
   121  	// Name, when non empty, provides the name of the structure
   122  	Name string `yaml:"name"`
   123  	// Label provides the filesystem label
   124  	Label string `yaml:"filesystem-label"`
   125  	// Offset defines a starting offset of the structure
   126  	Offset *quantity.Offset `yaml:"offset"`
   127  	// OffsetWrite describes a 32-bit address, within the volume, at which
   128  	// the offset of current structure will be written. The position may be
   129  	// specified as a byte offset relative to the start of a named structure
   130  	OffsetWrite *RelativeOffset `yaml:"offset-write"`
   131  	// Size of the structure
   132  	Size quantity.Size `yaml:"size"`
   133  	// Type of the structure, which can be 2-hex digit MBR partition,
   134  	// 36-char GUID partition, comma separated <mbr>,<guid> for hybrid
   135  	// partitioning schemes, or 'bare' when the structure is not considered
   136  	// a partition.
   137  	//
   138  	// For backwards compatibility type 'mbr' is also accepted, and the
   139  	// structure is treated as if it is of role 'mbr'.
   140  	Type string `yaml:"type"`
   141  	// Role describes the role of given structure, can be one of
   142  	// 'mbr', 'system-data', 'system-boot', 'system-boot-image',
   143  	// 'system-boot-select' or 'system-recovery-select'. Structures of type 'mbr', must have a
   144  	// size of 446 bytes and must start at 0 offset.
   145  	Role string `yaml:"role"`
   146  	// ID is the GPT partition ID
   147  	ID string `yaml:"id"`
   148  	// Filesystem used for the partition, 'vfat', 'ext4' or 'none' for
   149  	// structures of type 'bare'
   150  	Filesystem string `yaml:"filesystem"`
   151  	// Content of the structure
   152  	Content []VolumeContent `yaml:"content"`
   153  	Update  VolumeUpdate    `yaml:"update"`
   154  }
   155  
   156  // HasFilesystem returns true if the structure is using a filesystem.
   157  func (vs *VolumeStructure) HasFilesystem() bool {
   158  	return vs.Filesystem != "none" && vs.Filesystem != ""
   159  }
   160  
   161  // IsPartition returns true when the structure describes a partition in a block
   162  // device.
   163  func (vs *VolumeStructure) IsPartition() bool {
   164  	return vs.Type != "bare" && vs.Role != schemaMBR
   165  }
   166  
   167  // VolumeContent defines the contents of the structure. The content can be
   168  // either files within a filesystem described by the structure or raw images
   169  // written into the area of a bare structure.
   170  type VolumeContent struct {
   171  	// UnresovedSource is the data of the partition relative to
   172  	// the gadget base directory
   173  	UnresolvedSource string `yaml:"source"`
   174  	// Target is the location of the data inside the root filesystem
   175  	Target string `yaml:"target"`
   176  
   177  	// Image names the image, relative to gadget base directory, to be used
   178  	// for a 'bare' type structure
   179  	Image string `yaml:"image"`
   180  	// Offset the image is written at
   181  	Offset *quantity.Offset `yaml:"offset"`
   182  	// OffsetWrite describes a 32-bit address, within the volume, at which
   183  	// the offset of current image will be written. The position may be
   184  	// specified as a byte offset relative to the start of a named structure
   185  	OffsetWrite *RelativeOffset `yaml:"offset-write"`
   186  	// Size of the image, when empty size is calculated by looking at the
   187  	// image
   188  	Size quantity.Size `yaml:"size"`
   189  
   190  	Unpack bool `yaml:"unpack"`
   191  }
   192  
   193  func (vc VolumeContent) String() string {
   194  	if vc.Image != "" {
   195  		return fmt.Sprintf("image:%s", vc.Image)
   196  	}
   197  	return fmt.Sprintf("source:%s", vc.UnresolvedSource)
   198  }
   199  
   200  type VolumeUpdate struct {
   201  	Edition  edition.Number `yaml:"edition"`
   202  	Preserve []string       `yaml:"preserve"`
   203  }
   204  
   205  // GadgetConnect describes an interface connection requested by the gadget
   206  // between seeded snaps. The syntax is of a mapping like:
   207  //
   208  //  plug: (<plug-snap-id>|system):plug
   209  //  [slot: (<slot-snap-id>|system):slot]
   210  //
   211  // "system" indicates a system plug or slot.
   212  // Fully omitting the slot part indicates a system slot with the same name
   213  // as the plug.
   214  type Connection struct {
   215  	Plug ConnectionPlug `yaml:"plug"`
   216  	Slot ConnectionSlot `yaml:"slot"`
   217  }
   218  
   219  type ConnectionPlug struct {
   220  	SnapID string
   221  	Plug   string
   222  }
   223  
   224  func (gcplug *ConnectionPlug) Empty() bool {
   225  	return gcplug.SnapID == "" && gcplug.Plug == ""
   226  }
   227  
   228  func (gcplug *ConnectionPlug) UnmarshalYAML(unmarshal func(interface{}) error) error {
   229  	var s string
   230  	if err := unmarshal(&s); err != nil {
   231  		return err
   232  	}
   233  	snapID, name, err := parseSnapIDColonName(s)
   234  	if err != nil {
   235  		return fmt.Errorf("in gadget connection plug: %v", err)
   236  	}
   237  	gcplug.SnapID = snapID
   238  	gcplug.Plug = name
   239  	return nil
   240  }
   241  
   242  type ConnectionSlot struct {
   243  	SnapID string
   244  	Slot   string
   245  }
   246  
   247  func (gcslot *ConnectionSlot) Empty() bool {
   248  	return gcslot.SnapID == "" && gcslot.Slot == ""
   249  }
   250  
   251  func (gcslot *ConnectionSlot) UnmarshalYAML(unmarshal func(interface{}) error) error {
   252  	var s string
   253  	if err := unmarshal(&s); err != nil {
   254  		return err
   255  	}
   256  	snapID, name, err := parseSnapIDColonName(s)
   257  	if err != nil {
   258  		return fmt.Errorf("in gadget connection slot: %v", err)
   259  	}
   260  	gcslot.SnapID = snapID
   261  	gcslot.Slot = name
   262  	return nil
   263  }
   264  
   265  func parseSnapIDColonName(s string) (snapID, name string, err error) {
   266  	parts := strings.Split(s, ":")
   267  	if len(parts) == 2 {
   268  		snapID = parts[0]
   269  		name = parts[1]
   270  	}
   271  	if snapID == "" || name == "" {
   272  		return "", "", fmt.Errorf(`expected "(<snap-id>|system):name" not %q`, s)
   273  	}
   274  	return snapID, name, nil
   275  }
   276  
   277  func systemOrSnapID(s string) bool {
   278  	if s != "system" && naming.ValidateSnapID(s) != nil {
   279  		return false
   280  	}
   281  	return true
   282  }
   283  
   284  // Model carries characteristics about the model that are relevant to gadget.
   285  // Note *asserts.Model implements this, and that's the expected use case.
   286  type Model interface {
   287  	Classic() bool
   288  	Grade() asserts.ModelGrade
   289  }
   290  
   291  func classicOrUndetermined(m Model) bool {
   292  	return m == nil || m.Classic()
   293  }
   294  
   295  func wantsSystemSeed(m Model) bool {
   296  	return m != nil && m.Grade() != asserts.ModelGradeUnset
   297  }
   298  
   299  // InfoFromGadgetYaml parses the provided gadget metadata.
   300  // If model is nil only self-consistency checks are performed.
   301  // If model is not nil implied values for filesystem labels will be set
   302  // as well, based on whether the model is for classic, UC16/18 or UC20.
   303  // UC gadget metadata is expected to have volumes definitions.
   304  func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) {
   305  	var gi Info
   306  
   307  	if err := yaml.Unmarshal(gadgetYaml, &gi); err != nil {
   308  		return nil, fmt.Errorf("cannot parse gadget metadata: %v", err)
   309  	}
   310  
   311  	for k, v := range gi.Defaults {
   312  		if !systemOrSnapID(k) {
   313  			return nil, fmt.Errorf(`default stanza not keyed by "system" or snap-id: %s`, k)
   314  		}
   315  		dflt, err := metautil.NormalizeValue(v)
   316  		if err != nil {
   317  			return nil, fmt.Errorf("default value %q of %q: %v", v, k, err)
   318  		}
   319  		gi.Defaults[k] = dflt.(map[string]interface{})
   320  	}
   321  
   322  	for i, gconn := range gi.Connections {
   323  		if gconn.Plug.Empty() {
   324  			return nil, errors.New("gadget connection plug cannot be empty")
   325  		}
   326  		if gconn.Slot.Empty() {
   327  			gi.Connections[i].Slot.SnapID = "system"
   328  			gi.Connections[i].Slot.Slot = gconn.Plug.Plug
   329  		}
   330  	}
   331  
   332  	if len(gi.Volumes) == 0 && classicOrUndetermined(model) {
   333  		// volumes can be left out on classic
   334  		// can still specify defaults though
   335  		return &gi, nil
   336  	}
   337  
   338  	// basic validation
   339  	var bootloadersFound int
   340  	knownFsLabelsPerVolume := make(map[string]map[string]bool, len(gi.Volumes))
   341  	for name, v := range gi.Volumes {
   342  		if err := validateVolume(name, v, model, knownFsLabelsPerVolume); err != nil {
   343  			return nil, fmt.Errorf("invalid volume %q: %v", name, err)
   344  		}
   345  
   346  		switch v.Bootloader {
   347  		case "":
   348  			// pass
   349  		case "grub", "u-boot", "android-boot", "lk":
   350  			bootloadersFound += 1
   351  		default:
   352  			return nil, errors.New("bootloader must be one of grub, u-boot, android-boot or lk")
   353  		}
   354  	}
   355  	switch {
   356  	case bootloadersFound == 0:
   357  		return nil, errors.New("bootloader not declared in any volume")
   358  	case bootloadersFound > 1:
   359  		return nil, fmt.Errorf("too many (%d) bootloaders declared", bootloadersFound)
   360  	}
   361  
   362  	for name, v := range gi.Volumes {
   363  		if err := setImplicitForVolume(name, v, model, knownFsLabelsPerVolume[name]); err != nil {
   364  			return nil, fmt.Errorf("invalid volume %q: %v", name, err)
   365  		}
   366  	}
   367  
   368  	return &gi, nil
   369  }
   370  
   371  type volRuleset int
   372  
   373  const (
   374  	volRulesetUnknown volRuleset = iota
   375  	volRuleset16
   376  	volRuleset20
   377  )
   378  
   379  func whichVolRuleset(model Model) volRuleset {
   380  	if model == nil {
   381  		return volRulesetUnknown
   382  	}
   383  	if model.Grade() != asserts.ModelGradeUnset {
   384  		return volRuleset20
   385  	}
   386  	return volRuleset16
   387  }
   388  
   389  func setImplicitForVolume(name string, vol *Volume, model Model, knownFsLabels map[string]bool) error {
   390  	rs := whichVolRuleset(model)
   391  	if vol.Schema == "" {
   392  		// default for schema is gpt
   393  		vol.Schema = schemaGPT
   394  	}
   395  	for i := range vol.Structure {
   396  		if err := setImplicitForVolumeStructure(&vol.Structure[i], rs, knownFsLabels); err != nil {
   397  			return err
   398  		}
   399  	}
   400  	return nil
   401  }
   402  
   403  func setImplicitForVolumeStructure(vs *VolumeStructure, rs volRuleset, knownFsLabels map[string]bool) error {
   404  	if vs.Role == "" && vs.Type == schemaMBR {
   405  		vs.Role = schemaMBR
   406  		return nil
   407  	}
   408  	if rs == volRuleset16 && vs.Role == "" && vs.Label == SystemBoot {
   409  		// legacy behavior, for gadgets that only specify a filesystem-label, eg. pc
   410  		vs.Role = SystemBoot
   411  		return nil
   412  	}
   413  	if vs.Label == "" {
   414  		var implicitLabel string
   415  		switch {
   416  		case rs == volRuleset16 && vs.Role == SystemData:
   417  			implicitLabel = implicitSystemDataLabel
   418  		case rs == volRuleset20 && vs.Role == SystemData:
   419  			implicitLabel = ubuntuDataLabel
   420  		case rs == volRuleset20 && vs.Role == SystemSeed:
   421  			implicitLabel = ubuntuSeedLabel
   422  		case rs == volRuleset20 && vs.Role == SystemBoot:
   423  			implicitLabel = ubuntuBootLabel
   424  		case rs == volRuleset20 && vs.Role == SystemSave:
   425  			implicitLabel = ubuntuSaveLabel
   426  		}
   427  		if implicitLabel != "" {
   428  			if knownFsLabels[implicitLabel] {
   429  				return fmt.Errorf("filesystem label %q is implied by %s role but was already set elsewhere", implicitLabel, vs.Role)
   430  			}
   431  			knownFsLabels[implicitLabel] = true
   432  			vs.Label = implicitLabel
   433  		}
   434  	}
   435  	return nil
   436  }
   437  
   438  func readInfo(f func(string) ([]byte, error), gadgetYamlFn string, model Model) (*Info, error) {
   439  	gmeta, err := f(gadgetYamlFn)
   440  	if classicOrUndetermined(model) && os.IsNotExist(err) {
   441  		// gadget.yaml is optional for classic gadgets
   442  		return &Info{}, nil
   443  	}
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  
   448  	return InfoFromGadgetYaml(gmeta, model)
   449  }
   450  
   451  // ReadInfo reads the gadget specific metadata from meta/gadget.yaml in the snap
   452  // root directory.
   453  // See ReadInfoAndValidate for a variant that does role-usage consistency
   454  // validation like Validate.
   455  func ReadInfo(gadgetSnapRootDir string, model Model) (*Info, error) {
   456  	gadgetYamlFn := filepath.Join(gadgetSnapRootDir, "meta", "gadget.yaml")
   457  	ginfo, err := readInfo(ioutil.ReadFile, gadgetYamlFn, model)
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  	return ginfo, nil
   462  }
   463  
   464  // ReadInfoAndValidate reads the gadget specific metadata from
   465  // meta/gadget.yaml in the snap root directory.
   466  // It also performs role-usage consistency validation as Validate does
   467  // using the given constraints. See ReadInfo for a variant that does not.
   468  // See also ValidateContent for further validating the content itself
   469  // instead of the metadata.
   470  func ReadInfoAndValidate(gadgetSnapRootDir string, model Model, validationConstraints *ValidationConstraints) (*Info, error) {
   471  	ginfo, err := ReadInfo(gadgetSnapRootDir, model)
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  	if err := Validate(ginfo, model, validationConstraints); err != nil {
   476  		return nil, err
   477  	}
   478  	return ginfo, err
   479  }
   480  
   481  // ReadInfoFromSnapFile reads the gadget specific metadata from
   482  // meta/gadget.yaml in the given snap container.
   483  // It also performs role-usage consistency validation as Validate does.
   484  // See ReadInfoFromSnapFileNoValidate for a variant that does not.
   485  func ReadInfoFromSnapFile(snapf snap.Container, model Model) (*Info, error) {
   486  	ginfo, err := ReadInfoFromSnapFileNoValidate(snapf, model)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  	if err := Validate(ginfo, model, nil); err != nil {
   491  		return nil, err
   492  	}
   493  	return ginfo, nil
   494  }
   495  
   496  // ReadInfoFromSnapFileNoValidate reads the gadget specific metadata from
   497  // meta/gadget.yaml in the given snap container.
   498  // See ReadInfoFromSnapFile for a variant that does role-usage consistency
   499  // validation like Validate as well.
   500  func ReadInfoFromSnapFileNoValidate(snapf snap.Container, model Model) (*Info, error) {
   501  	gadgetYamlFn := "meta/gadget.yaml"
   502  	ginfo, err := readInfo(snapf.ReadFile, gadgetYamlFn, model)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  	return ginfo, nil
   507  }
   508  
   509  func fmtIndexAndName(idx int, name string) string {
   510  	if name != "" {
   511  		return fmt.Sprintf("#%v (%q)", idx, name)
   512  	}
   513  	return fmt.Sprintf("#%v", idx)
   514  }
   515  
   516  func validateVolume(name string, vol *Volume, model Model, knownFsLabelsPerVolume map[string]map[string]bool) error {
   517  	if !validVolumeName.MatchString(name) {
   518  		return errors.New("invalid name")
   519  	}
   520  	if vol.Schema != "" && vol.Schema != schemaGPT && vol.Schema != schemaMBR {
   521  		return fmt.Errorf("invalid schema %q", vol.Schema)
   522  	}
   523  
   524  	// named structures, for cross-referencing relative offset-write names
   525  	knownStructures := make(map[string]*LaidOutStructure, len(vol.Structure))
   526  	// for uniqueness of filesystem labels
   527  	knownFsLabels := make(map[string]bool, len(vol.Structure))
   528  	// for validating structure overlap
   529  	structures := make([]LaidOutStructure, len(vol.Structure))
   530  
   531  	if knownFsLabelsPerVolume != nil {
   532  		knownFsLabelsPerVolume[name] = knownFsLabels
   533  	}
   534  
   535  	previousEnd := quantity.Offset(0)
   536  	// TODO: should we also validate that if there is a system-recovery-select
   537  	// role there should also be at least 2 system-recovery-image roles and
   538  	// same for system-boot-select and at least 2 system-boot-image roles?
   539  	for idx, s := range vol.Structure {
   540  		if err := validateVolumeStructure(&s, vol); err != nil {
   541  			return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err)
   542  		}
   543  		var start quantity.Offset
   544  		if s.Offset != nil {
   545  			start = *s.Offset
   546  		} else {
   547  			start = previousEnd
   548  		}
   549  		end := start + quantity.Offset(s.Size)
   550  		ps := LaidOutStructure{
   551  			VolumeStructure: &vol.Structure[idx],
   552  			StartOffset:     start,
   553  			Index:           idx,
   554  		}
   555  		structures[idx] = ps
   556  		if s.Name != "" {
   557  			if _, ok := knownStructures[s.Name]; ok {
   558  				return fmt.Errorf("structure name %q is not unique", s.Name)
   559  			}
   560  			// keep track of named structures
   561  			knownStructures[s.Name] = &ps
   562  		}
   563  		if s.Label != "" {
   564  			if seen := knownFsLabels[s.Label]; seen {
   565  				return fmt.Errorf("filesystem label %q is not unique", s.Label)
   566  			}
   567  			knownFsLabels[s.Label] = true
   568  		}
   569  
   570  		previousEnd = end
   571  	}
   572  
   573  	// sort by starting offset
   574  	sort.Sort(byStartOffset(structures))
   575  
   576  	return validateCrossVolumeStructure(structures, knownStructures)
   577  }
   578  
   579  // isMBR returns whether the structure is the MBR and can be used before setImplicitForVolume
   580  func isMBR(vs *VolumeStructure) bool {
   581  	if vs.Role == schemaMBR {
   582  		return true
   583  	}
   584  	if vs.Role == "" && vs.Type == schemaMBR {
   585  		return true
   586  	}
   587  	return false
   588  }
   589  
   590  func validateCrossVolumeStructure(structures []LaidOutStructure, knownStructures map[string]*LaidOutStructure) error {
   591  	previousEnd := quantity.Offset(0)
   592  	// cross structure validation:
   593  	// - relative offsets that reference other structures by name
   594  	// - laid out structure overlap
   595  	// use structures laid out within the volume
   596  	for pidx, ps := range structures {
   597  		if isMBR(ps.VolumeStructure) {
   598  			if ps.StartOffset != 0 {
   599  				return fmt.Errorf(`structure %v has "mbr" role and must start at offset 0`, ps)
   600  			}
   601  		}
   602  		if ps.OffsetWrite != nil && ps.OffsetWrite.RelativeTo != "" {
   603  			// offset-write using a named structure
   604  			other := knownStructures[ps.OffsetWrite.RelativeTo]
   605  			if other == nil {
   606  				return fmt.Errorf("structure %v refers to an unknown structure %q",
   607  					ps, ps.OffsetWrite.RelativeTo)
   608  			}
   609  		}
   610  
   611  		if ps.StartOffset < previousEnd {
   612  			previous := structures[pidx-1]
   613  			return fmt.Errorf("structure %v overlaps with the preceding structure %v", ps, previous)
   614  		}
   615  		previousEnd = ps.StartOffset + quantity.Offset(ps.Size)
   616  
   617  		if ps.HasFilesystem() {
   618  			// content relative offset only possible if it's a bare structure
   619  			continue
   620  		}
   621  		for cidx, c := range ps.Content {
   622  			if c.OffsetWrite == nil || c.OffsetWrite.RelativeTo == "" {
   623  				continue
   624  			}
   625  			relativeToStructure := knownStructures[c.OffsetWrite.RelativeTo]
   626  			if relativeToStructure == nil {
   627  				return fmt.Errorf("structure %v, content %v refers to an unknown structure %q",
   628  					ps, fmtIndexAndName(cidx, c.Image), c.OffsetWrite.RelativeTo)
   629  			}
   630  		}
   631  	}
   632  	return nil
   633  }
   634  
   635  func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error {
   636  	if vs.Size == 0 {
   637  		return errors.New("missing size")
   638  	}
   639  	if err := validateStructureType(vs.Type, vol); err != nil {
   640  		return fmt.Errorf("invalid type %q: %v", vs.Type, err)
   641  	}
   642  	if err := validateRole(vs, vol); err != nil {
   643  		var what string
   644  		if vs.Role != "" {
   645  			what = fmt.Sprintf("role %q", vs.Role)
   646  		} else {
   647  			what = fmt.Sprintf("implicit role %q", vs.Type)
   648  		}
   649  		return fmt.Errorf("invalid %s: %v", what, err)
   650  	}
   651  	if vs.Filesystem != "" && !strutil.ListContains([]string{"ext4", "vfat", "none"}, vs.Filesystem) {
   652  		return fmt.Errorf("invalid filesystem %q", vs.Filesystem)
   653  	}
   654  
   655  	var contentChecker func(*VolumeContent) error
   656  
   657  	if !vs.HasFilesystem() {
   658  		contentChecker = validateBareContent
   659  	} else {
   660  		contentChecker = validateFilesystemContent
   661  	}
   662  	for i, c := range vs.Content {
   663  		if err := contentChecker(&c); err != nil {
   664  			return fmt.Errorf("invalid content #%v: %v", i, err)
   665  		}
   666  	}
   667  
   668  	if err := validateStructureUpdate(&vs.Update, vs); err != nil {
   669  		return err
   670  	}
   671  
   672  	// TODO: validate structure size against sector-size; ubuntu-image uses
   673  	// a tmp file to find out the default sector size of the device the tmp
   674  	// file is created on
   675  	return nil
   676  }
   677  
   678  func validateStructureType(s string, vol *Volume) error {
   679  	// Type can be one of:
   680  	// - "mbr" (backwards compatible)
   681  	// - "bare"
   682  	// - [0-9A-Z]{2} - MBR type
   683  	// - GPT UUID
   684  	// - hybrid ID
   685  	//
   686  	// Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID
   687  	// example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   688  
   689  	schema := vol.Schema
   690  	if schema == "" {
   691  		schema = schemaGPT
   692  	}
   693  
   694  	if s == "" {
   695  		return errors.New(`type is not specified`)
   696  	}
   697  
   698  	if s == "bare" {
   699  		// unknonwn blob
   700  		return nil
   701  	}
   702  
   703  	if s == schemaMBR {
   704  		// backward compatibility for type: mbr
   705  		return nil
   706  	}
   707  
   708  	var isGPT, isMBR bool
   709  
   710  	idx := strings.IndexRune(s, ',')
   711  	if idx == -1 {
   712  		// just ID
   713  		switch {
   714  		case validTypeID.MatchString(s):
   715  			isMBR = true
   716  		case validGUUID.MatchString(s):
   717  			isGPT = true
   718  		default:
   719  			return fmt.Errorf("invalid format")
   720  		}
   721  	} else {
   722  		// hybrid ID
   723  		code := s[:idx]
   724  		guid := s[idx+1:]
   725  		if len(code) != 2 || len(guid) != 36 || !validTypeID.MatchString(code) || !validGUUID.MatchString(guid) {
   726  			return fmt.Errorf("invalid format of hybrid type")
   727  		}
   728  	}
   729  
   730  	if schema != schemaGPT && isGPT {
   731  		// type: <uuid> is only valid for GPT volumes
   732  		return fmt.Errorf("GUID structure type with non-GPT schema %q", vol.Schema)
   733  	}
   734  	if schema != schemaMBR && isMBR {
   735  		return fmt.Errorf("MBR structure type with non-MBR schema %q", vol.Schema)
   736  	}
   737  
   738  	return nil
   739  }
   740  
   741  func validateRole(vs *VolumeStructure, vol *Volume) error {
   742  	if vs.Type == "bare" {
   743  		if vs.Role != "" && vs.Role != schemaMBR {
   744  			return fmt.Errorf("conflicting type: %q", vs.Type)
   745  		}
   746  	}
   747  	vsRole := vs.Role
   748  	if vs.Type == schemaMBR {
   749  		if vsRole != "" && vsRole != schemaMBR {
   750  			return fmt.Errorf(`conflicting legacy type: "mbr"`)
   751  		}
   752  		// backward compatibility
   753  		vsRole = schemaMBR
   754  	}
   755  
   756  	switch vsRole {
   757  	case SystemData, SystemSeed, SystemSave:
   758  		// roles have cross dependencies, consistency checks are done at
   759  		// the volume level
   760  	case schemaMBR:
   761  		if vs.Size > SizeMBR {
   762  			return errors.New("mbr structures cannot be larger than 446 bytes")
   763  		}
   764  		if vs.Offset != nil && *vs.Offset != 0 {
   765  			return errors.New("mbr structure must start at offset 0")
   766  		}
   767  		if vs.ID != "" {
   768  			return errors.New("mbr structure must not specify partition ID")
   769  		}
   770  		if vs.Filesystem != "" && vs.Filesystem != "none" {
   771  			return errors.New("mbr structures must not specify a file system")
   772  		}
   773  	case SystemBoot, bootImage, bootSelect, seedBootSelect, seedBootImage, "":
   774  		// noop
   775  	case legacyBootImage, legacyBootSelect:
   776  		// noop
   777  		// legacy role names were added in 2.42 can be removed
   778  		// on snapd epoch bump
   779  	default:
   780  		return fmt.Errorf("unsupported role")
   781  	}
   782  	return nil
   783  }
   784  
   785  func validateBareContent(vc *VolumeContent) error {
   786  	if vc.UnresolvedSource != "" || vc.Target != "" {
   787  		return fmt.Errorf("cannot use non-image content for bare file system")
   788  	}
   789  	if vc.Image == "" {
   790  		return fmt.Errorf("missing image file name")
   791  	}
   792  	return nil
   793  }
   794  
   795  func validateFilesystemContent(vc *VolumeContent) error {
   796  	if vc.Image != "" || vc.Offset != nil || vc.OffsetWrite != nil || vc.Size != 0 {
   797  		return fmt.Errorf("cannot use image content for non-bare file system")
   798  	}
   799  	if vc.UnresolvedSource == "" {
   800  		return fmt.Errorf("missing source")
   801  	}
   802  	if vc.Target == "" {
   803  		return fmt.Errorf("missing target")
   804  	}
   805  	return nil
   806  }
   807  
   808  func validateStructureUpdate(up *VolumeUpdate, vs *VolumeStructure) error {
   809  	if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 {
   810  		return errors.New("preserving files during update is not supported for non-filesystem structures")
   811  	}
   812  
   813  	names := make(map[string]bool, len(vs.Update.Preserve))
   814  	for _, n := range vs.Update.Preserve {
   815  		if names[n] {
   816  			return fmt.Errorf(`duplicate "preserve" entry %q`, n)
   817  		}
   818  		names[n] = true
   819  	}
   820  	return nil
   821  }
   822  
   823  const (
   824  	// SizeMBR is the maximum byte size of a structure of role 'mbr'
   825  	SizeMBR = quantity.Size(446)
   826  	// SizeLBA48Pointer is the byte size of a pointer value written at the
   827  	// location described by 'offset-write'
   828  	SizeLBA48Pointer = quantity.Size(4)
   829  )
   830  
   831  // RelativeOffset describes an offset where structure data is written at.
   832  // The position can be specified as byte-offset relative to the start of another
   833  // named structure.
   834  type RelativeOffset struct {
   835  	// RelativeTo names the structure relative to which the location of the
   836  	// address write will be calculated.
   837  	RelativeTo string
   838  	// Offset is a 32-bit value
   839  	Offset quantity.Offset
   840  }
   841  
   842  func (r *RelativeOffset) String() string {
   843  	if r == nil {
   844  		return "unspecified"
   845  	}
   846  	if r.RelativeTo != "" {
   847  		return fmt.Sprintf("%s+%d", r.RelativeTo, r.Offset)
   848  	}
   849  	return fmt.Sprintf("%d", r.Offset)
   850  }
   851  
   852  // parseRelativeOffset parses a string describing an offset that can be
   853  // expressed relative to a named structure, with the format: [<name>+]<offset>.
   854  func parseRelativeOffset(grs string) (*RelativeOffset, error) {
   855  	toWhat := ""
   856  	offsSpec := grs
   857  	if idx := strings.IndexRune(grs, '+'); idx != -1 {
   858  		toWhat, offsSpec = grs[:idx], grs[idx+1:]
   859  		if toWhat == "" {
   860  			return nil, errors.New("missing volume name")
   861  		}
   862  	}
   863  	if offsSpec == "" {
   864  		return nil, errors.New("missing offset")
   865  	}
   866  
   867  	offset, err := quantity.ParseOffset(offsSpec)
   868  	if err != nil {
   869  		return nil, fmt.Errorf("cannot parse offset %q: %v", offsSpec, err)
   870  	}
   871  	if offset > 4*1024*quantity.OffsetMiB {
   872  		return nil, fmt.Errorf("offset above 4G limit")
   873  	}
   874  
   875  	return &RelativeOffset{
   876  		RelativeTo: toWhat,
   877  		Offset:     offset,
   878  	}, nil
   879  }
   880  
   881  func (s *RelativeOffset) UnmarshalYAML(unmarshal func(interface{}) error) error {
   882  	var grs string
   883  	if err := unmarshal(&grs); err != nil {
   884  		return errors.New(`cannot unmarshal gadget relative offset`)
   885  	}
   886  
   887  	ro, err := parseRelativeOffset(grs)
   888  	if err != nil {
   889  		return fmt.Errorf("cannot parse relative offset %q: %v", grs, err)
   890  	}
   891  	*s = *ro
   892  	return nil
   893  }
   894  
   895  // IsCompatible checks whether the current and an update are compatible. Returns
   896  // nil or an error describing the incompatibility.
   897  func IsCompatible(current, new *Info) error {
   898  	// XXX: the only compatibility we have now is making sure that the new
   899  	// layout can be used on an existing volume
   900  	if len(new.Volumes) > 1 {
   901  		return fmt.Errorf("gadgets with multiple volumes are unsupported")
   902  	}
   903  
   904  	// XXX: the code below errors out with more than 1 volume in the current
   905  	// gadget, we allow this scenario in update but better bail out here and
   906  	// have users fix their gadgets
   907  	currentVol, newVol, err := resolveVolume(current, new)
   908  	if err != nil {
   909  		return err
   910  	}
   911  
   912  	if currentVol.Schema == "" || newVol.Schema == "" {
   913  		return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", currentVol.Schema, newVol.Schema)
   914  	}
   915  
   916  	// layout both volumes partially, without going deep into the layout of
   917  	// structure content, we only want to make sure that structures are
   918  	// comapatible
   919  	pCurrent, err := LayoutVolumePartially(currentVol, DefaultConstraints)
   920  	if err != nil {
   921  		return fmt.Errorf("cannot lay out the current volume: %v", err)
   922  	}
   923  	pNew, err := LayoutVolumePartially(newVol, DefaultConstraints)
   924  	if err != nil {
   925  		return fmt.Errorf("cannot lay out the new volume: %v", err)
   926  	}
   927  	if err := isLayoutCompatible(pCurrent, pNew); err != nil {
   928  		return fmt.Errorf("incompatible layout change: %v", err)
   929  	}
   930  	return nil
   931  }
   932  
   933  // LaidOutSystemVolumeFromGadget takes a gadget rootdir and lays out the
   934  // partitions as specified. It returns one specific volume, which is the volume
   935  // on which system-* roles/partitions exist, all other volumes are assumed to
   936  // already be flashed and managed separately at image build/flash time, while
   937  // the system-* roles can be manipulated on the returned volume during install
   938  // mode.
   939  func LaidOutSystemVolumeFromGadget(gadgetRoot, kernelRoot string, model Model) (*LaidOutVolume, error) {
   940  	// model should never be nil here
   941  	if model == nil {
   942  		return nil, fmt.Errorf("internal error: must have model to lay out system volumes from a gadget")
   943  	}
   944  	// rely on the basic validation from ReadInfo to ensure that the system-*
   945  	// roles are all on the same volume for example
   946  	info, err := ReadInfoAndValidate(gadgetRoot, model, nil)
   947  	if err != nil {
   948  		return nil, err
   949  	}
   950  
   951  	constraints := LayoutConstraints{
   952  		NonMBRStartOffset: 1 * quantity.OffsetMiB,
   953  	}
   954  
   955  	// find the volume with the system-boot role on it, we already validated
   956  	// that the system-* roles are all on the same volume
   957  	for _, vol := range info.Volumes {
   958  		for _, structure := range vol.Structure {
   959  			// use the system-boot role to identify the system volume
   960  			if structure.Role == SystemBoot {
   961  				pvol, err := LayoutVolume(gadgetRoot, kernelRoot, vol, constraints)
   962  				if err != nil {
   963  					return nil, err
   964  				}
   965  
   966  				return pvol, nil
   967  			}
   968  		}
   969  	}
   970  
   971  	// this should be impossible, the validation above should ensure this
   972  	return nil, fmt.Errorf("internal error: gadget passed validation but does not have system-* roles on any volume")
   973  }
   974  
   975  func flatten(path string, cfg interface{}, out map[string]interface{}) {
   976  	if cfgMap, ok := cfg.(map[string]interface{}); ok {
   977  		for k, v := range cfgMap {
   978  			p := k
   979  			if path != "" {
   980  				p = path + "." + k
   981  			}
   982  			flatten(p, v, out)
   983  		}
   984  	} else {
   985  		out[path] = cfg
   986  	}
   987  }
   988  
   989  // SystemDefaults returns default system configuration from gadget defaults.
   990  func SystemDefaults(gadgetDefaults map[string]map[string]interface{}) map[string]interface{} {
   991  	for _, systemSnap := range []string{"system", naming.WellKnownSnapID("core")} {
   992  		if defaults, ok := gadgetDefaults[systemSnap]; ok {
   993  			coreDefaults := map[string]interface{}{}
   994  			flatten("", defaults, coreDefaults)
   995  			return coreDefaults
   996  		}
   997  	}
   998  	return nil
   999  }
  1000  
  1001  // See https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html
  1002  var disallowedKernelArguments = []string{
  1003  	"root", "nfsroot",
  1004  	"init",
  1005  }
  1006  
  1007  // isKernelArgumentAllowed checks whether the kernel command line argument is
  1008  // allowed. Prohibits all arguments listed explicitly in
  1009  // disallowedKernelArguments list and those prefixed with snapd, with exception
  1010  // of snapd.debug. All other arguments are allowed.
  1011  func isKernelArgumentAllowed(arg string) bool {
  1012  	if strings.HasPrefix(arg, "snapd") && arg != "snapd.debug" {
  1013  		return false
  1014  	}
  1015  	if strutil.ListContains(disallowedKernelArguments, arg) {
  1016  		return false
  1017  	}
  1018  	return true
  1019  }
  1020  
  1021  var ErrNoKernelCommandline = errors.New("no kernel command line in the gadget")
  1022  
  1023  // KernelCommandLineFromGadget returns the desired kernel command line provided by the
  1024  // gadget. The full flag indicates whether the gadget provides a full command
  1025  // line or just the extra parameters that will be appended to the static ones.
  1026  // An ErrNoKernelCommandline is returned when thea gadget does not set any
  1027  // kernel command line.
  1028  func KernelCommandLineFromGadget(gadgetDirOrSnapPath string) (cmdline string, full bool, err error) {
  1029  	sf, err := snapfile.Open(gadgetDirOrSnapPath)
  1030  	if err != nil {
  1031  		return "", false, fmt.Errorf("cannot open gadget snap: %v", err)
  1032  	}
  1033  	contentExtra, err := sf.ReadFile("cmdline.extra")
  1034  	if err != nil && !os.IsNotExist(err) {
  1035  		return "", false, err
  1036  	}
  1037  	contentFull, err := sf.ReadFile("cmdline.full")
  1038  	if err != nil && !os.IsNotExist(err) {
  1039  		return "", false, err
  1040  	}
  1041  	content := contentExtra
  1042  	whichFile := "cmdline.extra"
  1043  	switch {
  1044  	case contentExtra != nil && contentFull != nil:
  1045  		return "", false, fmt.Errorf("cannot support both extra and full kernel command lines")
  1046  	case contentExtra == nil && contentFull == nil:
  1047  		return "", false, ErrNoKernelCommandline
  1048  	case contentFull != nil:
  1049  		content = contentFull
  1050  		whichFile = "cmdline.full"
  1051  		full = true
  1052  	}
  1053  	parsed, err := parseCommandLineFromGadget(content, whichFile)
  1054  	if err != nil {
  1055  		return "", full, fmt.Errorf("invalid kernel command line in %v: %v", whichFile, err)
  1056  	}
  1057  	return parsed, full, nil
  1058  }
  1059  
  1060  // parseCommandLineFromGadget parses the command line file and returns a
  1061  // reassembled kernel command line as a single string. The file can be multi
  1062  // line, where only lines stating with # are treated as comments, eg.
  1063  //
  1064  // foo
  1065  // # this is a comment
  1066  //
  1067  // According to https://elixir.bootlin.com/linux/latest/source/Documentation/admin-guide/kernel-parameters.txt
  1068  // the # character can appear as part of a valid kernel command line argument,
  1069  // specifically in the following argument:
  1070  //   memmap=nn[KMG]#ss[KMG]
  1071  //   memmap=100M@2G,100M#3G,1G!1024G
  1072  // Thus a lone # or a token starting with # are treated as errors.
  1073  func parseCommandLineFromGadget(content []byte, whichFile string) (string, error) {
  1074  	s := bufio.NewScanner(bytes.NewBuffer(content))
  1075  	filtered := &bytes.Buffer{}
  1076  	for s.Scan() {
  1077  		line := s.Text()
  1078  		if len(line) > 0 && line[0] == '#' {
  1079  			// comment
  1080  			continue
  1081  		}
  1082  		filtered.WriteRune(' ')
  1083  		filtered.WriteString(line)
  1084  	}
  1085  	if err := s.Err(); err != nil {
  1086  		return "", err
  1087  	}
  1088  	kargs, err := osutil.KernelCommandLineSplit(filtered.String())
  1089  	if err != nil {
  1090  		return "", err
  1091  	}
  1092  	for _, argValue := range kargs {
  1093  		if strings.HasPrefix(argValue, "#") {
  1094  			return "", fmt.Errorf("unexpected or invalid use of # in argument %q", argValue)
  1095  		}
  1096  		split := strings.SplitN(argValue, "=", 2)
  1097  		if !isKernelArgumentAllowed(split[0]) {
  1098  			return "", fmt.Errorf("disallowed kernel argument %q", argValue)
  1099  		}
  1100  	}
  1101  	return strings.Join(kargs, " "), nil
  1102  }