Files
snapd/bootloader/lk.go
Miguel Pires 79c5ac14b2 many: remove usages of deprecated io/ioutil package (#13768)
* many: remove usages of deprecated io/ioutil package

Signed-off-by: Miguel Pires <miguel.pires@canonical.com>

* .golangci.yml: remove errcheck ignore rule for io/ioutil

Signed-off-by: Miguel Pires <miguel.pires@canonical.com>

* run-checks: prevent new usages of io/ioutil

Signed-off-by: Miguel Pires <miguel.pires@canonical.com>

---------

Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
2024-04-03 23:23:24 +02:00

517 lines
17 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2022 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package bootloader
import (
"fmt"
"io"
"os"
"path/filepath"
"golang.org/x/xerrors"
"github.com/snapcore/snapd/bootloader/lkenv"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/snap"
)
const (
backupStorage = true
primaryStorage = false
)
type lk struct {
rootdir string
prepareImageTime bool
// role is what bootloader role we are, which also maps to which version of
// the underlying lkenv struct we use for bootenv
// * RoleSole == uc16 -> v1
// * RoleRecovery == uc20 + recovery -> v2 recovery
// * RoleRunMode == uc20 + run -> v2 run
role Role
// blDisk is what disk the bootloader informed us to use to look for the
// bootloader structure partitions
blDisk disks.Disk
}
// newLk create a new lk bootloader object
func newLk(rootdir string, opts *Options) Bootloader {
l := &lk{rootdir: rootdir}
l.processOpts(opts)
return l
}
func (l *lk) processOpts(opts *Options) {
if opts != nil {
// XXX: in the long run we want this to go away, we probably add
// something like "boot.PrepareImage()" and add an (optional)
// method "PrepareImage" to the bootloader interface that is
// used to setup a bootloader from prepare-image if things
// are very different from runtime vs image-building mode.
//
// determine mode we are in, runtime or image build
l.prepareImageTime = opts.PrepareImageTime
l.role = opts.Role
}
}
func (l *lk) Name() string {
return "lk"
}
func (l *lk) dir() string {
if l.prepareImageTime {
// at prepare-image time, then use rootdir and look for /boot/lk/ -
// this is only used in prepare-image time where the binary files exist
// extracted from the gadget
return filepath.Join(l.rootdir, "/boot/lk/")
}
// for runtime, we should only be using dir() for V1 and the dir is just
// the udev by-partlabel directory
switch l.role {
case RoleSole:
// TODO: this should be adjusted to try and use the kernel cmdline
// parameter for the disk that the bootloader says to find
// the lk partitions on if provided like the UC20 case does, but
// that involves changing many more tests, so let's do that in a
// followup PR
return filepath.Join(l.rootdir, "/dev/disk/by-partlabel/")
case RoleRecovery, RoleRunMode:
// TODO: maybe panic'ing here is a bit harsh...
panic("internal error: shouldn't be using lk.dir() for UC20+ runtime modes!")
default:
panic("unexpected bootloader role for lk dir")
}
}
func (l *lk) InstallBootConfig(gadgetDir string, opts *Options) error {
// make sure that the opts are put into the object
l.processOpts(opts)
gadgetFile := filepath.Join(gadgetDir, l.Name()+".conf")
// since we are just installing static files from the gadget, there is no
// backup to copy, the backup will be created automatically (if allowed) by
// lkenv when we go to Save() the environment file.
systemFile, err := l.envBackstore(primaryStorage)
if err != nil {
return err
}
return genericInstallBootConfig(gadgetFile, systemFile)
}
func (l *lk) Present() (bool, error) {
// if we are in prepare-image mode or in V1, just check the env file
if l.prepareImageTime || l.role == RoleSole {
primary, err := l.envBackstore(primaryStorage)
if err != nil {
return false, err
}
if osutil.FileExists(primary) {
return true, nil
}
// at prepare-image time, we won't have a backup file from the gadget,
// so just give up here
if l.prepareImageTime {
return false, nil
}
// but at runtime we should check the backup in case the primary
// partition got corrupted
backup, err := l.envBackstore(backupStorage)
if err != nil {
return false, err
}
return osutil.FileExists(backup), nil
}
// otherwise for V2, non-sole bootloader roles we need to check on the
// partition name existing, note that devPathForPartName will only return
// partiallyFound as true if it reasonably concludes that this is a lk
// device, so in that case forward err, otherwise return err as nil
partitionLabel := l.partLabelForRole()
_, partiallyFound, err := l.devPathForPartName(partitionLabel)
if partiallyFound {
return true, err
}
return false, nil
}
func (l *lk) partLabelForRole() string {
// TODO: should the partition labels be fetched from gadget.yaml instead? we
// have roles that we could use in the gadget.yaml structures to find
// them
label := ""
switch l.role {
case RoleSole, RoleRunMode:
label = "snapbootsel"
case RoleRecovery:
label = "snaprecoverysel"
default:
panic(fmt.Sprintf("unknown bootloader role for littlekernel: %s", l.role))
}
return label
}
// envBackstore returns a filepath for the lkenv bootloader environment file.
// For prepare-image time operations, it will be a normal config file; for
// runtime operations it will be a device file from a udev-created symlink in
// /dev/disk. If backup is true then the filename is suffixed with "bak" or at
// runtime the partition label is suffixed with "bak".
func (l *lk) envBackstore(backup bool) (string, error) {
partitionLabelOrConfFile := l.partLabelForRole()
if backup {
partitionLabelOrConfFile += "bak"
}
if l.prepareImageTime {
// at prepare-image time, we just use the env file, but append .bin
// since it is a file from the gadget we will evenutally install into
// a partition when flashing the image
return filepath.Join(l.dir(), partitionLabelOrConfFile+".bin"), nil
}
if l.role == RoleSole {
// for V1, we just use the partition label directly, dir() here will be
// the udev by-partlabel symlink dir.
// see TODO: in l.dir(), this should eventually also be using
// devPathForPartName() too
return filepath.Join(l.dir(), partitionLabelOrConfFile), nil
}
// for RoleRun or RoleRecovery, we need to find the partition securely
partitionFile, _, err := l.devPathForPartName(partitionLabelOrConfFile)
if err != nil {
return "", err
}
return partitionFile, nil
}
// devPathForPartName returns the environment file in /dev for the partition
// name, which will always be a partition on the disk given by
// the kernel command line parameter "snapd_lk_boot_disk" set by the bootloader.
// It returns a boolean as the second parameter which is primarily used by
// Present() to indicate if the searching process got "far enough" to reasonably
// conclude that the device is using a lk bootloader, but we had errors finding
// it. This feature is mainly for better error reporting in logs.
func (l *lk) devPathForPartName(partName string) (string, bool, error) {
// lazily initialize l.blDisk if it hasn't yet been initialized
if l.blDisk == nil {
// For security, we want to restrict our search for the partition
// that the binary structure exists on to only the disk that the
// bootloader tells us to search on - it uses a kernel cmdline
// parameter "snapd_lk_boot_disk" to indicated which disk we should look
// for partitions on. In typical boot scenario this will be something like
// "snapd_lk_boot_disk=mmcblk0".
m, err := kcmdline.KeyValues("snapd_lk_boot_disk")
if err != nil {
// return false, since we don't have enough info to conclude there
// is likely a lk bootloader here or not
return "", false, err
}
blDiskName, ok := m["snapd_lk_boot_disk"]
if blDiskName == "" {
// we switch on ok here, since if "snapd_lk_boot_disk" was found at
// all on the kernel command line, we can reasonably assume that
// only the lk bootloader would have put it there, but maybe
// it is buggy and put an empty value there.
if ok {
return "", true, fmt.Errorf("kernel command line parameter \"snapd_lk_boot_disk\" is empty")
}
// if we didn't find the kernel command line parameter at all, then
// we want to return false because we don't have enough info
return "", false, fmt.Errorf("kernel command line parameter \"snapd_lk_boot_disk\" is missing")
}
disk, err := disks.DiskFromDeviceName(blDiskName)
if err != nil {
return "", true, fmt.Errorf("cannot find disk from bootloader supplied disk name %q: %v", blDiskName, err)
}
l.blDisk = disk
}
partitionUUID, err := l.blDisk.FindMatchingPartitionUUIDWithPartLabel(partName)
if err != nil {
return "", true, err
}
// for the runtime lk bootloader we should never prefix the path with the
// bootloader rootdir and instead always use dirs.GlobalRootDir, since the
// file we are providing is at an absolute location for all bootloaders,
// regardless of role, in /dev, so using dirs.GlobalRootDir ensures that we
// are still able to mock things in test functions, but that we never end up
// trying to use a path like /run/mnt/ubuntu-boot/dev/disk/by-partuuid/...
// for example
return filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partuuid", partitionUUID), true, nil
}
func (l *lk) newenv() (*lkenv.Env, error) {
// check which role we are, it affects which struct is used for the env
var version lkenv.Version
switch l.role {
case RoleSole:
version = lkenv.V1
case RoleRecovery:
version = lkenv.V2Recovery
case RoleRunMode:
version = lkenv.V2Run
}
f, err := l.envBackstore(primaryStorage)
if err != nil {
return nil, err
}
backup, err := l.envBackstore(backupStorage)
if err != nil {
return nil, err
}
return lkenv.NewEnv(f, backup, version), nil
}
func (l *lk) GetBootVars(names ...string) (map[string]string, error) {
out := make(map[string]string)
env, err := l.newenv()
if err != nil {
return nil, err
}
if err := env.Load(); err != nil {
return nil, err
}
for _, name := range names {
out[name] = env.Get(name)
}
return out, nil
}
func (l *lk) SetBootVars(values map[string]string) error {
env, err := l.newenv()
if err != nil {
return err
}
// if we couldn't find the env, that's okay, as this may be the first thing
// to write boot vars to the env
if err := env.Load(); err != nil {
// if the error was something other than file not found, it is fatal
if !xerrors.Is(err, os.ErrNotExist) {
return err
}
// otherwise at prepare-image time it is okay to not have the file
// existing, but we should always have it at runtime as it is a
// partition, so it is highly unexpected for it to be missing and we
// cannot proceed
// also note that env.Load() will automatically try the backup, so if
// Load() failed to get the backup at runtime there's nothing left to
// try here
if !l.prepareImageTime {
return err
}
}
// update environment only if something changes
dirty := false
for k, v := range values {
// already set to the right value, nothing to do
if env.Get(k) == v {
continue
}
env.Set(k, v)
dirty = true
}
if dirty {
return env.Save()
}
return nil
}
func (l *lk) ExtractRecoveryKernelAssets(recoverySystemDir string, sn snap.PlaceInfo, snapf snap.Container) error {
if !l.prepareImageTime {
// error case, we cannot be extracting a recovery kernel and also be
// called with !opts.PrepareImageTime (yet)
// TODO:UC20: this codepath is exercised when creating new
// recovery systems from runtime
return fmt.Errorf("internal error: extracting recovery kernel assets is not supported for a runtime lk bootloader")
}
env, err := l.newenv()
if err != nil {
return err
}
if err := env.Load(); err != nil {
// don't handle os.ErrNotExist specially here, it doesn't really make
// sense to extract kernel assets if we can't load the existing env,
// since then the caller would just see an error about not being able
// to find the kernel blob name (as they will all be empty in the env),
// when in reality the reason one can't find an available boot image
// partition is because we couldn't read the env file and so returning
// that error is better
return err
}
recoverySystem := filepath.Base(recoverySystemDir)
bootPartition, err := env.FindFreeRecoverySystemBootPartition(recoverySystem)
if err != nil {
return err
}
// we are preparing a recovery system, just extract boot image to bootloader
// directory
logger.Debugf("extracting recovery kernel %s to %s with lk bootloader", sn.SnapName(), recoverySystem)
if err := snapf.Unpack(env.GetBootImageName(), l.dir()); err != nil {
return fmt.Errorf("cannot open unpacked %s: %v", env.GetBootImageName(), err)
}
if err := env.SetBootPartitionRecoverySystem(bootPartition, recoverySystem); err != nil {
return err
}
return env.Save()
}
// ExtractKernelAssets extract kernel assets per bootloader specifics
// lk bootloader requires boot partition to hold valid boot image
// there are two boot partition available, one holding current bootimage
// kernel assets are extracted to other (free) boot partition
// in case this function is called as part of image creation,
// boot image is extracted to the file
func (l *lk) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
blobName := s.Filename()
logger.Debugf("extracting kernel assets for %s with lk bootloader", s.SnapName())
env, err := l.newenv()
if err != nil {
return err
}
if err := env.Load(); err != nil {
// don't handle os.ErrNotExist specially here, it doesn't really make
// sense to extract kernel assets if we can't load the existing env,
// since then the caller would just see an error about not being able
// to find the kernel blob name (as they will all be empty in the env),
// when in reality the reason one can't find an available boot image
// partition is because we couldn't read the env file and so returning
// that error is better
return err
}
bootPartition, err := env.FindFreeKernelBootPartition(blobName)
if err != nil {
return err
}
if l.prepareImageTime {
// we are preparing image, just extract boot image to bootloader directory
if err := snapf.Unpack(env.GetBootImageName(), l.dir()); err != nil {
return fmt.Errorf("cannot open unpacked %s: %v", env.GetBootImageName(), err)
}
} else {
// this is live system, extracted bootimg needs to be flashed to
// free bootimg partition and env has to be updated with
// new kernel snap to bootimg partition mapping
tmpdir, err := os.MkdirTemp("", "bootimg")
if err != nil {
return fmt.Errorf("cannot create temp directory: %v", err)
}
defer os.RemoveAll(tmpdir)
bootImg := env.GetBootImageName()
if err := snapf.Unpack(bootImg, tmpdir); err != nil {
return fmt.Errorf("cannot unpack %s: %v", bootImg, err)
}
// write boot.img to free boot partition
bootimgName := filepath.Join(tmpdir, bootImg)
bif, err := os.Open(bootimgName)
if err != nil {
return fmt.Errorf("cannot open unpacked %s: %v", bootImg, err)
}
defer bif.Close()
var bpart string
// TODO: for RoleSole bootloaders this will eventually be the same
// codepath as for non-RoleSole bootloader
if l.role == RoleSole {
bpart = filepath.Join(l.dir(), bootPartition)
} else {
bpart, _, err = l.devPathForPartName(bootPartition)
if err != nil {
return err
}
}
bpf, err := os.OpenFile(bpart, os.O_WRONLY, 0660)
if err != nil {
return fmt.Errorf("cannot open boot partition [%s]: %v", bpart, err)
}
defer bpf.Close()
if _, err := io.Copy(bpf, bif); err != nil {
return err
}
}
if err := env.SetBootPartitionKernel(bootPartition, blobName); err != nil {
return err
}
return env.Save()
}
func (l *lk) RemoveKernelAssets(s snap.PlaceInfo) error {
blobName := s.Filename()
logger.Debugf("removing kernel assets for %s with lk bootloader", s.SnapName())
env, err := l.newenv()
if err != nil {
return err
}
if err := env.Load(); err != nil {
// don't handle os.ErrNotExist specially here, it doesn't really make
// sense to delete kernel assets if we can't load the existing env,
// since then the caller would just see an error about not being able
// to find the kernel blob name, when in reality the reason one can't
// find that kernel blob name is because we couldn't read the env file
return err
}
err = env.RemoveKernelFromBootPartition(blobName)
if err == nil {
// found and removed the revision from the bootimg matrix, need to
// update the env to persist the change
return env.Save()
}
return nil
}