blob: 4178c8da2b95b7e80ea86d1507c5185a48cc7bdd [file] [log] [blame]
// 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"
"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.
// 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 uuid.UUID
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.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)
}
// 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)
}
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),
)
}