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