github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/bootloader/bootloader.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 "errors" 24 "fmt" 25 "os" 26 "path/filepath" 27 28 "github.com/snapcore/snapd/bootloader/assets" 29 "github.com/snapcore/snapd/dirs" 30 "github.com/snapcore/snapd/osutil" 31 "github.com/snapcore/snapd/snap" 32 ) 33 34 var ( 35 // ErrBootloader is returned if the bootloader can not be determined. 36 ErrBootloader = errors.New("cannot determine bootloader") 37 38 // ErrNoTryKernelRef is returned if the bootloader finds no enabled 39 // try-kernel. 40 ErrNoTryKernelRef = errors.New("no try-kernel referenced") 41 ) 42 43 // Role indicates whether the bootloader is used for recovery or run mode. 44 type Role string 45 46 const ( 47 // RoleSole applies to the sole bootloader used by UC16/18. 48 RoleSole Role = "" 49 // RoleRunMode applies to the run mode booloader. 50 RoleRunMode Role = "run-mode" 51 // RoleRecovery apllies to the recovery bootloader. 52 RoleRecovery Role = "recovery" 53 ) 54 55 // Options carries bootloader options. 56 type Options struct { 57 // PrepareImageTime indicates whether the booloader is being 58 // used at prepare-image time, that means not on a runtime 59 // system. 60 PrepareImageTime bool 61 62 // Role specifies to use the bootloader for the given role. 63 Role Role 64 65 // NoSlashBoot indicates to use the native layout of the 66 // bootloader partition and not the /boot mount. 67 // It applies only for RoleRunMode. 68 // It is implied and ignored for RoleRecovery. 69 // It is an error to set it for RoleSole. 70 NoSlashBoot bool 71 } 72 73 func (o *Options) validate() error { 74 if o == nil { 75 return nil 76 } 77 if o.NoSlashBoot && o.Role == RoleSole { 78 return fmt.Errorf("internal error: bootloader.RoleSole doesn't expect NoSlashBoot set") 79 } 80 if o.PrepareImageTime && o.Role == RoleRunMode { 81 return fmt.Errorf("internal error: cannot use run mode bootloader at prepare-image time") 82 } 83 return nil 84 } 85 86 // Bootloader provides an interface to interact with the system 87 // bootloader. 88 type Bootloader interface { 89 // Return the value of the specified bootloader variable. 90 GetBootVars(names ...string) (map[string]string, error) 91 92 // Set the value of the specified bootloader variable. 93 SetBootVars(values map[string]string) error 94 95 // Name returns the bootloader name. 96 Name() string 97 98 // Present returns whether the bootloader is currently present on the 99 // system - in other words whether this bootloader has been installed to the 100 // current system. Implementations should only return non-nil error if they 101 // can positively identify that the bootloader is installed, but there is 102 // actually an error with the installation. 103 Present() (bool, error) 104 105 // InstallBootConfig will try to install the boot config in the 106 // given gadgetDir to rootdir. 107 InstallBootConfig(gadgetDir string, opts *Options) error 108 109 // ExtractKernelAssets extracts kernel assets from the given kernel snap. 110 ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error 111 112 // RemoveKernelAssets removes the assets for the given kernel snap. 113 RemoveKernelAssets(s snap.PlaceInfo) error 114 } 115 116 type RecoveryAwareBootloader interface { 117 Bootloader 118 SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error 119 GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) 120 } 121 122 type ExtractedRecoveryKernelImageBootloader interface { 123 Bootloader 124 ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error 125 } 126 127 // ExtractedRunKernelImageBootloader is a Bootloader that also supports specific 128 // methods needed to setup booting from an extracted kernel, which is needed to 129 // implement encryption and/or secure boot. Prototypical implementation is UC20 130 // grub implementation with FDE. 131 type ExtractedRunKernelImageBootloader interface { 132 Bootloader 133 134 // EnableKernel enables the specified kernel on ubuntu-boot to be used 135 // during normal boots. The specified kernel should already have been 136 // extracted. This is usually implemented with a "kernel.efi" symlink 137 // pointing to the extracted kernel image. 138 EnableKernel(snap.PlaceInfo) error 139 140 // EnableTryKernel enables the specified kernel on ubuntu-boot to be 141 // tried by the bootloader on a reboot, to be used in conjunction with 142 // setting "kernel_status" to "try". The specified kernel should already 143 // have been extracted. This is usually implemented with a 144 // "try-kernel.efi" symlink pointing to the extracted kernel image. 145 EnableTryKernel(snap.PlaceInfo) error 146 147 // Kernel returns the current enabled kernel on the bootloader, not 148 // necessarily the kernel that was used to boot the current session, but the 149 // kernel that is enabled to boot on "normal" boots. 150 // If error is not nil, the first argument shall be non-nil. 151 Kernel() (snap.PlaceInfo, error) 152 153 // TryKernel returns the current enabled try-kernel on the bootloader, if 154 // there is no such enabled try-kernel, then ErrNoTryKernelRef is returned. 155 // If error is not nil, the first argument shall be non-nil. 156 TryKernel() (snap.PlaceInfo, error) 157 158 // DisableTryKernel disables the current enabled try-kernel on the 159 // bootloader, if it exists. It does not need to return an error if the 160 // enabled try-kernel does not exist or is in an inconsistent state before 161 // disabling it, errors should only be returned when the implementation 162 // fails to disable the try-kernel. 163 DisableTryKernel() error 164 } 165 166 // ComamndLineComponents carries the components of the kernel command line. The 167 // bootloader is expected to combine the provided components, optionally 168 // including its built-in static set of arguments, and produce a command line 169 // that will be passed to the kernel during boot. 170 type CommandLineComponents struct { 171 // Argument related to mode selection. 172 ModeArg string 173 // Argument related to recovery system selection, relevant for given 174 // mode argument. 175 SystemArg string 176 // Extra arguments requested by the system. 177 ExtraArgs string 178 // A complete set of arguments that overrides both the built-in static 179 // set and ExtraArgs. Note that, it is an error if extra and full 180 // arguments are non-empty. 181 FullArgs string 182 } 183 184 func (c *CommandLineComponents) Validate() error { 185 if c.ExtraArgs != "" && c.FullArgs != "" { 186 return fmt.Errorf("cannot use both full and extra components of command line") 187 } 188 return nil 189 } 190 191 // TrustedAssetsBootloader has boot assets that take part in the secure boot 192 // process and need to be tracked, while other boot assets (typically boot 193 // config) are managed by snapd. 194 type TrustedAssetsBootloader interface { 195 Bootloader 196 197 // ManagedAssets returns a list of boot assets managed by the bootloader 198 // in the boot filesystem. Does not require rootdir to be set. 199 ManagedAssets() []string 200 // UpdateBootConfig attempts to update the boot config assets used by 201 // the bootloader. Returns true when assets were updated. 202 UpdateBootConfig() (bool, error) 203 // CommandLine returns the kernel command line composed of mode and 204 // system arguments, followed by either a built-in bootloader specific 205 // static arguments corresponding to the on-disk boot asset edition, and 206 // any extra arguments or a separate set of arguments provided in the 207 // components. The command line may be different when using a recovery 208 // bootloader. 209 CommandLine(pieces CommandLineComponents) (string, error) 210 // CandidateCommandLine is similar to CommandLine, but uses the current 211 // edition of managed built-in boot assets as reference. 212 CandidateCommandLine(pieces CommandLineComponents) (string, error) 213 214 // TrustedAssets returns the list of relative paths to assets inside the 215 // bootloader's rootdir that are measured in the boot process in the 216 // order of loading during the boot. Does not require rootdir to be set. 217 TrustedAssets() ([]string, error) 218 219 // RecoveryBootChain returns the load chain for recovery modes. 220 // It should be called on a RoleRecovery bootloader. 221 RecoveryBootChain(kernelPath string) ([]BootFile, error) 222 223 // BootChain returns the load chain for run mode. 224 // It should be called on a RoleRecovery bootloader passing the 225 // RoleRunMode bootloader. 226 BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) 227 } 228 229 func genericInstallBootConfig(gadgetFile, systemFile string) error { 230 if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { 231 return err 232 } 233 return osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) 234 } 235 236 func genericSetBootConfigFromAsset(systemFile, assetName string) error { 237 bootConfig := assets.Internal(assetName) 238 if bootConfig == nil { 239 return fmt.Errorf("internal error: no boot asset for %q", assetName) 240 } 241 if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { 242 return err 243 } 244 return osutil.AtomicWriteFile(systemFile, bootConfig, 0644, 0) 245 } 246 247 func genericUpdateBootConfigFromAssets(systemFile string, assetName string) (updated bool, err error) { 248 currentBootConfigEdition, err := editionFromDiskConfigAsset(systemFile) 249 if err != nil && err != errNoEdition { 250 return false, err 251 } 252 if err == errNoEdition { 253 return false, nil 254 } 255 newBootConfig := assets.Internal(assetName) 256 if len(newBootConfig) == 0 { 257 return false, fmt.Errorf("no boot config asset with name %q", assetName) 258 } 259 bc, err := configAssetFrom(newBootConfig) 260 if err != nil { 261 return false, err 262 } 263 if bc.Edition() <= currentBootConfigEdition { 264 // edition of the candidate boot config is lower than or equal 265 // to one currently installed 266 return false, nil 267 } 268 if err := osutil.AtomicWriteFile(systemFile, bc.Raw(), 0644, 0); err != nil { 269 return false, err 270 } 271 return true, nil 272 } 273 274 // InstallBootConfig installs the bootloader config from the gadget 275 // snap dir into the right place. 276 func InstallBootConfig(gadgetDir, rootDir string, opts *Options) error { 277 if err := opts.validate(); err != nil { 278 return err 279 } 280 bl, err := ForGadget(gadgetDir, rootDir, opts) 281 if err != nil { 282 return fmt.Errorf("cannot find boot config in %q", gadgetDir) 283 } 284 return bl.InstallBootConfig(gadgetDir, opts) 285 } 286 287 type bootloaderNewFunc func(rootdir string, opts *Options) Bootloader 288 289 var ( 290 // bootloaders list all possible bootloaders by their constructor 291 // function. 292 bootloaders = []bootloaderNewFunc{ 293 newUboot, 294 newGrub, 295 newAndroidBoot, 296 newLk, 297 } 298 ) 299 300 var ( 301 forcedBootloader Bootloader 302 forcedError error 303 ) 304 305 // Find returns the bootloader for the system 306 // or an error if no bootloader is found. 307 // 308 // The rootdir option is useful for image creation operations. It 309 // can also be used to find the recovery bootloader, e.g. on uc20: 310 // bootloader.Find("/run/mnt/ubuntu-seed") 311 func Find(rootdir string, opts *Options) (Bootloader, error) { 312 if err := opts.validate(); err != nil { 313 return nil, err 314 } 315 if forcedBootloader != nil || forcedError != nil { 316 return forcedBootloader, forcedError 317 } 318 319 if rootdir == "" { 320 rootdir = dirs.GlobalRootDir 321 } 322 if opts == nil { 323 opts = &Options{} 324 } 325 326 // note that the order of this is not deterministic 327 for _, blNew := range bootloaders { 328 bl := blNew(rootdir, opts) 329 present, err := bl.Present() 330 if err != nil { 331 return nil, fmt.Errorf("bootloader %q found but not usable: %v", bl.Name(), err) 332 } 333 if present { 334 return bl, nil 335 } 336 } 337 // no, weeeee 338 return nil, ErrBootloader 339 } 340 341 // Force can be used to force Find to always find the specified bootloader; use 342 // nil to reset to normal lookup. 343 func Force(booloader Bootloader) { 344 forcedBootloader = booloader 345 forcedError = nil 346 } 347 348 // ForceError can be used to force Find to return an error; use nil to 349 // reset to normal lookup. 350 func ForceError(err error) { 351 forcedBootloader = nil 352 forcedError = err 353 } 354 355 func extractKernelAssetsToBootDir(dstDir string, snapf snap.Container, assets []string) error { 356 // now do the kernel specific bits 357 if err := os.MkdirAll(dstDir, 0755); err != nil { 358 return err 359 } 360 dir, err := os.Open(dstDir) 361 if err != nil { 362 return err 363 } 364 defer dir.Close() 365 366 for _, src := range assets { 367 if err := snapf.Unpack(src, dstDir); err != nil { 368 return err 369 } 370 if err := dir.Sync(); err != nil { 371 return err 372 } 373 } 374 return nil 375 } 376 377 func removeKernelAssetsFromBootDir(bootDir string, s snap.PlaceInfo) error { 378 // remove the kernel blob 379 blobName := s.Filename() 380 dstDir := filepath.Join(bootDir, blobName) 381 if err := os.RemoveAll(dstDir); err != nil { 382 return err 383 } 384 385 return nil 386 } 387 388 // ForGadget returns a bootloader matching a given gadget by inspecting the 389 // contents of gadget directory or an error if no matching bootloader is found. 390 func ForGadget(gadgetDir, rootDir string, opts *Options) (Bootloader, error) { 391 if err := opts.validate(); err != nil { 392 return nil, err 393 } 394 if forcedBootloader != nil || forcedError != nil { 395 return forcedBootloader, forcedError 396 } 397 for _, blNew := range bootloaders { 398 bl := blNew(rootDir, opts) 399 markerConf := filepath.Join(gadgetDir, bl.Name()+".conf") 400 // do we have a marker file? 401 if osutil.FileExists(markerConf) { 402 return bl, nil 403 } 404 } 405 return nil, ErrBootloader 406 } 407 408 // BootFile represents each file in the chains of trusted assets and 409 // kernels used in the boot process. For example a boot file can be an 410 // EFI binary or a snap file containing an EFI binary. 411 type BootFile struct { 412 // Path is the path to the file in the filesystem or, if Snap 413 // is set, the relative path inside the snap file. 414 Path string 415 // Snap contains the path to the snap file if a snap file is used. 416 Snap string 417 // Role is set to the role of the bootloader this boot file 418 // originates from. 419 Role Role 420 } 421 422 func NewBootFile(snap, path string, role Role) BootFile { 423 return BootFile{ 424 Snap: snap, 425 Path: path, 426 Role: role, 427 } 428 } 429 430 // WithPath returns a copy of the BootFile with path updated to the 431 // specified value. 432 func (b BootFile) WithPath(path string) BootFile { 433 b.Path = path 434 return b 435 }