-
Notifications
You must be signed in to change notification settings - Fork 594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
boot,bootloader: add support for shim fallback and setting EFI boot variables on install #13205
Closed
olivercalder
wants to merge
4
commits into
canonical:master
from
olivercalder:shim-fallback-efi-vars
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9da44c0
bootloader/grub: Add support for shim with fallback
xnox 761cb51
boot: added function to set EFI variables
olivercalder 6d66468
tests: add nested spread test for setting EFI vars
olivercalder ba950c1
tests: skip EFI boot vars test after gadget update -- TODO: revert
olivercalder File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2014-2022 Canonical Ltd | ||
* Copyright (C) 2014-2023 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 | ||
|
@@ -558,6 +558,14 @@ func makeRunnableSystem(model *asserts.Model, bootWith *BootableSet, sealer *Tru | |
if err := MarkRecoveryCapableSystem(recoverySystemLabel); err != nil { | ||
return fmt.Errorf("cannot record %q as a recovery capable system: %v", recoverySystemLabel, err) | ||
} | ||
|
||
err = setUbuntuSeedEfiBootVariables() | ||
if err == errUnsupportedBootloader { | ||
logger.Debugf("%v", err) | ||
} else if err != nil { | ||
logger.Debugf("WARNING: %v", err) | ||
Comment on lines
+564
to
+566
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the decision to only log errors here? |
||
} | ||
|
||
return nil | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2023 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" | ||
|
||
var errUnsupportedBootloader = errors.New("bootloader does not support setting EFI boot variables") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2023 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 "github.com/snapcore/snapd/osutil" | ||
|
||
func SetEfiBootVariables(description string, assetPath string, optionalData []byte) error { | ||
return osutil.ErrDarwin | ||
} | ||
|
||
func setUbuntuSeedEfiBootVariables() error { | ||
return osutil.ErrDarwin | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2023 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 ( | ||
"bytes" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
|
||
"github.com/canonical/go-efilib" | ||
"github.com/canonical/go-efilib/linux" | ||
|
||
"github.com/snapcore/snapd/bootloader" | ||
) | ||
|
||
var ( | ||
ErrAllBootNumsUsed = errors.New("all Boot#### variable numbers are already in use") | ||
ErrNoMatchingVariable = errors.New("no variable matches the given boot option") | ||
ErrInvalidBootOrder = errors.New("BootOrder variable data must have even length") | ||
|
||
defaultVarAttrs = efi.AttributeNonVolatile | efi.AttributeBootserviceAccess | efi.AttributeRuntimeAccess | ||
|
||
efiListVariables = efi.ListVariables | ||
efiReadVariable = efi.ReadVariable | ||
efiWriteVariable = efi.WriteVariable | ||
|
||
linuxFilePathToDevicePath = linux.FilePathToDevicePath | ||
|
||
bootOptionRegexp = regexp.MustCompile("^Boot[0-9A-F]{4}$") | ||
) | ||
|
||
// constructLoadOption returns a serialized EFI load option with the device | ||
// path corresponding to the given asset path, along with the given description | ||
// and optional data. | ||
func constructLoadOption(description string, assetPath string, optionalData []byte) ([]byte, error) { | ||
devicePath, err := linuxFilePathToDevicePath(assetPath, linux.ShortFormPathHD) | ||
if err != nil { | ||
return nil, err | ||
} | ||
loadOption := &efi.LoadOption{ | ||
Attributes: efi.LoadOptionActive | efi.LoadOptionCategoryBoot, | ||
Description: description, | ||
FilePath: devicePath, | ||
OptionalData: optionalData, | ||
} | ||
loadOptionSerialized, err := loadOption.Bytes() | ||
if err != nil { | ||
return nil, err | ||
} | ||
return loadOptionSerialized, nil | ||
} | ||
|
||
// Searches existing Boot#### variables for one which matches the given data. | ||
// | ||
// If there is a match, returns the boot number of the existing variable. | ||
// Otherwise, finds the first available boot number and returns it, along with | ||
// ErrNoMatchingVariable, indicating that a new boot option with that boot | ||
// number should be written. If a different error occurs, returns that error, | ||
// and the returned boot number should be ignored. | ||
func findMatchingBootOption(optionData []byte) (uint16, error) { | ||
variables, err := efiListVariables() | ||
if err != nil { | ||
return 0, err | ||
} | ||
usedBootNums := make(map[uint64]bool) | ||
for _, varDesc := range variables { | ||
varName := varDesc.Name | ||
varGUID := varDesc.GUID | ||
if !bootOptionRegexp.MatchString(varName) { | ||
// Not a Boot#### variable, so skip it | ||
continue | ||
} | ||
if varGUID != efi.GlobalVariable { | ||
// Not an EFI global variable, so skip it | ||
continue | ||
} | ||
varNumber, err := strconv.ParseUint(varName[4:], 16, 16) | ||
if err != nil { | ||
// Should not occur, since variable matched bootOptionRegexp | ||
return 0, err | ||
} | ||
// Since we never overwrite an existing variable, we can ignore | ||
// variable attributes when reading the variable | ||
varData, _, err := efiReadVariable(varName, varGUID) | ||
if err != nil { | ||
return 0, err | ||
} | ||
if bytes.Compare(optionData, varData) == 0 { | ||
// existing variable already identical, use it | ||
return uint16(varNumber), nil | ||
} | ||
usedBootNums[varNumber] = true | ||
} | ||
for bootNum := uint64(0); bootNum <= 0xFFFF; bootNum++ { | ||
if !usedBootNums[bootNum] { | ||
return uint16(bootNum), ErrNoMatchingVariable | ||
} | ||
} | ||
return 0, ErrAllBootNumsUsed | ||
} | ||
|
||
// Ensures that a Boot#### variable contains the given EFI load option. | ||
// | ||
// It may be the case that an existing boot variable already contains the | ||
// given load option, in which case that boot variable is reused. Otherwise, | ||
// finds the first unused boot variable number and uses it. Writes the load | ||
// option to that variable, and returns the number of the variable that was | ||
// used. | ||
func setEfiBootOptionVariable(loadOptionData []byte) (uint16, error) { | ||
bootNum, err := findMatchingBootOption(loadOptionData) | ||
if err == nil { | ||
return bootNum, nil | ||
} else if err != ErrNoMatchingVariable { | ||
return 0, err | ||
} | ||
varName := fmt.Sprintf("Boot%04X", bootNum) | ||
err = efiWriteVariable(varName, efi.GlobalVariable, defaultVarAttrs, loadOptionData) | ||
return bootNum, err | ||
} | ||
|
||
// Reads the current BootOrder variable, inserts the given newBootNum at the | ||
// beginning of the number list (and removes it from later in the list if | ||
// it occurs) and writes the list as the new BootOrder variable. | ||
func setEfiBootOrderVariable(newBootNum uint16) error { | ||
origData, attrs, err := efiReadVariable("BootOrder", efi.GlobalVariable) | ||
if err == efi.ErrVarNotExist { | ||
attrs = defaultVarAttrs | ||
origData = make([]byte, 0) | ||
} else if err != nil { | ||
return err | ||
} | ||
if len(origData)%2 != 0 { | ||
return ErrInvalidBootOrder | ||
} | ||
var optionOffset = -1 | ||
for i := 0; i < len(origData); i += 2 { | ||
bootNum := binary.LittleEndian.Uint16(origData[i : i+2]) | ||
if newBootNum == bootNum { | ||
optionOffset = i | ||
break | ||
} | ||
} | ||
var newData []byte | ||
if optionOffset == 0 { | ||
// newBootNum already at start, no need to re-write variable | ||
return nil | ||
} else if optionOffset == -1 { | ||
// newBootNum not in original boot order | ||
newData = make([]byte, len(origData)+2) | ||
binary.LittleEndian.PutUint16(newData, newBootNum) | ||
copy(newData[2:], origData) | ||
} else { | ||
newData = make([]byte, len(origData)) | ||
binary.LittleEndian.PutUint16(newData, newBootNum) | ||
copy(newData[2:], origData[:optionOffset]) | ||
copy(newData[optionOffset+2:], origData[optionOffset+2:]) | ||
} | ||
return efiWriteVariable("BootOrder", efi.GlobalVariable, attrs, newData) | ||
} | ||
|
||
// SetEfiBootVariables sets the Boot#### and BootOrder variables for the given | ||
// load option information. | ||
// | ||
// Constructs an EFI load option with the given description, the device path | ||
// corresponding to the given asset path, and the given optional data. Writes | ||
// the EFI boot variable Boot#### to contain the resulting load option. Then, | ||
// sets the BootOrder variable so that the #### number from the chosen Boot#### | ||
// is first, removing it from elsewhere in the BootOrder if it occurs. | ||
func SetEfiBootVariables(description string, assetPath string, optionalData []byte) error { | ||
loadOptionData, err := constructLoadOption(description, assetPath, optionalData) | ||
if err != nil { | ||
return err | ||
} | ||
bootNum, err := setEfiBootOptionVariable(loadOptionData) | ||
if err != nil { | ||
return err | ||
} | ||
return setEfiBootOrderVariable(bootNum) | ||
} | ||
|
||
// setUbuntuSeedEfiBootVariables sets EFI variables according to the bootloader | ||
// found on ubuntu seed if it is a UefiBootloader. | ||
func setUbuntuSeedEfiBootVariables() error { | ||
opts := &bootloader.Options{ | ||
Role: bootloader.RoleRecovery, | ||
} | ||
// Set EFI boot variables according to bootloader on ubuntu-seed | ||
seedBl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) | ||
if err != nil { | ||
return fmt.Errorf("cannot find bootloader in seed directory: %v; skipping setting EFI variables", err) | ||
} | ||
ubl, ok := seedBl.(bootloader.UefiBootloader) | ||
if !ok { | ||
return errUnsupportedBootloader | ||
} | ||
description, assetPath, optionalData, err := ubl.EfiLoadOptionParameters() | ||
if err != nil { | ||
return fmt.Errorf("cannot get EFI load option parameter: %v", err) | ||
} | ||
if err = SetEfiBootVariables(description, assetPath, optionalData); err != nil { | ||
return fmt.Errorf("failed to set EFI boot variables: %v", err) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be closer to where we call bootloader.InstallBootConfig