Files
snapd/boot/boot.go
Valentin David 7e3b1bb83e boot: lock the modeenv while reading revisions
`ubuntu-core-20-64:tests/core/system-snap-refresh` fails because
of this.
2023-10-26 16:20:07 +01:00

581 lines
18 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2019-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 boot
import (
"errors"
"fmt"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/snap"
)
// Unlocker functions are passed from code using boot to indicate that global
// state should be unlocked during slow operations, e.g sealing/unsealing.
// Boot code is then expected to call the unlocker around the slow section and
// relock using the returned function. Unlocker being nil indicates not to do
// this.
type Unlocker func() (relock func())
const (
// DefaultStatus is the value of a status boot variable when nothing is
// being tried
DefaultStatus = ""
// TryStatus is the value of a status boot variable when something is about
// to be tried
TryStatus = "try"
// TryingStatus is the value of a status boot variable after we have
// attempted a boot with a try snap - this status is only set in the early
// boot sequence (bootloader, initramfs, etc.)
TryingStatus = "trying"
)
// RebootInfo contains information about how to perform a reboot if
// required.
type RebootInfo struct {
// RebootRequired is true if we need to reboot after an update.
RebootRequired bool
// BootloaderOptions will be used to find the correct bootloader when
// checking for any set reboot arguments.
BootloaderOptions *bootloader.Options
}
// NextBootContext carries additional significative information used when
// setting the next boot.
type NextBootContext struct {
// BootWithoutTry is sets if we don't want to use the "try" logic. This
// is useful if the next boot is part of an installation undo.
BootWithoutTry bool
}
// A BootParticipant handles the boot process details for a snap involved in it.
type BootParticipant interface {
// SetNextBoot will schedule the snap to be used in the next
// boot. bootCtx contains context information that influences how the
// next boot is performed. For base snaps it is up to the caller to
// select the right bootable base (from the model assertion). It is a
// noop for not relevant snaps. Otherwise it returns whether a reboot
// is required.
SetNextBoot(bootCtx NextBootContext) (rebootInfo RebootInfo, err error)
// Is this a trivial implementation of the interface?
IsTrivial() bool
}
// A BootKernel handles the bootloader setup of a kernel.
type BootKernel interface {
// RemoveKernelAssets removes the unpacked kernel/initrd for the given
// kernel snap.
RemoveKernelAssets() error
// ExtractKernelAssets extracts kernel/initrd/dtb data from the given
// kernel snap, if required, to a versioned bootloader directory so
// that the bootloader can use it.
ExtractKernelAssets(snap.Container) error
// Is this a trivial implementation of the interface?
IsTrivial() bool
}
type trivial struct{}
func (trivial) SetNextBoot(bootCtx NextBootContext) (RebootInfo, error) {
return RebootInfo{RebootRequired: false}, nil
}
func (trivial) IsTrivial() bool { return true }
func (trivial) RemoveKernelAssets() error { return nil }
func (trivial) ExtractKernelAssets(snap.Container) error { return nil }
// ensure trivial is a BootParticipant
var _ BootParticipant = trivial{}
// ensure trivial is a Kernel
var _ BootKernel = trivial{}
// Participant figures out what the BootParticipant is for the given
// arguments, and returns it. If the snap does _not_ participate in
// the boot process, the returned object will be a NOP, so it's safe
// to call anything on it always.
//
// Currently, on classic, nothing is a boot participant (returned will
// always be NOP).
func Participant(s snap.PlaceInfo, t snap.Type, dev snap.Device) BootParticipant {
if applicable(s, t, dev) {
bs, err := bootStateFor(t, dev)
if err != nil {
// all internal errors at this point
panic(err)
}
return &coreBootParticipant{s: s, bs: bs}
}
return trivial{}
}
// bootloaderOptionsForDeviceKernel returns a set of bootloader options that
// enable correct kernel extraction and removal for given device
func bootloaderOptionsForDeviceKernel(dev snap.Device) *bootloader.Options {
if !dev.HasModeenv() {
return nil
}
// find the run-mode bootloader with its kernel support for UC20
return &bootloader.Options{
Role: bootloader.RoleRunMode,
}
}
// Kernel checks that the given arguments refer to a kernel snap
// that participates in the boot process, and returns the associated
// BootKernel, or a trivial implementation otherwise.
func Kernel(s snap.PlaceInfo, t snap.Type, dev snap.Device) BootKernel {
if t == snap.TypeKernel && applicable(s, t, dev) {
return &coreKernel{s: s, bopts: bootloaderOptionsForDeviceKernel(dev)}
}
return trivial{}
}
// SnapTypeParticipatesInBoot returns whether a snap type participates in the
// boot for a given device.
func SnapTypeParticipatesInBoot(t snap.Type, dev snap.Device) bool {
if dev.IsClassicBoot() {
return false
}
switch t {
case snap.TypeBase, snap.TypeOS:
// Bases are not boot participants for classic with modes
return !dev.Classic()
case snap.TypeKernel, snap.TypeGadget:
return true
}
return false
}
func applicable(s snap.PlaceInfo, t snap.Type, dev snap.Device) bool {
if !SnapTypeParticipatesInBoot(t, dev) {
return false
}
// In ephemeral modes we never need to care about updating the boot
// config. This will be done via boot.MakeBootable().
if !dev.RunMode() {
return false
}
switch t {
case snap.TypeKernel:
if s.InstanceName() != dev.Kernel() {
// a remodel might leave behind installed a kernel that
// is not the device kernel anymore, ignore such a
// kernel by checking the name
return false
}
case snap.TypeBase, snap.TypeOS:
base := dev.Base()
if base == "" {
base = "core"
}
if s.InstanceName() != base {
return false
}
case snap.TypeGadget:
// First condition: gadget is not a boot participant for UC16/18
// Second condition: a remodel might leave behind installed a
// gadget that is not the device gadget anymore, ignore such a
// gadget by checking the name
if !dev.HasModeenv() || s.InstanceName() != dev.Gadget() {
return false
}
default:
return false
}
return true
}
// bootState exposes the boot state for a type of boot snap during
// normal running state, i.e. after the pivot_root and after the initramfs.
type bootState interface {
// revisions retrieves the revisions of the current snap and
// the try snap (only the latter might not be set), and
// the status of the trying snap.
// Note that the error could be only specific to the try snap, in which case
// curSnap may still be non-nil and valid. Callers concerned with robustness
// should always inspect a non-nil error with isTrySnapError, and use
// curSnap instead if the error is only for the trySnap or tryingStatus.
revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error)
// setNext lazily implements setting the next boot target for the type's
// boot snap. bootCtx specifies additional information bits we might
// need. Actually committing the update is done via the returned
// bootStateUpdate's commit method. It will return information for
// rebooting if necessary.
setNext(s snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error)
// markSuccessful lazily implements marking the boot
// successful for the type's boot snap. The actual committing
// of the update is done via bootStateUpdate's commit, that
// way different markSuccessful can be folded together.
markSuccessful(bootStateUpdate) (bootStateUpdate, error)
}
// successfulBootState exposes the state of resources requiring bookkeeping on a
// successful boot.
type successfulBootState interface {
// markSuccessful lazily implements marking the boot
// successful for the given type of resource.
markSuccessful(bootStateUpdate) (bootStateUpdate, error)
}
// bootStateFor finds the right bootState implementation of the given
// snap type and Device, if applicable.
func bootStateFor(typ snap.Type, dev snap.Device) (s bootState, err error) {
if !dev.RunMode() {
return nil, fmt.Errorf("internal error: no boot state handling for ephemeral modes")
}
if typ == snap.TypeOS {
typ = snap.TypeBase
}
newBootState := newBootState16
participantTypes := []snap.Type{snap.TypeBase, snap.TypeKernel}
if dev.HasModeenv() {
newBootState = newBootState20
participantTypes = append(participantTypes, snap.TypeGadget)
}
for _, partTyp := range participantTypes {
if typ == partTyp {
return newBootState(typ, dev), nil
}
}
return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ)
}
// InUseFunc is a function to check if the snap is in use or not.
type InUseFunc func(name string, rev snap.Revision) bool
func fixedInUse(inUse bool) InUseFunc {
return func(string, snap.Revision) bool {
return inUse
}
}
// InUse returns a checker for whether a given name/revision is used in the
// boot environment for snaps of the relevant snap type.
func InUse(typ snap.Type, dev snap.Device) (InUseFunc, error) {
modeenvLock()
defer modeenvUnlock()
if !dev.RunMode() {
// ephemeral mode, block manipulations for now
return fixedInUse(true), nil
}
if !SnapTypeParticipatesInBoot(typ, dev) || typ == snap.TypeGadget {
return fixedInUse(false), nil
}
cands := make([]snap.PlaceInfo, 0, 2)
s, err := bootStateFor(typ, dev)
if err != nil {
return nil, err
}
cand, tryCand, _, err := s.revisions()
if err != nil {
return nil, err
}
cands = append(cands, cand)
if tryCand != nil {
cands = append(cands, tryCand)
}
return func(name string, rev snap.Revision) bool {
for _, cand := range cands {
if cand.SnapName() == name && cand.SnapRevision() == rev {
return true
}
}
return false
}, nil
}
var (
// ErrBootNameAndRevisionNotReady is returned when the boot revision is not
// established yet.
ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established")
)
// GetCurrentBoot returns the currently set name and revision for boot for the given
// type of snap, which can be snap.TypeBase (or snap.TypeOS), or snap.TypeKernel.
// Returns ErrBootNameAndRevisionNotReady if the values are temporarily not established.
func GetCurrentBoot(t snap.Type, dev snap.Device) (snap.PlaceInfo, error) {
modeenvLock()
defer modeenvUnlock()
s, err := bootStateFor(t, dev)
if err != nil {
return nil, err
}
snap, _, status, err := s.revisions()
if err != nil {
return nil, err
}
if status == TryingStatus {
return nil, ErrBootNameAndRevisionNotReady
}
return snap, nil
}
// bootStateUpdate carries the state for an on-going boot state update.
// At the end it can be used to commit it.
type bootStateUpdate interface {
commit() error
}
// MarkBootSuccessful marks the current boot as successful. This means
// that snappy will consider this combination of kernel/os a valid
// target for rollback.
//
// The states that a boot goes through for UC16/18 are the following:
// - By default snap_mode is "" in which case the bootloader loads
// two squashfs'es denoted by variables snap_core and snap_kernel.
// - On a refresh of core/kernel snapd will set snap_mode=try and
// will also set snap_try_{core,kernel} to the core/kernel that
// will be tried next.
// - On reboot the bootloader will inspect the snap_mode and if the
// mode is set to "try" it will set "snap_mode=trying" and then
// try to boot the snap_try_{core,kernel}".
// - On a successful boot snapd resets snap_mode to "" and copies
// snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
// values are cleared afterwards.
// - On a failing boot the bootloader will see snap_mode=trying which
// means snapd did not start successfully. In this case the bootloader
// will set snap_mode="" and the system will boot with the known good
// values from snap_{core,kernel}
func MarkBootSuccessful(dev snap.Device) error {
modeenvLock()
defer modeenvUnlock()
const errPrefix = "cannot mark boot successful: %s"
var u bootStateUpdate
for _, t := range []snap.Type{snap.TypeBase, snap.TypeKernel} {
if !SnapTypeParticipatesInBoot(t, dev) {
continue
}
s, err := bootStateFor(t, dev)
if err != nil {
return err
}
u, err = s.markSuccessful(u)
if err != nil {
return fmt.Errorf(errPrefix, err)
}
}
if dev.HasModeenv() {
for _, bs := range []successfulBootState{
trustedAssetsBootState(dev),
trustedCommandLineBootState(dev),
recoverySystemsBootState(dev),
modelBootState(dev),
} {
var err error
u, err = bs.markSuccessful(u)
if err != nil {
return fmt.Errorf(errPrefix, err)
}
}
}
if u != nil {
if err := u.commit(); err != nil {
return fmt.Errorf(errPrefix, err)
}
}
return nil
}
var ErrUnsupportedSystemMode = errors.New("system mode is unsupported")
// SetRecoveryBootSystemAndMode configures the recovery bootloader to boot into
// the given recovery system in a particular mode. Returns
// ErrUnsupportedSystemMode when booting into a recovery system is not supported
// by the device.
func SetRecoveryBootSystemAndMode(dev snap.Device, systemLabel, mode string) error {
if !dev.HasModeenv() {
// only UC20 devices are supported
return ErrUnsupportedSystemMode
}
if systemLabel == "" {
return fmt.Errorf("internal error: system label is unset")
}
if mode == "" {
return fmt.Errorf("internal error: system mode is unset")
}
opts := &bootloader.Options{
// setup the recovery bootloader
Role: bootloader.RoleRecovery,
}
// TODO:UC20: should the recovery partition stay around as RW during run
// mode all the time?
bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
if err != nil {
return err
}
m := map[string]string{
"snapd_recovery_system": systemLabel,
"snapd_recovery_mode": mode,
}
return bl.SetBootVars(m)
}
// UpdateManagedBootConfigs updates managed boot config assets if
// those are present for the ubuntu-boot bootloader. To do this it
// needs information from the model, the gadget we are updating to,
// and any additional kernel command line arguments coming from system
// options. Returns true when an update was carried out.
func UpdateManagedBootConfigs(dev snap.Device, gadgetSnapOrDir, cmdlineAppend string) (updated bool, err error) {
if !dev.HasModeenv() {
// only UC20 devices use managed boot config
return false, nil
}
if !dev.RunMode() {
return false, fmt.Errorf("internal error: boot config can only be updated in run mode")
}
modeenvLock()
defer modeenvUnlock()
return updateManagedBootConfigForBootloader(dev, ModeRun, gadgetSnapOrDir, cmdlineAppend)
}
func updateCmdlineVars(tbl bootloader.TrustedAssetsBootloader, gadgetSnapOrDir, cmdlineAppend string, candidate bool, dev snap.Device) error {
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
if err != nil {
return err
}
cmdlineVars, err := bootVarsForTrustedCommandLineFromGadget(gadgetSnapOrDir, cmdlineAppend, defaultCmdLine, dev.Model())
if err != nil {
return fmt.Errorf("cannot prepare bootloader variables for kernel command line: %v", err)
}
if err := tbl.SetBootVars(cmdlineVars); err != nil {
return fmt.Errorf("cannot set run system kernel command line arguments: %v", err)
}
return nil
}
func updateManagedBootConfigForBootloader(dev snap.Device, mode, gadgetSnapOrDir, cmdlineAppend string) (updated bool, err error) {
if mode != ModeRun {
return false, fmt.Errorf("internal error: updating boot config of recovery bootloader is not supported yet")
}
opts := &bootloader.Options{
Role: bootloader.RoleRunMode,
NoSlashBoot: true,
}
tbl, err := getBootloaderManagingItsAssets(InitramfsUbuntuBootDir, opts)
if err != nil {
if err == errBootConfigNotManaged {
// we're not managing this bootloader's boot config
return false, nil
}
return false, err
}
// boot config update can lead to a change of kernel command line
cmdlineChange, err := observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonSnapd, gadgetSnapOrDir, cmdlineAppend)
if err != nil {
return false, err
}
if cmdlineChange {
candidate := true
if err := updateCmdlineVars(tbl, gadgetSnapOrDir, cmdlineAppend, candidate, dev); err != nil {
return false, err
}
}
assetChange, err := tbl.UpdateBootConfig()
if err != nil {
return false, err
}
return assetChange || cmdlineChange, nil
}
// UpdateCommandLineForGadgetComponent handles the update of a gadget
// that contributes to the kernel command line of the run system
// (appending any additional kernel command line arguments coming from
// system options). Returns true when a change in command line has
// been observed and a reboot is needed. The reboot, if needed, should
// be requested at the the earliest possible occasion.
func UpdateCommandLineForGadgetComponent(dev snap.Device, gadgetSnapOrDir, cmdlineAppend string) (needsReboot bool, err error) {
if !dev.HasModeenv() {
// only UC20 devices are supported
return false, fmt.Errorf("internal error: command line component cannot be updated on pre-UC20 devices")
}
modeenvLock()
defer modeenvUnlock()
opts := &bootloader.Options{
Role: bootloader.RoleRunMode,
}
// TODO: add support for bootloaders that that do not have any managed
// assets
tbl, err := getBootloaderManagingItsAssets("", opts)
if err != nil {
if err == errBootConfigNotManaged {
// we're not managing this bootloader's boot config
return false, nil
}
return false, err
}
// gadget update can lead to a change of kernel command line
cmdlineChange, err := observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonGadget, gadgetSnapOrDir, cmdlineAppend)
if err != nil {
return false, err
}
if !cmdlineChange {
return false, nil
}
candidate := false
if err := updateCmdlineVars(tbl, gadgetSnapOrDir, cmdlineAppend, candidate, dev); err != nil {
return false, err
}
return cmdlineChange, nil
}
// MarkFactoryResetComplete runs a series of steps in a run system that complete a
// factory reset process.
func MarkFactoryResetComplete(encrypted bool) error {
if !encrypted {
// there is nothing to do on an unencrypted system
return nil
}
if err := postFactoryResetCleanup(); err != nil {
return fmt.Errorf("cannot perform post factory reset boot cleanup: %v", err)
}
return nil
}