github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/gadget/layout.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/gadget/quantity"
    30  	"github.com/snapcore/snapd/kernel"
    31  )
    32  
    33  // LayoutConstraints defines the constraints for arranging structures within a
    34  // volume
    35  type LayoutConstraints struct {
    36  	// NonMBRStartOffset is the default start offset of non-MBR structure in
    37  	// the volume.
    38  	NonMBRStartOffset quantity.Offset
    39  	// SkipResolveContent will skip resolving content paths
    40  	// and `$kernel:` style references
    41  	SkipResolveContent bool
    42  }
    43  
    44  // LaidOutVolume defines the size of a volume and arrangement of all the
    45  // structures within it
    46  type LaidOutVolume struct {
    47  	*Volume
    48  	// Size is the total size of the volume
    49  	Size quantity.Size
    50  	// LaidOutStructure is a list of structures within the volume, sorted
    51  	// by their start offsets
    52  	LaidOutStructure []LaidOutStructure
    53  	// RootDir is the root directory for volume data
    54  	RootDir string
    55  }
    56  
    57  // PartiallyLaidOutVolume defines the layout of volume structures, but lacks the
    58  // details about the layout of raw image content within the bare structures.
    59  type PartiallyLaidOutVolume struct {
    60  	*Volume
    61  	// LaidOutStructure is a list of structures within the volume, sorted
    62  	// by their start offsets
    63  	LaidOutStructure []LaidOutStructure
    64  }
    65  
    66  // LaidOutStructure describes a VolumeStructure that has been placed within the
    67  // volume
    68  type LaidOutStructure struct {
    69  	*VolumeStructure
    70  	// StartOffset defines the start offset of the structure within the
    71  	// enclosing volume
    72  	StartOffset quantity.Offset
    73  	// AbsoluteOffsetWrite is the resolved absolute position of offset-write
    74  	// for this structure element within the enclosing volume
    75  	AbsoluteOffsetWrite *quantity.Offset
    76  	// Index of the structure definition in gadget YAML
    77  	Index int
    78  	// LaidOutContent is a list of raw content inside the structure
    79  	LaidOutContent []LaidOutContent
    80  	// ResolvedContent is a list of filesystem content that has all
    81  	// relative paths or references resolved
    82  	ResolvedContent []ResolvedContent
    83  }
    84  
    85  // IsRoleMBR returns whether a structure's role is MBR or not.
    86  // meh this function is weirdly placed, not sure what to do w/o making schemaMBR
    87  // constant exported
    88  func IsRoleMBR(ls LaidOutStructure) bool {
    89  	return ls.Role == schemaMBR
    90  }
    91  
    92  func (p LaidOutStructure) String() string {
    93  	return fmtIndexAndName(p.Index, p.Name)
    94  }
    95  
    96  type byStartOffset []LaidOutStructure
    97  
    98  func (b byStartOffset) Len() int           { return len(b) }
    99  func (b byStartOffset) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   100  func (b byStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
   101  
   102  // LaidOutContent describes raw content that has been placed within the
   103  // encompassing structure and volume
   104  //
   105  // TODO: this can't have "$kernel:" refs at this point, fail in validate
   106  //       for bare structures with "$kernel:" refs
   107  type LaidOutContent struct {
   108  	*VolumeContent
   109  
   110  	// StartOffset defines the start offset of this content image
   111  	StartOffset quantity.Offset
   112  	// AbsoluteOffsetWrite is the resolved absolute position of offset-write
   113  	// for this content element within the enclosing volume
   114  	AbsoluteOffsetWrite *quantity.Offset
   115  	// Size is the maximum size occupied by this image
   116  	Size quantity.Size
   117  	// Index of the content in structure declaration inside gadget YAML
   118  	Index int
   119  }
   120  
   121  func (p LaidOutContent) String() string {
   122  	if p.Image != "" {
   123  		return fmt.Sprintf("#%v (%q@%#x{%v})", p.Index, p.Image, p.StartOffset, p.Size)
   124  	}
   125  	return fmt.Sprintf("#%v (source:%q)", p.Index, p.UnresolvedSource)
   126  }
   127  
   128  type ResolvedContent struct {
   129  	*VolumeContent
   130  
   131  	// ResolvedSource is the absolute path of the Source after resolving
   132  	// any references (e.g. to a "$kernel:" snap).
   133  	ResolvedSource string
   134  
   135  	// KernelUpdate is true if this content comes from the kernel
   136  	// and has the "Update" property set
   137  	KernelUpdate bool
   138  }
   139  
   140  func layoutVolumeStructures(volume *Volume, constraints LayoutConstraints) (structures []LaidOutStructure, byName map[string]*LaidOutStructure, err error) {
   141  	previousEnd := quantity.Offset(0)
   142  	structures = make([]LaidOutStructure, len(volume.Structure))
   143  	byName = make(map[string]*LaidOutStructure, len(volume.Structure))
   144  
   145  	for idx, s := range volume.Structure {
   146  		var start quantity.Offset
   147  		if s.Offset == nil {
   148  			if s.Role != schemaMBR && previousEnd < constraints.NonMBRStartOffset {
   149  				start = constraints.NonMBRStartOffset
   150  			} else {
   151  				start = previousEnd
   152  			}
   153  		} else {
   154  			start = *s.Offset
   155  		}
   156  
   157  		end := start + quantity.Offset(s.Size)
   158  		ps := LaidOutStructure{
   159  			VolumeStructure: &volume.Structure[idx],
   160  			StartOffset:     start,
   161  			Index:           idx,
   162  		}
   163  
   164  		if ps.Name != "" {
   165  			byName[ps.Name] = &ps
   166  		}
   167  
   168  		structures[idx] = ps
   169  
   170  		previousEnd = end
   171  	}
   172  
   173  	// sort by starting offset
   174  	sort.Sort(byStartOffset(structures))
   175  
   176  	previousEnd = quantity.Offset(0)
   177  	for idx, ps := range structures {
   178  		if ps.StartOffset < previousEnd {
   179  			return nil, nil, fmt.Errorf("cannot lay out volume, structure %v overlaps with preceding structure %v", ps, structures[idx-1])
   180  		}
   181  		previousEnd = ps.StartOffset + quantity.Offset(ps.Size)
   182  
   183  		offsetWrite, err := resolveOffsetWrite(ps.OffsetWrite, byName)
   184  		if err != nil {
   185  			return nil, nil, fmt.Errorf("cannot resolve offset-write of structure %v: %v", ps, err)
   186  		}
   187  		structures[idx].AbsoluteOffsetWrite = offsetWrite
   188  	}
   189  
   190  	return structures, byName, nil
   191  }
   192  
   193  // LayoutVolumePartially attempts to lay out only the structures in the volume using provided constraints
   194  func LayoutVolumePartially(volume *Volume, constraints LayoutConstraints) (*PartiallyLaidOutVolume, error) {
   195  	structures, _, err := layoutVolumeStructures(volume, constraints)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	vol := &PartiallyLaidOutVolume{
   201  		Volume:           volume,
   202  		LaidOutStructure: structures,
   203  	}
   204  	return vol, nil
   205  }
   206  
   207  // LayoutVolume attempts to completely lay out the volume, that is the
   208  // structures and their content, using provided constraints
   209  func LayoutVolume(gadgetRootDir, kernelRootDir string, volume *Volume, constraints LayoutConstraints) (*LaidOutVolume, error) {
   210  	var err error
   211  
   212  	var kernelInfo *kernel.Info
   213  	if !constraints.SkipResolveContent {
   214  		// TODO:UC20: check and error if kernelRootDir == "" here
   215  		// This needs the upper layer of gadget updates to be
   216  		// updated to pass the kernel root first.
   217  		//
   218  		// Note that the kernelRootDir may reference the running
   219  		// kernel if there is a gadget update or the new kernel if
   220  		// there is a kernel update.
   221  		kernelInfo, err = kernel.ReadInfo(kernelRootDir)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  	}
   226  
   227  	structures, byName, err := layoutVolumeStructures(volume, constraints)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	farthestEnd := quantity.Offset(0)
   233  	fartherstOffsetWrite := quantity.Offset(0)
   234  
   235  	for idx, ps := range structures {
   236  		if ps.AbsoluteOffsetWrite != nil && *ps.AbsoluteOffsetWrite > fartherstOffsetWrite {
   237  			fartherstOffsetWrite = *ps.AbsoluteOffsetWrite
   238  		}
   239  		if end := ps.StartOffset + quantity.Offset(ps.Size); end > farthestEnd {
   240  			farthestEnd = end
   241  		}
   242  
   243  		// lay out raw content
   244  		content, err := layOutStructureContent(gadgetRootDir, &structures[idx], byName)
   245  		if err != nil {
   246  			return nil, err
   247  		}
   248  
   249  		for _, c := range content {
   250  			if c.AbsoluteOffsetWrite != nil && *c.AbsoluteOffsetWrite > fartherstOffsetWrite {
   251  				fartherstOffsetWrite = *c.AbsoluteOffsetWrite
   252  			}
   253  		}
   254  
   255  		structures[idx].LaidOutContent = content
   256  
   257  		// resolve filesystem content
   258  		if !constraints.SkipResolveContent {
   259  			resolvedContent, err := resolveVolumeContent(gadgetRootDir, kernelRootDir, kernelInfo, &structures[idx], nil)
   260  			if err != nil {
   261  				return nil, err
   262  			}
   263  			structures[idx].ResolvedContent = resolvedContent
   264  		}
   265  	}
   266  
   267  	volumeSize := quantity.Size(farthestEnd)
   268  	if fartherstOffsetWrite+quantity.Offset(SizeLBA48Pointer) > farthestEnd {
   269  		volumeSize = quantity.Size(fartherstOffsetWrite) + SizeLBA48Pointer
   270  	}
   271  
   272  	vol := &LaidOutVolume{
   273  		Volume:           volume,
   274  		Size:             volumeSize,
   275  		LaidOutStructure: structures,
   276  		RootDir:          gadgetRootDir,
   277  	}
   278  	return vol, nil
   279  }
   280  
   281  func resolveVolumeContent(gadgetRootDir, kernelRootDir string, kernelInfo *kernel.Info, ps *LaidOutStructure, filter ResolvedContentFilterFunc) ([]ResolvedContent, error) {
   282  	if !ps.HasFilesystem() {
   283  		// structures without a file system are not resolved here
   284  		return nil, nil
   285  	}
   286  	if len(ps.Content) == 0 {
   287  		return nil, nil
   288  	}
   289  
   290  	content := make([]ResolvedContent, 0, len(ps.Content))
   291  	for idx := range ps.Content {
   292  		resolvedSource, kupdate, err := resolveContentPathOrRef(gadgetRootDir, kernelRootDir, kernelInfo, ps.Content[idx].UnresolvedSource)
   293  		if err != nil {
   294  			return nil, fmt.Errorf("cannot resolve content for structure %v at index %v: %v", ps, idx, err)
   295  		}
   296  		rc := ResolvedContent{
   297  			VolumeContent:  &ps.Content[idx],
   298  			ResolvedSource: resolvedSource,
   299  			KernelUpdate:   kupdate,
   300  		}
   301  		if filter != nil && !filter(&rc) {
   302  			continue
   303  		}
   304  		content = append(content, rc)
   305  	}
   306  
   307  	return content, nil
   308  }
   309  
   310  // resolveContentPathOrRef resolves the relative path from gadget
   311  // assets and any "$kernel:" references from "pathOrRef" using the
   312  // provided gadget/kernel directories and the kernel info. It returns
   313  // an absolute path, a flag indicating whether the content is part of
   314  // a kernel update, or an error.
   315  func resolveContentPathOrRef(gadgetRootDir, kernelRootDir string, kernelInfo *kernel.Info, pathOrRef string) (resolved string, kupdate bool, err error) {
   316  
   317  	// TODO: add kernelRootDir == "" error too once all the higher
   318  	//       layers in devicestate call gadget.Update() with a
   319  	//       kernel dir set
   320  	switch {
   321  	case gadgetRootDir == "":
   322  		return "", false, fmt.Errorf("internal error: gadget root dir cannot beempty")
   323  	case pathOrRef == "":
   324  		return "", false, fmt.Errorf("cannot use empty source")
   325  	}
   326  
   327  	// content may refer to "$kernel:<name>/<content>"
   328  	var resolvedSource string
   329  	if strings.HasPrefix(pathOrRef, "$kernel:") {
   330  		wantedAsset, wantedContent, err := splitKernelRef(pathOrRef)
   331  		if err != nil {
   332  			return "", false, fmt.Errorf("cannot parse kernel ref: %v", err)
   333  		}
   334  		kernelAsset, ok := kernelInfo.Assets[wantedAsset]
   335  		if !ok {
   336  			return "", false, fmt.Errorf("cannot find %q in kernel info from %q", wantedAsset, kernelRootDir)
   337  		}
   338  		// look for exact content match or for a directory prefix match
   339  		found := false
   340  		for _, kcontent := range kernelAsset.Content {
   341  			if wantedContent == kcontent {
   342  				found = true
   343  				break
   344  			}
   345  			// ensure we only check subdirs
   346  			suffix := ""
   347  			if !strings.HasSuffix(kcontent, "/") {
   348  				suffix = "/"
   349  			}
   350  			if strings.HasPrefix(wantedContent, kcontent+suffix) {
   351  				found = true
   352  				break
   353  			}
   354  		}
   355  		if !found {
   356  			return "", false, fmt.Errorf("cannot find wanted kernel content %q in %q", wantedContent, kernelRootDir)
   357  		}
   358  		resolvedSource = filepath.Join(kernelRootDir, wantedContent)
   359  		kupdate = kernelAsset.Update
   360  	} else {
   361  		resolvedSource = filepath.Join(gadgetRootDir, pathOrRef)
   362  	}
   363  
   364  	// restore trailing / if one was there
   365  	if strings.HasSuffix(pathOrRef, "/") {
   366  		resolvedSource += "/"
   367  	}
   368  
   369  	return resolvedSource, kupdate, nil
   370  }
   371  
   372  type byContentStartOffset []LaidOutContent
   373  
   374  func (b byContentStartOffset) Len() int           { return len(b) }
   375  func (b byContentStartOffset) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   376  func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
   377  
   378  func getImageSize(path string) (quantity.Size, error) {
   379  	stat, err := os.Stat(path)
   380  	if err != nil {
   381  		return 0, err
   382  	}
   383  	return quantity.Size(stat.Size()), nil
   384  }
   385  
   386  func layOutStructureContent(gadgetRootDir string, ps *LaidOutStructure, known map[string]*LaidOutStructure) ([]LaidOutContent, error) {
   387  	if ps.HasFilesystem() {
   388  		// structures with a filesystem do not need any extra layout
   389  		return nil, nil
   390  	}
   391  	if len(ps.Content) == 0 {
   392  		return nil, nil
   393  	}
   394  
   395  	content := make([]LaidOutContent, len(ps.Content))
   396  	previousEnd := quantity.Offset(0)
   397  
   398  	for idx, c := range ps.Content {
   399  		imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image))
   400  		if err != nil {
   401  			return nil, fmt.Errorf("cannot lay out structure %v: content %q: %v", ps, c.Image, err)
   402  		}
   403  
   404  		var start quantity.Offset
   405  		if c.Offset != nil {
   406  			start = *c.Offset
   407  		} else {
   408  			start = previousEnd
   409  		}
   410  
   411  		actualSize := imageSize
   412  
   413  		if c.Size != 0 {
   414  			if c.Size < imageSize {
   415  				return nil, fmt.Errorf("cannot lay out structure %v: content %q size %v is larger than declared %v", ps, c.Image, actualSize, c.Size)
   416  			}
   417  			actualSize = c.Size
   418  		}
   419  
   420  		offsetWrite, err := resolveOffsetWrite(c.OffsetWrite, known)
   421  		if err != nil {
   422  			return nil, fmt.Errorf("cannot resolve offset-write of structure %v content %q: %v", ps, c.Image, err)
   423  		}
   424  
   425  		content[idx] = LaidOutContent{
   426  			VolumeContent: &ps.Content[idx],
   427  			Size:          actualSize,
   428  			StartOffset:   ps.StartOffset + start,
   429  			Index:         idx,
   430  			// break for gofmt < 1.11
   431  			AbsoluteOffsetWrite: offsetWrite,
   432  		}
   433  		previousEnd = start + quantity.Offset(actualSize)
   434  		if quantity.Size(previousEnd) > ps.Size {
   435  			return nil, fmt.Errorf("cannot lay out structure %v: content %q does not fit in the structure", ps, c.Image)
   436  		}
   437  	}
   438  
   439  	sort.Sort(byContentStartOffset(content))
   440  
   441  	previousEnd = ps.StartOffset
   442  	for idx, pc := range content {
   443  		if pc.StartOffset < previousEnd {
   444  			return nil, fmt.Errorf("cannot lay out structure %v: content %q overlaps with preceding image %q", ps, pc.Image, content[idx-1].Image)
   445  		}
   446  		previousEnd = pc.StartOffset + quantity.Offset(pc.Size)
   447  	}
   448  
   449  	return content, nil
   450  }
   451  
   452  func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*quantity.Offset, error) {
   453  	if offsetWrite == nil {
   454  		return nil, nil
   455  	}
   456  
   457  	var relativeToOffset quantity.Offset
   458  	if offsetWrite.RelativeTo != "" {
   459  		otherStruct, ok := knownStructs[offsetWrite.RelativeTo]
   460  		if !ok {
   461  			return nil, fmt.Errorf("refers to an unknown structure %q", offsetWrite.RelativeTo)
   462  		}
   463  		relativeToOffset = otherStruct.StartOffset
   464  	}
   465  
   466  	resolvedOffsetWrite := relativeToOffset + offsetWrite.Offset
   467  	return &resolvedOffsetWrite, nil
   468  }
   469  
   470  // ShiftStructureTo translates the starting offset of a laid out structure and
   471  // its content to the provided offset.
   472  func ShiftStructureTo(ps LaidOutStructure, offset quantity.Offset) LaidOutStructure {
   473  	change := int64(offset - ps.StartOffset)
   474  
   475  	newPs := ps
   476  	newPs.StartOffset = quantity.Offset(int64(ps.StartOffset) + change)
   477  
   478  	newPs.LaidOutContent = make([]LaidOutContent, len(ps.LaidOutContent))
   479  	for idx, pc := range ps.LaidOutContent {
   480  		newPc := pc
   481  		newPc.StartOffset = quantity.Offset(int64(pc.StartOffset) + change)
   482  		newPs.LaidOutContent[idx] = newPc
   483  	}
   484  	return newPs
   485  }
   486  
   487  func isLayoutCompatible(current, new *PartiallyLaidOutVolume) error {
   488  	if current.ID != new.ID {
   489  		return fmt.Errorf("incompatible ID change from %v to %v", current.ID, new.ID)
   490  	}
   491  	if current.Schema != new.Schema {
   492  		return fmt.Errorf("incompatible schema change from %v to %v",
   493  			current.Schema, new.Schema)
   494  	}
   495  	if current.Bootloader != new.Bootloader {
   496  		return fmt.Errorf("incompatible bootloader change from %v to %v",
   497  			current.Bootloader, new.Bootloader)
   498  	}
   499  
   500  	// XXX: the code below asssumes both volumes have the same number of
   501  	// structures, this limitation may be lifted later
   502  	if len(current.LaidOutStructure) != len(new.LaidOutStructure) {
   503  		return fmt.Errorf("incompatible change in the number of structures from %v to %v",
   504  			len(current.LaidOutStructure), len(new.LaidOutStructure))
   505  	}
   506  
   507  	// at the structure level we expect the volume to be identical
   508  	for i := range current.LaidOutStructure {
   509  		from := &current.LaidOutStructure[i]
   510  		to := &new.LaidOutStructure[i]
   511  		if err := canUpdateStructure(from, to, new.Schema); err != nil {
   512  			return fmt.Errorf("incompatible structure %v change: %v", to, err)
   513  		}
   514  	}
   515  	return nil
   516  }