github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/bootloader/grub.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 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 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/snapcore/snapd/bootloader/assets" 29 "github.com/snapcore/snapd/bootloader/grubenv" 30 "github.com/snapcore/snapd/osutil" 31 "github.com/snapcore/snapd/snap" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 // sanity - grub implements the required interfaces 36 var ( 37 _ Bootloader = (*grub)(nil) 38 _ installableBootloader = (*grub)(nil) 39 _ RecoveryAwareBootloader = (*grub)(nil) 40 _ ExtractedRunKernelImageBootloader = (*grub)(nil) 41 _ TrustedAssetsBootloader = (*grub)(nil) 42 ) 43 44 type grub struct { 45 rootdir string 46 47 basedir string 48 49 uefiRunKernelExtraction bool 50 recovery bool 51 nativePartitionLayout bool 52 } 53 54 // newGrub create a new Grub bootloader object 55 func newGrub(rootdir string, opts *Options) Bootloader { 56 g := &grub{rootdir: rootdir} 57 if opts != nil { 58 // Set the flag to extract the run kernel, only 59 // for UC20 run mode. 60 // Both UC16/18 and the recovery mode of UC20 load 61 // the kernel directly from snaps. 62 g.uefiRunKernelExtraction = opts.Role == RoleRunMode 63 g.recovery = opts.Role == RoleRecovery 64 g.nativePartitionLayout = opts.NoSlashBoot || g.recovery 65 } 66 if g.nativePartitionLayout { 67 g.basedir = "EFI/ubuntu" 68 } else { 69 g.basedir = "boot/grub" 70 } 71 72 return g 73 } 74 75 func (g *grub) Name() string { 76 return "grub" 77 } 78 79 func (g *grub) setRootDir(rootdir string) { 80 g.rootdir = rootdir 81 } 82 83 func (g *grub) dir() string { 84 if g.rootdir == "" { 85 panic("internal error: unset rootdir") 86 } 87 return filepath.Join(g.rootdir, g.basedir) 88 } 89 90 func (g *grub) installManagedRecoveryBootConfig(gadgetDir string) (bool, error) { 91 gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf") 92 if !osutil.FileExists(gadgetGrubCfg) { 93 // gadget does not use grub bootloader 94 return false, nil 95 } 96 assetName := g.Name() + "-recovery.cfg" 97 systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") 98 return genericSetBootConfigFromAsset(systemFile, assetName) 99 } 100 101 func (g *grub) installManagedBootConfig(gadgetDir string) (bool, error) { 102 gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf") 103 if !osutil.FileExists(gadgetGrubCfg) { 104 // gadget does not use grub bootloader 105 return false, nil 106 } 107 assetName := g.Name() + ".cfg" 108 systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") 109 return genericSetBootConfigFromAsset(systemFile, assetName) 110 } 111 112 func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { 113 if opts != nil && opts.Role == RoleRecovery { 114 // install managed config for the recovery partition 115 return g.installManagedRecoveryBootConfig(gadgetDir) 116 } 117 if opts != nil && opts.Role == RoleRunMode { 118 // install managed boot config that can handle kernel.efi 119 return g.installManagedBootConfig(gadgetDir) 120 } 121 122 gadgetFile := filepath.Join(gadgetDir, g.Name()+".conf") 123 systemFile := filepath.Join(g.rootdir, "/boot/grub/grub.cfg") 124 return genericInstallBootConfig(gadgetFile, systemFile) 125 } 126 127 func (g *grub) SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error { 128 if recoverySystemDir == "" { 129 return fmt.Errorf("internal error: recoverySystemDir unset") 130 } 131 recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv") 132 if err := os.MkdirAll(filepath.Dir(recoverySystemGrubEnv), 0755); err != nil { 133 return err 134 } 135 genv := grubenv.NewEnv(recoverySystemGrubEnv) 136 for k, v := range values { 137 genv.Set(k, v) 138 } 139 return genv.Save() 140 } 141 142 func (g *grub) GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) { 143 if recoverySystemDir == "" { 144 return "", fmt.Errorf("internal error: recoverySystemDir unset") 145 } 146 recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv") 147 genv := grubenv.NewEnv(recoverySystemGrubEnv) 148 if err := genv.Load(); err != nil { 149 if os.IsNotExist(err) { 150 return "", nil 151 } 152 return "", err 153 } 154 return genv.Get(key), nil 155 } 156 157 func (g *grub) ConfigFile() string { 158 return filepath.Join(g.dir(), "grub.cfg") 159 } 160 161 func (g *grub) envFile() string { 162 return filepath.Join(g.dir(), "grubenv") 163 } 164 165 func (g *grub) GetBootVars(names ...string) (map[string]string, error) { 166 out := make(map[string]string) 167 168 env := grubenv.NewEnv(g.envFile()) 169 if err := env.Load(); err != nil { 170 return nil, err 171 } 172 173 for _, name := range names { 174 out[name] = env.Get(name) 175 } 176 177 return out, nil 178 } 179 180 func (g *grub) SetBootVars(values map[string]string) error { 181 env := grubenv.NewEnv(g.envFile()) 182 if err := env.Load(); err != nil && !os.IsNotExist(err) { 183 return err 184 } 185 for k, v := range values { 186 env.Set(k, v) 187 } 188 return env.Save() 189 } 190 191 func (g *grub) extractedKernelDir(prefix string, s snap.PlaceInfo) string { 192 return filepath.Join( 193 prefix, 194 s.Filename(), 195 ) 196 } 197 198 func (g *grub) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { 199 // default kernel assets are: 200 // - kernel.img 201 // - initrd.img 202 // - dtbs/* 203 var assets []string 204 if g.uefiRunKernelExtraction { 205 assets = []string{"kernel.efi"} 206 } else { 207 assets = []string{"kernel.img", "initrd.img", "dtbs/*"} 208 } 209 210 // extraction can be forced through either a special file in the kernel snap 211 // or through an option in the bootloader 212 _, err := snapf.ReadFile("meta/force-kernel-extraction") 213 if g.uefiRunKernelExtraction || err == nil { 214 return extractKernelAssetsToBootDir( 215 g.extractedKernelDir(g.dir(), s), 216 snapf, 217 assets, 218 ) 219 } 220 return nil 221 } 222 223 func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error { 224 return removeKernelAssetsFromBootDir(g.dir(), s) 225 } 226 227 // ExtractedRunKernelImageBootloader helper methods 228 229 func (g *grub) makeKernelEfiSymlink(s snap.PlaceInfo, name string) error { 230 // use a relative symlink destination so that it resolves properly, if grub 231 // is located at /run/mnt/ubuntu-boot or /boot/grub, etc. 232 target := filepath.Join( 233 s.Filename(), 234 "kernel.efi", 235 ) 236 237 // the location of the destination symlink as an absolute filepath 238 source := filepath.Join(g.dir(), name) 239 240 // check that the kernel snap has been extracted already so we don't 241 // inadvertently create a dangling symlink 242 // expand the relative symlink from g.dir() 243 if !osutil.FileExists(filepath.Join(g.dir(), target)) { 244 return fmt.Errorf( 245 "cannot enable %s at %s: %v", 246 name, 247 target, 248 os.ErrNotExist, 249 ) 250 } 251 252 // the symlink doesn't exist so just create it 253 return osutil.AtomicSymlink(target, source) 254 } 255 256 // unlinkKernelEfiSymlink will remove the specified symlink if it exists. Note 257 // that if the symlink is "dangling", it will still remove the symlink without 258 // returning an error. This is useful for example to disable a try-kernel that 259 // was incorrectly created. 260 func (g *grub) unlinkKernelEfiSymlink(name string) error { 261 symlink := filepath.Join(g.dir(), name) 262 err := os.Remove(symlink) 263 if err != nil && !os.IsNotExist(err) { 264 return err 265 } 266 return nil 267 } 268 269 func (g *grub) readKernelSymlink(name string) (snap.PlaceInfo, error) { 270 // read the symlink from <grub-dir>/<name> to 271 // <grub-dir>/<snap-file-name>/<name> and parse the 272 // directory (which is supposed to be the name of the snap) into the snap 273 link := filepath.Join(g.dir(), name) 274 275 // check that the symlink is not dangling before continuing 276 if !osutil.FileExists(link) { 277 return nil, fmt.Errorf("cannot read dangling symlink %s", name) 278 } 279 280 targetKernelEfi, err := os.Readlink(link) 281 if err != nil { 282 return nil, fmt.Errorf("cannot read %s symlink: %v", link, err) 283 } 284 285 kernelSnapFileName := filepath.Base(filepath.Dir(targetKernelEfi)) 286 sn, err := snap.ParsePlaceInfoFromSnapFileName(kernelSnapFileName) 287 if err != nil { 288 return nil, fmt.Errorf( 289 "cannot parse kernel snap file name from symlink target %q: %v", 290 kernelSnapFileName, 291 err, 292 ) 293 } 294 return sn, nil 295 } 296 297 // actual ExtractedRunKernelImageBootloader methods 298 299 // EnableKernel will install a kernel.efi symlink in the bootloader partition, 300 // pointing to the referenced kernel snap. EnableKernel() will fail if the 301 // referenced kernel snap does not exist. 302 func (g *grub) EnableKernel(s snap.PlaceInfo) error { 303 // add symlink from ubuntuBootPartition/kernel.efi to 304 // <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi 305 // so that we are consistent between uc16/uc18 and uc20 with where we 306 // extract kernels 307 return g.makeKernelEfiSymlink(s, "kernel.efi") 308 } 309 310 // EnableTryKernel will install a try-kernel.efi symlink in the bootloader 311 // partition, pointing towards the referenced kernel snap. EnableTryKernel() 312 // will fail if the referenced kernel snap does not exist. 313 func (g *grub) EnableTryKernel(s snap.PlaceInfo) error { 314 // add symlink from ubuntuBootPartition/kernel.efi to 315 // <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi 316 // so that we are consistent between uc16/uc18 and uc20 with where we 317 // extract kernels 318 return g.makeKernelEfiSymlink(s, "try-kernel.efi") 319 } 320 321 // DisableTryKernel will remove the try-kernel.efi symlink if it exists. Note 322 // that when performing an update, you should probably first use EnableKernel(), 323 // then DisableTryKernel() for maximum safety. 324 func (g *grub) DisableTryKernel() error { 325 return g.unlinkKernelEfiSymlink("try-kernel.efi") 326 } 327 328 // Kernel will return the kernel snap currently installed in the bootloader 329 // partition, pointed to by the kernel.efi symlink. 330 func (g *grub) Kernel() (snap.PlaceInfo, error) { 331 return g.readKernelSymlink("kernel.efi") 332 } 333 334 // TryKernel will return the kernel snap currently being tried if it exists and 335 // false if there is not currently a try-kernel.efi symlink. Note if the symlink 336 // exists but does not point to an existing file an error will be returned. 337 func (g *grub) TryKernel() (snap.PlaceInfo, error) { 338 // check that the _symlink_ exists, not that it points to something real 339 // we check for whether it is a dangling symlink inside readKernelSymlink, 340 // which returns an error when the symlink is dangling 341 _, err := os.Lstat(filepath.Join(g.dir(), "try-kernel.efi")) 342 if err == nil { 343 p, err := g.readKernelSymlink("try-kernel.efi") 344 // if we failed to read the symlink, then the try kernel isn't usable, 345 // so return err because the symlink is there 346 if err != nil { 347 return nil, err 348 } 349 return p, nil 350 } 351 return nil, ErrNoTryKernelRef 352 } 353 354 // UpdateBootConfig updates the grub boot config only if it is already managed 355 // and has a lower edition. 356 // 357 // Implements ManagedAssetsBootloader for the grub bootloader. 358 func (g *grub) UpdateBootConfig(opts *Options) error { 359 // XXX: do we need to take opts here? 360 bootScriptName := "grub.cfg" 361 currentBootConfig := filepath.Join(g.dir(), "grub.cfg") 362 if opts != nil && opts.Role == RoleRecovery { 363 // use the recovery asset when asked to do so 364 bootScriptName = "grub-recovery.cfg" 365 } 366 return genericUpdateBootConfigFromAssets(currentBootConfig, bootScriptName) 367 } 368 369 // ManagedAssets returns a list relative paths to boot assets inside the root 370 // directory of the filesystem. 371 // 372 // Implements ManagedAssetsBootloader for the grub bootloader. 373 func (g *grub) ManagedAssets() []string { 374 return []string{ 375 filepath.Join(g.basedir, "grub.cfg"), 376 } 377 } 378 379 func (g *grub) commandLineForEdition(edition uint, modeArg, systemArg, extraArgs string) (string, error) { 380 assetName := "grub.cfg" 381 if g.recovery { 382 assetName = "grub-recovery.cfg" 383 } 384 staticCmdline := staticCommandLineForGrubAssetEdition(assetName, edition) 385 args, err := strutil.KernelCommandLineSplit(staticCmdline + " " + extraArgs) 386 if err != nil { 387 return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err) 388 } 389 // join all argument with a single space, see 390 // grub-core/lib/cmdline.c:grub_create_loader_cmdline() for reference, 391 // arguments are separated by a single space, the space after last is 392 // replaced with terminating NULL 393 snapdArgs := make([]string, 0, 2) 394 if modeArg != "" { 395 snapdArgs = append(snapdArgs, modeArg) 396 } 397 if systemArg != "" { 398 snapdArgs = append(snapdArgs, systemArg) 399 } 400 return strings.Join(append(snapdArgs, args...), " "), nil 401 } 402 403 // CommandLine returns the kernel command line composed of mode and 404 // system arguments, built-in bootloader specific static arguments 405 // corresponding to the on-disk boot asset edition, followed by any 406 // extra arguments. The command line may be different when using a 407 // recovery bootloader. 408 // 409 // Implements ManagedAssetsBootloader for the grub bootloader. 410 func (g *grub) CommandLine(modeArg, systemArg, extraArgs string) (string, error) { 411 currentBootConfig := filepath.Join(g.dir(), "grub.cfg") 412 edition, err := editionFromDiskConfigAsset(currentBootConfig) 413 if err != nil { 414 if err != errNoEdition { 415 return "", fmt.Errorf("cannot obtain edition number of current boot config: %v", err) 416 } 417 // we were called using the ManagedAssetsBootloader interface 418 // meaning the caller expects to us to use the managed assets, 419 // since one on disk is not managed, use the initial edition of 420 // the internal boot asset which is compatible with grub.cfg 421 // used before we started writing out the files ourselves 422 edition = 1 423 } 424 return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs) 425 } 426 427 // CandidateCommandLine is similar to CommandLine, but uses the current 428 // edition of managed built-in boot assets as reference. 429 // 430 // Implements ManagedAssetsBootloader for the grub bootloader. 431 func (g *grub) CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) { 432 assetName := "grub.cfg" 433 if g.recovery { 434 assetName = "grub-recovery.cfg" 435 } 436 edition, err := editionFromInternalConfigAsset(assetName) 437 if err != nil { 438 return "", err 439 } 440 return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs) 441 } 442 443 // staticCommandLineForGrubAssetEdition fetches a static command line for given 444 // grub asset edition 445 func staticCommandLineForGrubAssetEdition(asset string, edition uint) string { 446 cmdline := assets.SnippetForEdition(fmt.Sprintf("%s:static-cmdline", asset), edition) 447 if cmdline == nil { 448 return "" 449 } 450 return string(cmdline) 451 } 452 453 var ( 454 grubRecoveryModeTrustedAssets = []string{ 455 // recovery mode shim EFI binary 456 "EFI/boot/bootx64.efi", 457 // recovery mode grub EFI binary 458 "EFI/boot/grubx64.efi", 459 } 460 461 grubRunModeTrustedAssets = []string{ 462 // run mode grub EFI binary 463 "EFI/boot/grubx64.efi", 464 } 465 ) 466 467 // TrustedAssets returns the list of relative paths to assets inside 468 // the bootloader's rootdir that are measured in the boot process in the 469 // order of loading during the boot. 470 func (g *grub) TrustedAssets() ([]string, error) { 471 if !g.nativePartitionLayout { 472 return nil, fmt.Errorf("internal error: trusted assets called without native host-partition layout") 473 } 474 if g.recovery { 475 return grubRecoveryModeTrustedAssets, nil 476 } 477 return grubRunModeTrustedAssets, nil 478 } 479 480 // RecoveryBootChain returns the load chain for recovery modes. 481 // It should be called on a RoleRecovery bootloader. 482 func (g *grub) RecoveryBootChain(kernelPath string) ([]BootFile, error) { 483 if !g.recovery { 484 return nil, fmt.Errorf("not a recovery bootloader") 485 } 486 487 // add trusted assets to the recovery chain 488 chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+1) 489 for _, ta := range grubRecoveryModeTrustedAssets { 490 chain = append(chain, NewBootFile("", ta, RoleRecovery)) 491 } 492 // add recovery kernel to the recovery chain 493 chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRecovery)) 494 495 return chain, nil 496 } 497 498 // BootChain returns the load chain for run mode. 499 // It should be called on a RoleRecovery bootloader passing the 500 // RoleRunMode bootloader. 501 func (g *grub) BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) { 502 if !g.recovery { 503 return nil, fmt.Errorf("not a recovery bootloader") 504 } 505 if runBl.Name() != "grub" { 506 return nil, fmt.Errorf("run mode bootloader must be grub") 507 } 508 509 // add trusted assets to the recovery chain 510 chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+len(grubRunModeTrustedAssets)+1) 511 for _, ta := range grubRecoveryModeTrustedAssets { 512 chain = append(chain, NewBootFile("", ta, RoleRecovery)) 513 } 514 for _, ta := range grubRunModeTrustedAssets { 515 chain = append(chain, NewBootFile("", ta, RoleRunMode)) 516 } 517 // add kernel to the boot chain 518 chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRunMode)) 519 520 return chain, nil 521 }