1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
|
// -*- 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/osutil"
"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(markedSuccesful bool) 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 {
const markedSuccessful = true
if err := u.commit(markedSuccessful); 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
}
osutil.MaybeInjectFault("update-config-bootloader")
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
}
osutil.MaybeInjectFault("update-command-line-gadget")
candidate := false
if err := updateCmdlineVars(tbl, gadgetSnapOrDir, cmdlineAppend, candidate, dev); err != nil {
return false, err
}
return cmdlineChange, nil
}
|