github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/bootloader/lk.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 bootloader
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  
    29  	"golang.org/x/xerrors"
    30  
    31  	"github.com/snapcore/snapd/bootloader/lkenv"
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/osutil/disks"
    36  	"github.com/snapcore/snapd/snap"
    37  )
    38  
    39  const (
    40  	backupStorage  = true
    41  	primaryStorage = false
    42  )
    43  
    44  type lk struct {
    45  	rootdir          string
    46  	prepareImageTime bool
    47  
    48  	// role is what bootloader role we are, which also maps to which version of
    49  	// the underlying lkenv struct we use for bootenv
    50  	// * RoleSole == uc16 -> v1
    51  	// * RoleRecovery == uc20 + recovery -> v2 recovery
    52  	// * RoleRunMode == uc20 + run -> v2 run
    53  	role Role
    54  
    55  	// blDisk is what disk the bootloader informed us to use to look for the
    56  	// bootloader structure partitions
    57  	blDisk disks.Disk
    58  }
    59  
    60  // newLk create a new lk bootloader object
    61  func newLk(rootdir string, opts *Options) Bootloader {
    62  	l := &lk{rootdir: rootdir}
    63  
    64  	l.processOpts(opts)
    65  
    66  	return l
    67  }
    68  
    69  func (l *lk) processOpts(opts *Options) {
    70  	if opts != nil {
    71  		// XXX: in the long run we want this to go away, we probably add
    72  		//      something like "boot.PrepareImage()" and add an (optional)
    73  		//      method "PrepareImage" to the bootloader interface that is
    74  		//      used to setup a bootloader from prepare-image if things
    75  		//      are very different from runtime vs image-building mode.
    76  		//
    77  		// determine mode we are in, runtime or image build
    78  
    79  		l.prepareImageTime = opts.PrepareImageTime
    80  
    81  		l.role = opts.Role
    82  	}
    83  }
    84  
    85  func (l *lk) Name() string {
    86  	return "lk"
    87  }
    88  
    89  func (l *lk) dir() string {
    90  	if l.prepareImageTime {
    91  		// at prepare-image time, then use rootdir and look for /boot/lk/ -
    92  		// this is only used in prepare-image time where the binary files exist
    93  		// extracted from the gadget
    94  		return filepath.Join(l.rootdir, "/boot/lk/")
    95  	}
    96  
    97  	// for runtime, we should only be using dir() for V1 and the dir is just
    98  	// the udev by-partlabel directory
    99  	switch l.role {
   100  	case RoleSole:
   101  		// TODO: this should be adjusted to try and use the kernel cmdline
   102  		//       parameter for the disk that the bootloader says to find
   103  		//       the lk partitions on if provided like the UC20 case does, but
   104  		//       that involves changing many more tests, so let's do that in a
   105  		//       followup PR
   106  		return filepath.Join(l.rootdir, "/dev/disk/by-partlabel/")
   107  	case RoleRecovery, RoleRunMode:
   108  		// TODO: maybe panic'ing here is a bit harsh...
   109  		panic("internal error: shouldn't be using lk.dir() for uc20 runtime modes!")
   110  	default:
   111  		panic("unexpected bootloader role for lk dir")
   112  	}
   113  }
   114  
   115  func (l *lk) InstallBootConfig(gadgetDir string, opts *Options) error {
   116  	// make sure that the opts are put into the object
   117  	l.processOpts(opts)
   118  	gadgetFile := filepath.Join(gadgetDir, l.Name()+".conf")
   119  	// since we are just installing static files from the gadget, there is no
   120  	// backup to copy, the backup will be created automatically (if allowed) by
   121  	// lkenv when we go to Save() the environment file.
   122  	systemFile, err := l.envBackstore(primaryStorage)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	return genericInstallBootConfig(gadgetFile, systemFile)
   127  }
   128  
   129  func (l *lk) Present() (bool, error) {
   130  	// if we are in prepare-image mode or in V1, just check the env file
   131  	if l.prepareImageTime || l.role == RoleSole {
   132  		primary, err := l.envBackstore(primaryStorage)
   133  		if err != nil {
   134  			return false, err
   135  		}
   136  
   137  		if osutil.FileExists(primary) {
   138  			return true, nil
   139  		}
   140  
   141  		// at prepare-image time, we won't have a backup file from the gadget,
   142  		// so just give up here
   143  		if l.prepareImageTime {
   144  			return false, nil
   145  		}
   146  
   147  		// but at runtime we should check the backup in case the primary
   148  		// partition got corrupted
   149  		backup, err := l.envBackstore(backupStorage)
   150  		if err != nil {
   151  			return false, err
   152  		}
   153  		return osutil.FileExists(backup), nil
   154  	}
   155  
   156  	// otherwise for V2, non-sole bootloader roles we need to check on the
   157  	// partition name existing, note that devPathForPartName will only return
   158  	// partiallyFound as true if it reasonably concludes that this is a lk
   159  	// device, so in that case forward err, otherwise return err as nil
   160  	partitionLabel := l.partLabelForRole()
   161  	_, partiallyFound, err := l.devPathForPartName(partitionLabel)
   162  	if partiallyFound {
   163  		return true, err
   164  	}
   165  	return false, nil
   166  }
   167  
   168  func (l *lk) partLabelForRole() string {
   169  	// TODO: should the partition labels be fetched from gadget.yaml instead? we
   170  	//       have roles that we could use in the gadget.yaml structures to find
   171  	//       them
   172  	label := ""
   173  	switch l.role {
   174  	case RoleSole, RoleRunMode:
   175  		label = "snapbootsel"
   176  	case RoleRecovery:
   177  		label = "snaprecoverysel"
   178  	default:
   179  		panic(fmt.Sprintf("unknown bootloader role for littlekernel: %s", l.role))
   180  	}
   181  	return label
   182  }
   183  
   184  // envBackstore returns a filepath for the lkenv bootloader environment file.
   185  // For prepare-image time operations, it will be a normal config file; for
   186  // runtime operations it will be a device file from a udev-created symlink in
   187  // /dev/disk. If backup is true then the filename is suffixed with "bak" or at
   188  // runtime the partition label is suffixed with "bak".
   189  func (l *lk) envBackstore(backup bool) (string, error) {
   190  	partitionLabelOrConfFile := l.partLabelForRole()
   191  	if backup {
   192  		partitionLabelOrConfFile += "bak"
   193  	}
   194  	if l.prepareImageTime {
   195  		// at prepare-image time, we just use the env file, but append .bin
   196  		// since it is a file from the gadget we will evenutally install into
   197  		// a partition when flashing the image
   198  		return filepath.Join(l.dir(), partitionLabelOrConfFile+".bin"), nil
   199  	}
   200  
   201  	if l.role == RoleSole {
   202  		// for V1, we just use the partition label directly, dir() here will be
   203  		// the udev by-partlabel symlink dir.
   204  		// see TODO: in l.dir(), this should eventually also be using
   205  		// devPathForPartName() too
   206  		return filepath.Join(l.dir(), partitionLabelOrConfFile), nil
   207  	}
   208  
   209  	// for RoleRun or RoleRecovery, we need to find the partition securely
   210  	partitionFile, _, err := l.devPathForPartName(partitionLabelOrConfFile)
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  	return partitionFile, nil
   215  }
   216  
   217  // devPathForPartName returns the environment file in /dev for the partition
   218  // name, which will always be a partition on the disk given by
   219  // the kernel command line parameter "snapd_lk_boot_disk" set by the bootloader.
   220  // It returns a boolean as the second parameter which is primarily used by
   221  // Present() to indicate if the searching process got "far enough" to reasonably
   222  // conclude that the device is using a lk bootloader, but we had errors finding
   223  // it. This feature is mainly for better error reporting in logs.
   224  func (l *lk) devPathForPartName(partName string) (string, bool, error) {
   225  	// lazily initialize l.blDisk if it hasn't yet been initialized
   226  	if l.blDisk == nil {
   227  		// For security, we want to restrict our search for the partition
   228  		// that the binary structure exists on to only the disk that the
   229  		// bootloader tells us to search on - it uses a kernel cmdline
   230  		// parameter "snapd_lk_boot_disk" to indicated which disk we should look
   231  		// for partitions on. In typical boot scenario this will be something like
   232  		// "snapd_lk_boot_disk=mmcblk0".
   233  		m, err := osutil.KernelCommandLineKeyValues("snapd_lk_boot_disk")
   234  		if err != nil {
   235  			// return false, since we don't have enough info to conclude there
   236  			// is likely a lk bootloader here or not
   237  			return "", false, err
   238  		}
   239  		blDiskName, ok := m["snapd_lk_boot_disk"]
   240  		if blDiskName == "" {
   241  			// we switch on ok here, since if "snapd_lk_boot_disk" was found at
   242  			// all on the kernel command line, we can reasonably assume that
   243  			// only the lk bootloader would have put it there, but maybe
   244  			// it is buggy and put an empty value there.
   245  			if ok {
   246  				return "", true, fmt.Errorf("kernel command line parameter \"snapd_lk_boot_disk\" is empty")
   247  			}
   248  			// if we didn't find the kernel command line parameter at all, then
   249  			// we want to return false because we don't have enough info
   250  			return "", false, fmt.Errorf("kernel command line parameter \"snapd_lk_boot_disk\" is missing")
   251  		}
   252  
   253  		disk, err := disks.DiskFromDeviceName(blDiskName)
   254  		if err != nil {
   255  			return "", true, fmt.Errorf("cannot find disk from bootloader supplied disk name %q: %v", blDiskName, err)
   256  		}
   257  
   258  		l.blDisk = disk
   259  	}
   260  
   261  	partitionUUID, err := l.blDisk.FindMatchingPartitionUUIDWithPartLabel(partName)
   262  	if err != nil {
   263  		return "", true, err
   264  	}
   265  
   266  	// for the runtime lk bootloader we should never prefix the path with the
   267  	// bootloader rootdir and instead always use dirs.GlobalRootDir, since the
   268  	// file we are providing is at an absolute location for all bootloaders,
   269  	// regardless of role, in /dev, so using dirs.GlobalRootDir ensures that we
   270  	// are still able to mock things in test functions, but that we never end up
   271  	// trying to use a path like /run/mnt/ubuntu-boot/dev/disk/by-partuuid/...
   272  	// for example
   273  	return filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partuuid", partitionUUID), true, nil
   274  }
   275  
   276  func (l *lk) newenv() (*lkenv.Env, error) {
   277  	// check which role we are, it affects which struct is used for the env
   278  	var version lkenv.Version
   279  	switch l.role {
   280  	case RoleSole:
   281  		version = lkenv.V1
   282  	case RoleRecovery:
   283  		version = lkenv.V2Recovery
   284  	case RoleRunMode:
   285  		version = lkenv.V2Run
   286  	}
   287  	f, err := l.envBackstore(primaryStorage)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	backup, err := l.envBackstore(backupStorage)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  
   297  	return lkenv.NewEnv(f, backup, version), nil
   298  }
   299  
   300  func (l *lk) GetBootVars(names ...string) (map[string]string, error) {
   301  	out := make(map[string]string)
   302  
   303  	env, err := l.newenv()
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	if err := env.Load(); err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	for _, name := range names {
   312  		out[name] = env.Get(name)
   313  	}
   314  
   315  	return out, nil
   316  }
   317  
   318  func (l *lk) SetBootVars(values map[string]string) error {
   319  	env, err := l.newenv()
   320  	if err != nil {
   321  		return err
   322  	}
   323  	// if we couldn't find the env, that's okay, as this may be the first thing
   324  	// to write boot vars to the env
   325  	if err := env.Load(); err != nil {
   326  		// if the error was something other than file not found, it is fatal
   327  		if !xerrors.Is(err, os.ErrNotExist) {
   328  			return err
   329  		}
   330  		// otherwise at prepare-image time it is okay to not have the file
   331  		// existing, but we should always have it at runtime as it is a
   332  		// partition, so it is highly unexpected for it to be missing and we
   333  		// cannot proceed
   334  		// also note that env.Load() will automatically try the backup, so if
   335  		// Load() failed to get the backup at runtime there's nothing left to
   336  		// try here
   337  		if !l.prepareImageTime {
   338  			return err
   339  		}
   340  	}
   341  
   342  	// update environment only if something changes
   343  	dirty := false
   344  	for k, v := range values {
   345  		// already set to the right value, nothing to do
   346  		if env.Get(k) == v {
   347  			continue
   348  		}
   349  		env.Set(k, v)
   350  		dirty = true
   351  	}
   352  
   353  	if dirty {
   354  		return env.Save()
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  func (l *lk) ExtractRecoveryKernelAssets(recoverySystemDir string, sn snap.PlaceInfo, snapf snap.Container) error {
   361  	if !l.prepareImageTime {
   362  		// error case, we cannot be extracting a recovery kernel and also be
   363  		// called with !opts.PrepareImageTime (yet)
   364  
   365  		// TODO:UC20: this codepath is exercised when creating new
   366  		// recovery systems from runtime
   367  		return fmt.Errorf("internal error: extracting recovery kernel assets is not supported for a runtime lk bootloader")
   368  	}
   369  
   370  	env, err := l.newenv()
   371  	if err != nil {
   372  		return err
   373  	}
   374  	if err := env.Load(); err != nil {
   375  		// don't handle os.ErrNotExist specially here, it doesn't really make
   376  		// sense to extract kernel assets if we can't load the existing env,
   377  		// since then the caller would just see an error about not being able
   378  		// to find the kernel blob name (as they will all be empty in the env),
   379  		// when in reality the reason one can't find an available boot image
   380  		// partition is because we couldn't read the env file and so returning
   381  		// that error is better
   382  		return err
   383  	}
   384  
   385  	recoverySystem := filepath.Base(recoverySystemDir)
   386  
   387  	bootPartition, err := env.FindFreeRecoverySystemBootPartition(recoverySystem)
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	// we are preparing a recovery system, just extract boot image to bootloader
   393  	// directory
   394  	logger.Debugf("extracting recovery kernel %s to %s with lk bootloader", sn.SnapName(), recoverySystem)
   395  	if err := snapf.Unpack(env.GetBootImageName(), l.dir()); err != nil {
   396  		return fmt.Errorf("cannot open unpacked %s: %v", env.GetBootImageName(), err)
   397  	}
   398  
   399  	if err := env.SetBootPartitionRecoverySystem(bootPartition, recoverySystem); err != nil {
   400  		return err
   401  	}
   402  
   403  	return env.Save()
   404  }
   405  
   406  // ExtractKernelAssets extract kernel assets per bootloader specifics
   407  // lk bootloader requires boot partition to hold valid boot image
   408  // there are two boot partition available, one holding current bootimage
   409  // kernel assets are extracted to other (free) boot partition
   410  // in case this function is called as part of image creation,
   411  // boot image is extracted to the file
   412  func (l *lk) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
   413  	blobName := s.Filename()
   414  
   415  	logger.Debugf("extracting kernel assets for %s with lk bootloader", s.SnapName())
   416  
   417  	env, err := l.newenv()
   418  	if err != nil {
   419  		return err
   420  	}
   421  	if err := env.Load(); err != nil {
   422  		// don't handle os.ErrNotExist specially here, it doesn't really make
   423  		// sense to extract kernel assets if we can't load the existing env,
   424  		// since then the caller would just see an error about not being able
   425  		// to find the kernel blob name (as they will all be empty in the env),
   426  		// when in reality the reason one can't find an available boot image
   427  		// partition is because we couldn't read the env file and so returning
   428  		// that error is better
   429  		return err
   430  	}
   431  
   432  	bootPartition, err := env.FindFreeKernelBootPartition(blobName)
   433  	if err != nil {
   434  		return err
   435  	}
   436  
   437  	if l.prepareImageTime {
   438  		// we are preparing image, just extract boot image to bootloader directory
   439  		if err := snapf.Unpack(env.GetBootImageName(), l.dir()); err != nil {
   440  			return fmt.Errorf("cannot open unpacked %s: %v", env.GetBootImageName(), err)
   441  		}
   442  	} else {
   443  		// this is live system, extracted bootimg needs to be flashed to
   444  		// free bootimg partition and env has to be updated with
   445  		// new kernel snap to bootimg partition mapping
   446  		tmpdir, err := ioutil.TempDir("", "bootimg")
   447  		if err != nil {
   448  			return fmt.Errorf("cannot create temp directory: %v", err)
   449  		}
   450  		defer os.RemoveAll(tmpdir)
   451  
   452  		bootImg := env.GetBootImageName()
   453  		if err := snapf.Unpack(bootImg, tmpdir); err != nil {
   454  			return fmt.Errorf("cannot unpack %s: %v", bootImg, err)
   455  		}
   456  		// write boot.img to free boot partition
   457  		bootimgName := filepath.Join(tmpdir, bootImg)
   458  		bif, err := os.Open(bootimgName)
   459  		if err != nil {
   460  			return fmt.Errorf("cannot open unpacked %s: %v", bootImg, err)
   461  		}
   462  		defer bif.Close()
   463  		var bpart string
   464  		// TODO: for RoleSole bootloaders this will eventually be the same
   465  		// codepath as for non-RoleSole bootloader
   466  		if l.role == RoleSole {
   467  			bpart = filepath.Join(l.dir(), bootPartition)
   468  		} else {
   469  			bpart, _, err = l.devPathForPartName(bootPartition)
   470  			if err != nil {
   471  				return err
   472  			}
   473  		}
   474  
   475  		bpf, err := os.OpenFile(bpart, os.O_WRONLY, 0660)
   476  		if err != nil {
   477  			return fmt.Errorf("cannot open boot partition [%s]: %v", bpart, err)
   478  		}
   479  		defer bpf.Close()
   480  
   481  		if _, err := io.Copy(bpf, bif); err != nil {
   482  			return err
   483  		}
   484  	}
   485  
   486  	if err := env.SetBootPartitionKernel(bootPartition, blobName); err != nil {
   487  		return err
   488  	}
   489  
   490  	return env.Save()
   491  }
   492  
   493  func (l *lk) RemoveKernelAssets(s snap.PlaceInfo) error {
   494  	blobName := s.Filename()
   495  	logger.Debugf("removing kernel assets for %s with lk bootloader", s.SnapName())
   496  
   497  	env, err := l.newenv()
   498  	if err != nil {
   499  		return err
   500  	}
   501  	if err := env.Load(); err != nil {
   502  		// don't handle os.ErrNotExist specially here, it doesn't really make
   503  		// sense to delete kernel assets if we can't load the existing env,
   504  		// since then the caller would just see an error about not being able
   505  		// to find the kernel blob name, when in reality the reason one can't
   506  		// find that kernel blob name is because we couldn't read the env file
   507  		return err
   508  	}
   509  	err = env.RemoveKernelFromBootPartition(blobName)
   510  	if err == nil {
   511  		// found and removed the revision from the bootimg matrix, need to
   512  		// update the env to persist the change
   513  		return env.Save()
   514  	}
   515  	return nil
   516  }