github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/bootloader/bootloader.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 "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 return nil 81 } 82 83 // Bootloader provides an interface to interact with the system 84 // bootloader. 85 type Bootloader interface { 86 // Return the value of the specified bootloader variable. 87 GetBootVars(names ...string) (map[string]string, error) 88 89 // Set the value of the specified bootloader variable. 90 SetBootVars(values map[string]string) error 91 92 // Name returns the bootloader name. 93 Name() string 94 95 // ConfigFile returns the name of the config file. 96 ConfigFile() string 97 98 // InstallBootConfig will try to install the boot config in the 99 // given gadgetDir to rootdir. If no boot config for this bootloader 100 // is found ok is false. 101 InstallBootConfig(gadgetDir string, opts *Options) (ok bool, err error) 102 103 // ExtractKernelAssets extracts kernel assets from the given kernel snap. 104 ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error 105 106 // RemoveKernelAssets removes the assets for the given kernel snap. 107 RemoveKernelAssets(s snap.PlaceInfo) error 108 } 109 110 type installableBootloader interface { 111 Bootloader 112 setRootDir(string) 113 } 114 115 type RecoveryAwareBootloader interface { 116 Bootloader 117 SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error 118 GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) 119 } 120 121 type ExtractedRecoveryKernelImageBootloader interface { 122 Bootloader 123 ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error 124 } 125 126 // ExtractedRunKernelImageBootloader is a Bootloader that also supports specific 127 // methods needed to setup booting from an extracted kernel, which is needed to 128 // implement encryption and/or secure boot. Prototypical implementation is UC20 129 // grub implementation with FDE. 130 type ExtractedRunKernelImageBootloader interface { 131 Bootloader 132 133 // EnableKernel enables the specified kernel on ubuntu-boot to be used 134 // during normal boots. The specified kernel should already have been 135 // extracted. This is usually implemented with a "kernel.efi" symlink 136 // pointing to the extracted kernel image. 137 EnableKernel(snap.PlaceInfo) error 138 139 // EnableTryKernel enables the specified kernel on ubuntu-boot to be 140 // tried by the bootloader on a reboot, to be used in conjunction with 141 // setting "kernel_status" to "try". The specified kernel should already 142 // have been extracted. This is usually implemented with a 143 // "try-kernel.efi" symlink pointing to the extracted kernel image. 144 EnableTryKernel(snap.PlaceInfo) error 145 146 // Kernel returns the current enabled kernel on the bootloader, not 147 // necessarily the kernel that was used to boot the current session, but the 148 // kernel that is enabled to boot on "normal" boots. 149 // If error is not nil, the first argument shall be non-nil. 150 Kernel() (snap.PlaceInfo, error) 151 152 // TryKernel returns the current enabled try-kernel on the bootloader, if 153 // there is no such enabled try-kernel, then ErrNoTryKernelRef is returned. 154 // If error is not nil, the first argument shall be non-nil. 155 TryKernel() (snap.PlaceInfo, error) 156 157 // DisableTryKernel disables the current enabled try-kernel on the 158 // bootloader, if it exists. It does not need to return an error if the 159 // enabled try-kernel does not exist or is in an inconsistent state before 160 // disabling it, errors should only be returned when the implementation 161 // fails to disable the try-kernel. 162 DisableTryKernel() error 163 } 164 165 // TrustedAssetsBootloader has boot assets that take part in the secure boot 166 // process and need to be tracked, while other boot assets (typically boot 167 // config) are managed by snapd. 168 type TrustedAssetsBootloader interface { 169 Bootloader 170 171 // ManagedAssets returns a list of boot assets managed by the bootloader 172 // in the boot filesystem. 173 ManagedAssets() []string 174 // UpdateBootConfig updates the boot config assets used by the bootloader. 175 UpdateBootConfig(*Options) error 176 // CommandLine returns the kernel command line composed of mode and 177 // system arguments, built-in bootloader specific static arguments 178 // corresponding to the on-disk boot asset edition, followed by any 179 // extra arguments. The command line may be different when using a 180 // recovery bootloader. 181 CommandLine(modeArg, systemArg, extraArgs string) (string, error) 182 // CandidateCommandLine is similar to CommandLine, but uses the current 183 // edition of managed built-in boot assets as reference. 184 CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) 185 186 // TrustedAssets returns the list of relative paths to assets inside 187 // the bootloader's rootdir that are measured in the boot process in the 188 // order of loading during the boot. 189 TrustedAssets() ([]string, error) 190 191 // RecoveryBootChain returns the load chain for recovery modes. 192 // It should be called on a RoleRecovery bootloader. 193 RecoveryBootChain(kernelPath string) ([]BootFile, error) 194 195 // BootChain returns the load chain for run mode. 196 // It should be called on a RoleRecovery bootloader passing the 197 // RoleRunMode bootloader. 198 BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) 199 } 200 201 func genericInstallBootConfig(gadgetFile, systemFile string) (bool, error) { 202 if !osutil.FileExists(gadgetFile) { 203 return false, nil 204 } 205 if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { 206 return true, err 207 } 208 return true, osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) 209 } 210 211 func genericSetBootConfigFromAsset(systemFile, assetName string) (bool, error) { 212 bootConfig := assets.Internal(assetName) 213 if bootConfig == nil { 214 return true, fmt.Errorf("internal error: no boot asset for %q", assetName) 215 } 216 if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { 217 return true, err 218 } 219 return true, osutil.AtomicWriteFile(systemFile, bootConfig, 0644, 0) 220 } 221 222 func genericUpdateBootConfigFromAssets(systemFile string, assetName string) error { 223 currentBootConfigEdition, err := editionFromDiskConfigAsset(systemFile) 224 if err != nil && err != errNoEdition { 225 return err 226 } 227 if err == errNoEdition { 228 return nil 229 } 230 newBootConfig := assets.Internal(assetName) 231 if len(newBootConfig) == 0 { 232 return fmt.Errorf("no boot config asset with name %q", assetName) 233 } 234 bc, err := configAssetFrom(newBootConfig) 235 if err != nil { 236 return err 237 } 238 if bc.Edition() <= currentBootConfigEdition { 239 // edition of the candidate boot config is lower than or equal 240 // to one currently installed 241 return nil 242 } 243 return osutil.AtomicWriteFile(systemFile, bc.Raw(), 0644, 0) 244 } 245 246 // InstallBootConfig installs the bootloader config from the gadget 247 // snap dir into the right place. 248 func InstallBootConfig(gadgetDir, rootDir string, opts *Options) error { 249 if err := opts.validate(); err != nil { 250 return err 251 } 252 // TODO:UC20 use ForGadget() to obtain the right bootloader 253 for _, bl := range []installableBootloader{&grub{}, &uboot{}, &androidboot{}, &lk{}} { 254 bl.setRootDir(rootDir) 255 ok, err := bl.InstallBootConfig(gadgetDir, opts) 256 if ok { 257 return err 258 } 259 } 260 261 return fmt.Errorf("cannot find boot config in %q", gadgetDir) 262 } 263 264 type bootloaderNewFunc func(rootdir string, opts *Options) Bootloader 265 266 var ( 267 // bootloaders list all possible bootloaders by their constructor 268 // function. 269 bootloaders = []bootloaderNewFunc{ 270 newUboot, 271 newGrub, 272 newAndroidBoot, 273 newLk, 274 } 275 ) 276 277 var ( 278 forcedBootloader Bootloader 279 forcedError error 280 ) 281 282 // Find returns the bootloader for the system 283 // or an error if no bootloader is found. 284 // 285 // The rootdir option is useful for image creation operations. It 286 // can also be used to find the recovery bootloader, e.g. on uc20: 287 // bootloader.Find("/run/mnt/ubuntu-seed") 288 func Find(rootdir string, opts *Options) (Bootloader, error) { 289 if err := opts.validate(); err != nil { 290 return nil, err 291 } 292 if forcedBootloader != nil || forcedError != nil { 293 return forcedBootloader, forcedError 294 } 295 296 if rootdir == "" { 297 rootdir = dirs.GlobalRootDir 298 } 299 if opts == nil { 300 opts = &Options{} 301 } 302 303 for _, blNew := range bootloaders { 304 bl := blNew(rootdir, opts) 305 if osutil.FileExists(bl.ConfigFile()) { 306 return bl, nil 307 } 308 } 309 // no, weeeee 310 return nil, ErrBootloader 311 } 312 313 // Force can be used to force Find to always find the specified bootloader; use 314 // nil to reset to normal lookup. 315 func Force(booloader Bootloader) { 316 forcedBootloader = booloader 317 forcedError = nil 318 } 319 320 // ForceError can be used to force Find to return an error; use nil to 321 // reset to normal lookup. 322 func ForceError(err error) { 323 forcedBootloader = nil 324 forcedError = err 325 } 326 327 func extractKernelAssetsToBootDir(dstDir string, snapf snap.Container, assets []string) error { 328 // now do the kernel specific bits 329 if err := os.MkdirAll(dstDir, 0755); err != nil { 330 return err 331 } 332 dir, err := os.Open(dstDir) 333 if err != nil { 334 return err 335 } 336 defer dir.Close() 337 338 for _, src := range assets { 339 if err := snapf.Unpack(src, dstDir); err != nil { 340 return err 341 } 342 if err := dir.Sync(); err != nil { 343 return err 344 } 345 } 346 return nil 347 } 348 349 func removeKernelAssetsFromBootDir(bootDir string, s snap.PlaceInfo) error { 350 // remove the kernel blob 351 blobName := s.Filename() 352 dstDir := filepath.Join(bootDir, blobName) 353 if err := os.RemoveAll(dstDir); err != nil { 354 return err 355 } 356 357 return nil 358 } 359 360 // ForGadget returns a bootloader matching a given gadget by inspecting the 361 // contents of gadget directory or an error if no matching bootloader is found. 362 func ForGadget(gadgetDir, rootDir string, opts *Options) (Bootloader, error) { 363 if err := opts.validate(); err != nil { 364 return nil, err 365 } 366 if forcedBootloader != nil || forcedError != nil { 367 return forcedBootloader, forcedError 368 } 369 for _, blNew := range bootloaders { 370 bl := blNew(rootDir, opts) 371 // do we have a marker file? 372 if osutil.FileExists(filepath.Join(gadgetDir, bl.Name()+".conf")) { 373 return bl, nil 374 } 375 } 376 return nil, ErrBootloader 377 } 378 379 // BootFile represents each file in the chains of trusted assets and 380 // kernels used in the boot process. For example a boot file can be an 381 // EFI binary or a snap file containing an EFI binary. 382 type BootFile struct { 383 // Path is the path to the file in the filesystem or, if Snap 384 // is set, the relative path inside the snap file. 385 Path string 386 // Snap contains the path to the snap file if a snap file is used. 387 Snap string 388 // Role is set to the role of the bootloader this boot file 389 // originates from. 390 Role Role 391 } 392 393 func NewBootFile(snap, path string, role Role) BootFile { 394 return BootFile{ 395 Snap: snap, 396 Path: path, 397 Role: role, 398 } 399 } 400 401 // WithPath returns a copy of the BootFile with path updated to the 402 // specified value. 403 func (b BootFile) WithPath(path string) BootFile { 404 b.Path = path 405 return b 406 }