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