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/boot.go b/metropolis/pkg/efivarfs/boot.go
index 4178c8d..92b8ac9 100644
--- a/metropolis/pkg/efivarfs/boot.go
+++ b/metropolis/pkg/efivarfs/boot.go
@@ -24,159 +24,121 @@
package efivarfs
import (
+ "bytes"
+ "encoding/binary"
+ "errors"
"fmt"
"math"
"strings"
-
- "github.com/google/uuid"
- "golang.org/x/text/transform"
)
-// Note on binary format of EFI variables:
-// This code follows Section 3 "Boot Manager" of version 2.6 of the UEFI Spec:
-// http://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf
-// It uses the binary representation from the Linux "efivars" filesystem.
-// Specifically, all binary data that is marshaled and unmarshaled is preceded by
-// 4 bytes of "Variable Attributes".
-// All binary data must have exactly the correct length and may not be padded
-// with extra bytes while reading or writing.
+type LoadOptionCategory uint8
-// Note on EFI variable attributes:
-// This code ignores all EFI variable attributes when reading.
-// This code always writes variables with the following attributes:
-// - EFI_VARIABLE_NON_VOLATILE (0x00000001)
-// - EFI_VARIABLE_BOOTSERVICE_ACCESS (0x00000002)
-// - EFI_VARIABLE_RUNTIME_ACCESS (0x00000004)
-const defaultAttrsByte0 uint8 = 7
+const (
+ // Boot entries belonging to the Boot category are normal boot entries.
+ LoadOptionCategoryBoot LoadOptionCategory = 0x0
+ // Boot entries belonging to the App category are not booted as part of
+ // the normal boot order, but are only launched via menu or hotkey.
+ // This category is optional for bootloaders to support, before creating
+ // new boot entries of this category firmware support needs to be
+ // confirmed.
+ LoadOptionCategoryApp LoadOptionCategory = 0x1
+)
-// BootEntry represents a subset of the contents of a Boot#### EFI variable.
-type BootEntry struct {
- Description string // eg. "Linux Boot Manager"
- Path string // eg. `\EFI\systemd\systemd-bootx64.efi`
- PartitionGUID uuid.UUID
- PartitionNumber uint32 // Starts with 1
- PartitionStart uint64 // LBA
- PartitionSize uint64 // LBA
+// LoadOption contains information on a payload to be loaded by EFI.
+type LoadOption struct {
+ // Human-readable description of what this load option loads.
+ // This is what's being shown by the firmware when selecting a boot option.
+ Description string
+ // If set, firmware will skip this load option when it is in BootOrder.
+ // It is unspecificed whether this prevents the user from booting the entry
+ // manually.
+ Inactive bool
+ // If set, this load option will not be shown in any menu for load option
+ // selection. This does not affect other functionality.
+ Hidden bool
+ // Category contains the category of the load entry. The selected category
+ // affects various firmware behaviors, see the individual value
+ // descriptions for more information.
+ Category LoadOptionCategory
+ // Path to the UEFI PE executable to execute when this load option is being
+ // loaded.
+ FilePath DevicePath
+ // OptionalData gets passed as an argument to the executed PE executable.
+ // If zero-length a NULL value is passed to the executable.
+ OptionalData []byte
}
-// Marshal generates the binary representation of a BootEntry (EFI_LOAD_OPTION).
-// Description, DiskGUID and Path must be set.
-// Attributes of the boot entry (EFI_LOAD_OPTION.Attributes, not the same
-// as attributes of an EFI variable) are always set to LOAD_OPTION_ACTIVE.
-func (t *BootEntry) Marshal() ([]byte, error) {
- if t.Description == "" ||
- t.PartitionGUID.String() == "00000000-0000-0000-0000-000000000000" ||
- t.Path == "" ||
- t.PartitionNumber == 0 ||
- t.PartitionStart == 0 ||
- t.PartitionSize == 0 {
- return nil, fmt.Errorf("missing field, all are required: %+v", *t)
+// Marshal encodes a LoadOption into a binary EFI_LOAD_OPTION.
+func (e *LoadOption) Marshal() ([]byte, error) {
+ var data []byte
+ var attrs uint32
+ attrs |= (uint32(e.Category) & 0x1f) << 8
+ if e.Hidden {
+ attrs |= 0x08
}
-
- // EFI_LOAD_OPTION.FilePathList
- var dp []byte
-
- // EFI_LOAD_OPTION.FilePathList[0]
- dp = append(dp,
- 0x04, // Type ("Media Device Path")
- 0x01, // Sub-Type ("Hard Drive")
- 0x2a, 0x00, // Length (always 42 bytes for this type)
- )
- dp = append32(dp, t.PartitionNumber)
- dp = append64(dp, t.PartitionStart)
- dp = append64(dp, t.PartitionSize)
- // Append the partition GUID in the EFI format.
- dp = append(dp, MarshalEFIGUID(t.PartitionGUID)...)
-
- dp = append(dp,
- 0x02, // Partition Format ("GUID Partition Table")
- 0x02, // Signature Type ("GUID signature")
- )
-
- // EFI_LOAD_OPTION.FilePathList[1]
- enc := Encoding.NewEncoder()
- bsp := strings.ReplaceAll(t.Path, "/", "\\")
- path, _, e := transform.Bytes(enc, []byte(bsp))
- if e != nil {
- return nil, fmt.Errorf("while encoding Path: %v", e)
+ if !e.Inactive {
+ attrs |= 0x01
}
- path = append16(path, 0) // null terminate string
- filePathLen := len(path) + 4
- dp = append(dp,
- 0x04, // Type ("Media Device Path")
- 0x04, // Sub-Type ("File Path")
- )
- dp = append16(dp, uint16(filePathLen))
- dp = append(dp, path...)
-
- // EFI_LOAD_OPTION.FilePathList[2] ("Device Path End Structure")
- dp = append(dp,
- 0x7F, // Type ("End of Hardware Device Path")
- 0xFF, // Sub-Type ("End Entire Device Path")
- 0x04, 0x00, // Length (always 4 bytes for this type)
- )
-
- out := []byte{
- // EFI variable attributes
- defaultAttrsByte0, 0x00, 0x00, 0x00,
-
- // EFI_LOAD_OPTION.Attributes (only LOAD_OPTION_ACTIVE)
- 0x01, 0x00, 0x00, 0x00,
+ data = append32(data, attrs)
+ filePathRaw, err := e.FilePath.Marshal()
+ if err != nil {
+ return nil, fmt.Errorf("failed marshalling FilePath: %w", err)
}
-
- // EFI_LOAD_OPTION.FilePathListLength
- if len(dp) > math.MaxUint16 {
- // No need to also check for overflows for Path length field explicitly,
- // since if that overflows, this field will definitely overflow as well.
- // There is no explicit length field for Description, so no special
- // handling is required.
- return nil, fmt.Errorf("variable too large, use shorter strings")
+ if len(filePathRaw) > math.MaxUint16 {
+ return nil, fmt.Errorf("failed marshalling FilePath: value too big (%d)", len(filePathRaw))
}
- out = append16(out, uint16(len(dp)))
-
- // EFI_LOAD_OPTION.Description
- desc, _, e := transform.Bytes(enc, []byte(t.Description))
- if e != nil {
- return nil, fmt.Errorf("while encoding Description: %v", e)
+ data = append16(data, uint16(len(filePathRaw)))
+ if strings.IndexByte(e.Description, 0x00) != -1 {
+ return nil, fmt.Errorf("failed to encode Description: contains invalid null bytes")
}
- desc = append16(desc, 0) // null terminate string
- out = append(out, desc...)
-
- // EFI_LOAD_OPTION.FilePathList
- out = append(out, dp...)
-
- // EFI_LOAD_OPTION.OptionalData is always empty
-
- return out, nil
+ encodedDesc, err := Encoding.NewEncoder().Bytes([]byte(e.Description))
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode Description: %w", err)
+ }
+ data = append(data, encodedDesc...)
+ data = append(data, 0x00, 0x00) // Final UTF-16/UCS-2 null code
+ data = append(data, filePathRaw...)
+ data = append(data, e.OptionalData...)
+ return data, nil
}
-// UnmarshalBootEntry loads a BootEntry from its binary representation.
-// WARNING: UnmarshalBootEntry only loads the Description field.
-// Everything else is ignored (and not validated if possible)
-func UnmarshalBootEntry(d []byte) (*BootEntry, error) {
- descOffset := 4 /* EFI Var Attrs */ + 4 /* EFI_LOAD_OPTION.Attributes */ + 2 /*FilePathListLength*/
- if len(d) < descOffset {
- return nil, fmt.Errorf("too short: %v bytes", len(d))
+// UnmarshalLoadOption decodes a binary EFI_LOAD_OPTION into a LoadOption.
+func UnmarshalLoadOption(data []byte) (*LoadOption, error) {
+ if len(data) < 6 {
+ return nil, fmt.Errorf("invalid load option: minimum 6 bytes are required, got %d", len(data))
}
- descBytes := []byte{}
- var foundNull bool
- for i := descOffset; i+1 < len(d); i += 2 {
- a := d[i]
- b := d[i+1]
- if a == 0 && b == 0 {
- foundNull = true
- break
- }
- descBytes = append(descBytes, a, b)
+ var opt LoadOption
+ attrs := binary.LittleEndian.Uint32(data[:4])
+ opt.Category = LoadOptionCategory((attrs >> 8) & 0x1f)
+ opt.Hidden = attrs&0x08 != 0
+ opt.Inactive = attrs&0x01 == 0
+ lenPath := binary.LittleEndian.Uint16(data[4:6])
+ // Search for UTF-16 null code
+ nullIdx := bytes.Index(data[6:], []byte{0x00, 0x00})
+ if nullIdx == -1 {
+ return nil, errors.New("no null code point marking end of Description found")
}
- if !foundNull {
- return nil, fmt.Errorf("didn't find null terminator for Description")
+ descriptionEnd := 6 + nullIdx + 1
+ descriptionRaw := data[6:descriptionEnd]
+ description, err := Encoding.NewDecoder().Bytes(descriptionRaw)
+ if err != nil {
+ return nil, fmt.Errorf("error decoding UTF-16 in Description: %w", err)
}
- descDecoded, _, e := transform.Bytes(Encoding.NewDecoder(), descBytes)
- if e != nil {
- return nil, fmt.Errorf("while decoding Description: %v", e)
+ descriptionEnd += 2 // 2 null bytes terminating UTF-16 string
+ opt.Description = string(description)
+ if descriptionEnd+int(lenPath) > len(data) {
+ return nil, fmt.Errorf("declared length of FilePath (%d) overruns available data (%d)", lenPath, len(data)-descriptionEnd)
}
- return &BootEntry{Description: string(descDecoded)}, nil
+ filePathData := data[descriptionEnd : descriptionEnd+int(lenPath)]
+ opt.FilePath, err = UnmarshalDevicePath(filePathData)
+ if err != nil {
+ return nil, fmt.Errorf("failed unmarshaling FilePath: %w", err)
+ }
+ if descriptionEnd+int(lenPath) < len(data) {
+ opt.OptionalData = data[descriptionEnd+int(lenPath):]
+ }
+ return &opt, nil
}
// BootOrder represents the contents of the BootOrder EFI variable.
@@ -184,7 +146,7 @@
// Marshal generates the binary representation of a BootOrder.
func (t *BootOrder) Marshal() []byte {
- out := []byte{defaultAttrsByte0, 0x00, 0x00, 0x00}
+ var out []byte
for _, v := range *t {
out = append16(out, v)
}
@@ -193,10 +155,10 @@
// UnmarshalBootOrder loads a BootOrder from its binary representation.
func UnmarshalBootOrder(d []byte) (*BootOrder, error) {
- if len(d) < 4 || len(d)%2 != 0 {
+ if len(d)%2 != 0 {
return nil, fmt.Errorf("invalid length: %v bytes", len(d))
}
- l := (len(d) - 4) / 2
+ l := len(d) / 2
out := make(BootOrder, l)
for i := 0; i < l; i++ {
out[i] = uint16(d[4+2*i]) | uint16(d[4+2*i+1])<<8
@@ -219,16 +181,3 @@
byte(v>>24&0xFF),
)
}
-
-func append64(d []byte, v uint64) []byte {
- return append(d,
- byte(v&0xFF),
- byte(v>>8&0xFF),
- byte(v>>16&0xFF),
- byte(v>>24&0xFF),
- byte(v>>32&0xFF),
- byte(v>>40&0xFF),
- byte(v>>48&0xFF),
- byte(v>>56&0xFF),
- )
-}