m/p/efivarfs: refactor
This accomplishes three things:
First, split out the variable access layer from the rest of the code.
This cleans up the attribute handling, which is now done centrally as
well as making the high-level functions very short and clean. They now
also return better errors.
Second this introduces proper types for LoadOption, which can now also
be unmarshaled which was a requirement for A/B updates. This required
implementation of EFI's DevicePath structure.
While refactoring the higher-level functions for this, this also
fixes a bug where the variable index (the 4 hex nibbles at the end) were
improperly generated as lowercase hex.
Third, this adds new high-level functions for interacting with more
boot-related variables needed for the A/B effort.
Change-Id: I53490fa4898a5e7a5498ecc05a9078bd2d66c26e
Reviewed-on: https://review.monogon.dev/c/monogon/+/1855
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/efivarfs/variables.go b/metropolis/pkg/efivarfs/variables.go
new file mode 100644
index 0000000..876173c
--- /dev/null
+++ b/metropolis/pkg/efivarfs/variables.go
@@ -0,0 +1,136 @@
+package efivarfs
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io/fs"
+ "math"
+ "regexp"
+ "strconv"
+
+ "github.com/google/uuid"
+)
+
+func decodeString(varData []byte) (string, error) {
+ efiStringRaw, err := Encoding.NewDecoder().Bytes(varData)
+ if err != nil {
+ // Pass the decoding error unwrapped.
+ return "", err
+ }
+ // Remove the null suffix.
+ return string(bytes.TrimSuffix(efiStringRaw, []byte{0})), nil
+}
+
+// ReadLoaderDevicePartUUID reads the ESP UUID from an EFI variable.
+func ReadLoaderDevicePartUUID() (uuid.UUID, error) {
+ efiVar, _, err := Read(ScopeSystemd, "LoaderDevicePartUUID")
+ if err != nil {
+ return uuid.Nil, err
+ }
+ strContent, err := decodeString(efiVar)
+ if err != nil {
+ return uuid.Nil, fmt.Errorf("decoding string failed: %w", err)
+ }
+ out, err := uuid.Parse(strContent)
+ if err != nil {
+ return uuid.Nil, fmt.Errorf("value in LoaderDevicePartUUID could not be parsed as UUID: %w", err)
+ }
+ return out, nil
+}
+
+// Technically UEFI mandates that only upper-case hex indices are valid, but in
+// practice even vendors themselves ship firmware with lowercase hex indices,
+// thus accept these here as well.
+var bootVarRegexp = regexp.MustCompile(`^Boot([0-9A-Fa-f]{4})$`)
+
+// AddBootEntry creates an new EFI boot entry variable and returns its
+// non-negative index on success.
+func AddBootEntry(be *LoadOption) (int, error) {
+ varNames, err := List(ScopeGlobal)
+ if err != nil {
+ return -1, fmt.Errorf("failed to list EFI variables: %w", err)
+ }
+ presentEntries := make(map[int]bool)
+ // Technically these are sorted, but due to the lower/upper case issue
+ // we cannot rely on this fact.
+ for _, varName := range varNames {
+ s := bootVarRegexp.FindStringSubmatch(varName)
+ if s == nil {
+ continue
+ }
+ idx, err := strconv.ParseUint(s[1], 16, 16)
+ if err != nil {
+ // This cannot be hit as all regexp matches are parseable.
+ // A quick fuzz run agrees.
+ panic(err)
+ }
+ presentEntries[int(idx)] = true
+ }
+ idx := -1
+ for i := 0; i < math.MaxUint16; i++ {
+ if !presentEntries[i] {
+ idx = i
+ break
+ }
+ }
+ if idx == -1 {
+ return -1, errors.New("all 2^16 boot entry variables are occupied")
+ }
+
+ err = SetBootEntry(idx, be)
+ if err != nil {
+ return -1, fmt.Errorf("failed to set new boot entry: %w", err)
+ }
+ return idx, nil
+}
+
+// GetBootEntry returns the boot entry at the given index.
+func GetBootEntry(idx int) (*LoadOption, error) {
+ raw, _, err := Read(ScopeGlobal, fmt.Sprintf("Boot%04X", idx))
+ if errors.Is(err, fs.ErrNotExist) {
+ // Try non-spec-conforming lowercase entry
+ raw, _, err = Read(ScopeGlobal, fmt.Sprintf("Boot%04x", idx))
+ }
+ if err != nil {
+ return nil, err
+ }
+ return UnmarshalLoadOption(raw)
+}
+
+// SetBootEntry writes the given boot entry to the given index.
+func SetBootEntry(idx int, be *LoadOption) error {
+ bem, err := be.Marshal()
+ if err != nil {
+ return fmt.Errorf("while marshaling the EFI boot entry: %w", err)
+ }
+ return Write(ScopeGlobal, fmt.Sprintf("Boot%04X", idx), AttrNonVolatile|AttrRuntimeAccess, bem)
+}
+
+// SetBootOrder replaces contents of the boot order variable with the order
+// specified in ord.
+func SetBootOrder(ord *BootOrder) error {
+ return Write(ScopeGlobal, "BootOrder", AttrNonVolatile|AttrRuntimeAccess, ord.Marshal())
+}
+
+// GetBootOrder returns the current boot order of the system.
+func GetBootOrder() (*BootOrder, error) {
+ raw, _, err := Read(ScopeGlobal, "BootOrder")
+ if err != nil {
+ return nil, err
+ }
+ ord, err := UnmarshalBootOrder(raw)
+ if err != nil {
+ return nil, fmt.Errorf("invalid boot order structure: %w", err)
+ }
+ return ord, nil
+}
+
+// SetBootNext sets the boot entry used for the next boot only. It automatically
+// resets after the next boot.
+func SetBootNext(entryIdx uint16) error {
+ data := make([]byte, 2)
+ binary.LittleEndian.PutUint16(data, entryIdx)
+ return Write(ScopeGlobal, "BootNext", AttrNonVolatile|AttrRuntimeAccess, data)
+}