m/p/efivarfs: import the EFI boot entry data type
This imports marshal.go from the Softmetal project.
The complete MIT license under which it was released was added at the
start of the file. It was renamed to boot.go which better reflects its
purpose in its current context. The implementation was adapted for
Metropolis.
Change-Id: I41d1b10bf5105c52fa7de7695def5b6f3a9b192e
Reviewed-on: https://review.monogon.dev/c/monogon/+/427
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/pkg/efivarfs/boot.go b/metropolis/pkg/efivarfs/boot.go
new file mode 100644
index 0000000..096ec7c
--- /dev/null
+++ b/metropolis/pkg/efivarfs/boot.go
@@ -0,0 +1,229 @@
+// MIT License
+//
+// Copyright (c) 2021 Philippe Voinov (philippevoinov@gmail.com)
+// Copyright 2021 The Monogon Project Authors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package efivarfs
+
+import (
+ "fmt"
+ "math"
+
+ "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.
+
+// 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
+
+// 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 string
+ PartitionNumber uint32 // Starts with 1
+ PartitionStart uint64 // LBA
+ PartitionSize uint64 // LBA
+}
+
+// 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 == "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)
+ }
+
+ // 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)
+ dp = append(dp, t.PartitionGUID[0:16]...) // Partition Signature
+ dp = append(dp,
+ 0x02, // Partition Format ("GUID Partition Table")
+ 0x02, // Signature Type ("GUID signature")
+ )
+
+ // EFI_LOAD_OPTION.FilePathList[1]
+ enc := Encoding.NewEncoder()
+ path, _, e := transform.Bytes(enc, []byte(t.Path))
+ if e != nil {
+ return nil, fmt.Errorf("while encoding Path: %v", e)
+ }
+ 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,
+ }
+
+ // 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")
+ }
+ 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)
+ }
+ 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
+}
+
+// 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))
+ }
+ 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)
+ }
+ if !foundNull {
+ return nil, fmt.Errorf("didn't find null terminator for Description")
+ }
+ descDecoded, _, e := transform.Bytes(Encoding.NewDecoder(), descBytes)
+ if e != nil {
+ return nil, fmt.Errorf("while decoding Description: %v", e)
+ }
+ return &BootEntry{Description: string(descDecoded)}, nil
+}
+
+// BootOrder represents the contents of the BootOrder EFI variable.
+type BootOrder []uint16
+
+// Marshal generates the binary representation of a BootOrder.
+func (t *BootOrder) Marshal() []byte {
+ out := []byte{defaultAttrsByte0, 0x00, 0x00, 0x00}
+ for _, v := range *t {
+ out = append16(out, v)
+ }
+ return out
+}
+
+// UnmarshalBootOrder loads a BootOrder from its binary representation.
+func UnmarshalBootOrder(d []byte) (*BootOrder, error) {
+ if len(d) < 4 || len(d)%2 != 0 {
+ return nil, fmt.Errorf("invalid length: %v bytes", len(d))
+ }
+ l := (len(d) - 4) / 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
+ }
+ return &out, nil
+}
+
+func append16(d []byte, v uint16) []byte {
+ return append(d,
+ byte(v&0xFF),
+ byte(v>>8&0xFF),
+ )
+}
+
+func append32(d []byte, v uint32) []byte {
+ return append(d,
+ byte(v&0xFF),
+ byte(v>>8&0xFF),
+ byte(v>>16&0xFF),
+ 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),
+ )
+}