refactor: efivarfs mock and tests

Refactor efivarfs and add tests.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
Andrey Smirnov 2025-09-24 14:15:43 +04:00 committed by Noel Georgi
parent 1fca111e24
commit ea4ed165ad
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
8 changed files with 330 additions and 177 deletions

View File

@ -12,7 +12,6 @@ import (
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/go-blockdevice/v2/blkid"
"golang.org/x/sys/unix"
"github.com/siderolabs/talos/internal/pkg/efivarfs"
"github.com/siderolabs/talos/pkg/machinery/constants"
@ -37,7 +36,14 @@ const (
// ReadVariable reads a SystemdBoot EFI variable.
func ReadVariable(name string) (string, error) {
data, _, err := efivarfs.Read(efivarfs.ScopeSystemd, name)
efi, err := efivarfs.NewFilesystemReaderWriter(false)
if err != nil {
return "", fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
}
defer efi.Close() //nolint:errcheck
data, _, err := efi.Read(efivarfs.ScopeSystemd, name)
if err != nil {
// if the variable does not exist, return an empty string
if errors.Is(err, os.ErrNotExist) {
@ -65,12 +71,12 @@ func ReadVariable(name string) (string, error) {
// WriteVariable reads a SystemdBoot EFI variable.
func WriteVariable(name, value string) error {
// mount EFI vars as rw
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
return err
efi, err := efivarfs.NewFilesystemReaderWriter(true)
if err != nil {
return fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
}
defer unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "") //nolint:errcheck
defer efi.Close() //nolint:errcheck
out := make([]byte, (len(value)+1)*2)
@ -83,7 +89,7 @@ func WriteVariable(name, value string) error {
out = append(out[:n], 0, 0)
return efivarfs.Write(efivarfs.ScopeSystemd, name, efivarfs.AttrBootserviceAccess|efivarfs.AttrRuntimeAccess|efivarfs.AttrNonVolatile, out)
return efi.Write(efivarfs.ScopeSystemd, name, efivarfs.AttrBootserviceAccess|efivarfs.AttrRuntimeAccess|efivarfs.AttrNonVolatile, out)
}
// CreateBootEntry creates a UEFI boot entry named "Talos Linux UKI" and sets it as the first in the `BootOrder`
@ -92,14 +98,14 @@ func WriteVariable(name, value string) error {
//
//nolint:gocyclo
func CreateBootEntry(installDisk, sdBootFilePath string) error {
// mount EFI vars as rw
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
return err
efi, err := efivarfs.NewFilesystemReaderWriter(true)
if err != nil {
return fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
}
defer unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "") //nolint:errcheck
defer efi.Close() //nolint:errcheck
rawBootOrderData, _, err := efivarfs.Read(efivarfs.ScopeGlobal, "BootOrder")
rawBootOrderData, _, err := efi.Read(efivarfs.ScopeGlobal, "BootOrder")
if err != nil {
return fmt.Errorf("failed to read BootOrder: %w", err)
}
@ -113,7 +119,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
talosBootIndex := len(bootOrder)
for _, idx := range bootOrder {
bootEntry, err := efivarfs.GetBootEntry(int(idx))
bootEntry, err := efivarfs.GetBootEntry(efi, int(idx))
if err != nil {
return fmt.Errorf("failed to get boot entry %d: %w", idx, err)
}
@ -150,7 +156,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
return fmt.Errorf("EFI partition UUID not found on install disk %q", installDisk)
}
if err := efivarfs.SetBootEntry(talosBootIndex, &efivarfs.LoadOption{
if err := efivarfs.SetBootEntry(efi, talosBootIndex, &efivarfs.LoadOption{
Description: TalosBootEntryDescription,
FilePath: efivarfs.DevicePath{
&efivarfs.HardDrivePath{
@ -167,7 +173,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
return fmt.Errorf("failed to set boot entry %d: %w", talosBootIndex, err)
}
currentBootOrder, err := efivarfs.GetBootOrder()
currentBootOrder, err := efivarfs.GetBootOrder(efi)
if err != nil {
return fmt.Errorf("failed to get current BootOrder: %w", err)
}
@ -177,7 +183,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
return nil
}
if err := efivarfs.SetBootOrder(slices.Concat([]uint16{uint16(talosBootIndex)}, currentBootOrder)); err != nil {
if err := efivarfs.SetBootOrder(efi, slices.Concat([]uint16{uint16(talosBootIndex)}, currentBootOrder)); err != nil {
return fmt.Errorf("failed to set BootOrder: %w", err)
}

View File

@ -74,7 +74,7 @@ func (e *LoadOption) Marshal() ([]byte, error) {
attrs |= 0x01
}
data = append32(data, attrs)
data = binary.LittleEndian.AppendUint32(data, attrs)
filePathRaw, err := e.FilePath.Marshal()
if err != nil {
@ -94,7 +94,7 @@ func (e *LoadOption) Marshal() ([]byte, error) {
return nil, fmt.Errorf("failed marshaling FilePath/ExtraPath: value too big (%d)", len(filePathRaw))
}
data = append16(data, uint16(len(filePathRaw)))
data = binary.LittleEndian.AppendUint16(data, uint16(len(filePathRaw)))
if strings.IndexByte(e.Description, 0x00) != -1 {
return nil, fmt.Errorf("failed to encode Description: contains invalid null bytes")
@ -178,8 +178,9 @@ type BootOrder []uint16
// Marshal generates the binary representation of a BootOrder.
func (t *BootOrder) Marshal() []byte {
var out []byte
for _, v := range *t {
out = append16(out, v)
out = binary.LittleEndian.AppendUint16(out, v)
}
return out
@ -195,24 +196,8 @@ func UnmarshalBootOrder(d []byte) (BootOrder, error) {
out := make(BootOrder, l)
for i := range l {
out[i] = uint16(d[2*i]) | uint16(d[2*i+1])<<8
out[i] = binary.LittleEndian.Uint16(d[i*2:])
}
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),
)
}

View File

@ -50,7 +50,7 @@ type PartitionMBR struct {
func (p PartitionMBR) partitionSignature() (sig [16]byte) {
copy(sig[:4], p.DiskSignature[:])
return
return sig
}
func (p PartitionMBR) partitionFormat() uint8 {
@ -283,7 +283,7 @@ func (d DevicePath) Marshal() ([]byte, error) {
return nil, fmt.Errorf("path element payload over maximum size")
}
buf = append16(buf, uint16(len(elemBuf)+4))
buf = binary.LittleEndian.AppendUint16(buf, uint16(len(elemBuf)+4))
buf = append(buf, elemBuf...)
}
// End of device path (Type 0x7f, SubType 0xFF)

View File

@ -23,7 +23,10 @@ import (
"github.com/g0rbe/go-chattr"
"github.com/google/uuid"
"golang.org/x/sys/unix"
"golang.org/x/text/encoding/unicode"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
const (
@ -87,8 +90,48 @@ func varPath(scope uuid.UUID, varName string) string {
return fmt.Sprintf("/sys/firmware/efi/efivars/%s-%s", varName, scope.String())
}
// ReaderWriter is an interface for reading and writing EFI variables.
type ReaderWriter interface {
Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error
Delete(scope uuid.UUID, varName string) error
Read(scope uuid.UUID, varName string) ([]byte, Attribute, error)
List(scope uuid.UUID) ([]string, error)
}
// FilesystemReaderWriter implements ReaderWriter using the efivars Linux filesystem.
type FilesystemReaderWriter struct {
write bool
}
// NewFilesystemReaderWriter creates a new FilesystemReaderWriter.
func NewFilesystemReaderWriter(write bool) (*FilesystemReaderWriter, error) {
if write {
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
return nil, err
}
}
return &FilesystemReaderWriter{
write: write,
}, nil
}
// Close unmounts efivarfs if the FilesystemReaderWriter was created with write
// access.
func (rw *FilesystemReaderWriter) Close() error {
if rw.write {
return unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "")
}
return nil
}
// Write writes the value of the named variable in the given scope.
func Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error {
func (rw *FilesystemReaderWriter) Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error {
if !rw.write {
return errors.New("efivarfs was opened read-only")
}
// Ref: https://docs.kernel.org/filesystems/efivarfs.html
// Remove immutable attribute from the efivarfs file if it exists
if _, err := os.Stat(varPath(scope, varName)); err == nil {
@ -140,7 +183,7 @@ func Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error
}
// Read reads the value of the named variable in the given scope.
func Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
func (rw *FilesystemReaderWriter) Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
val, err := os.ReadFile(varPath(scope, varName))
if err != nil {
e := err
@ -162,7 +205,7 @@ func Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
// List lists all variable names present for a given scope sorted by their names
// in Go's "native" string sort order.
func List(scope uuid.UUID) ([]string, error) {
func (rw *FilesystemReaderWriter) List(scope uuid.UUID) ([]string, error) {
vars, err := os.ReadDir(Path)
if err != nil {
return nil, fmt.Errorf("failed to list variable directory: %w", err)
@ -189,6 +232,10 @@ func List(scope uuid.UUID) ([]string, error) {
// Delete deletes the given variable name in the given scope. Use with care,
// some firmware fails to boot if variables it uses are deleted.
func Delete(scope uuid.UUID, varName string) error {
func (rw *FilesystemReaderWriter) Delete(scope uuid.UUID, varName string) error {
if !rw.write {
return errors.New("efivarfs was opened read-only")
}
return os.Remove(varPath(scope, varName))
}

View File

@ -0,0 +1,129 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package efivarfs_test
import (
"encoding/binary"
"io/fs"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/internal/pkg/efivarfs"
)
func TestBootOrder(t *testing.T) {
t.Parallel()
var bootOrderEntries []byte
for _, entry := range []int{1, 0, 2, 3} {
bootOrderEntries = binary.LittleEndian.AppendUint16(bootOrderEntries, uint16(entry))
}
efiRW := efivarfs.Mock{
Variables: map[uuid.UUID]map[string]efivarfs.MockVariable{
efivarfs.ScopeGlobal: {
"BootOrder": {
Attrs: 0,
Data: bootOrderEntries,
},
},
},
}
vars, err := efiRW.List(efivarfs.ScopeGlobal)
require.NoError(t, err)
require.Contains(t, vars, "BootOrder", "variable BootOrder not found")
bootOrder, err := efivarfs.GetBootOrder(&efiRW)
require.NoError(t, err)
require.Equal(t, efivarfs.BootOrder([]uint16{1, 0, 2, 3}), bootOrder, "BootOrder does not match expected value")
require.NoError(t, efivarfs.SetBootOrder(&efiRW, efivarfs.BootOrder([]uint16{1, 0, 3})))
bootOrder, err = efivarfs.GetBootOrder(&efiRW)
require.NoError(t, err)
require.Equal(t, efivarfs.BootOrder([]uint16{1, 0, 3}), bootOrder, "BootOrder does not match expected value after SetBootOrder")
}
func TestBootEntries(t *testing.T) {
t.Parallel()
efiRW := efivarfs.Mock{}
// no entries yet
entries, err := efivarfs.ListBootEntries(&efiRW)
require.NoError(t, err)
require.Empty(t, len(entries), "expected no boot entries in empty mock")
// create first entry
idx, err := efivarfs.AddBootEntry(&efiRW, &efivarfs.LoadOption{
Description: "First Entry",
FilePath: efivarfs.DevicePath{
efivarfs.FilePath("/first.efi"),
},
})
require.NoError(t, err)
require.Equal(t, 0, idx, "first boot entry index should be 0")
// verify first entry
entry, err := efivarfs.GetBootEntry(&efiRW, idx)
require.NoError(t, err)
require.Equal(t, "First Entry", entry.Description, "first boot entry description does not match")
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/first.efi")}, entry.FilePath, "first boot entry file path does not match")
// create second entry
require.NoError(t, efivarfs.SetBootEntry(&efiRW, 1, &efivarfs.LoadOption{
Description: "Second Entry",
FilePath: efivarfs.DevicePath{
efivarfs.FilePath("/second.efi"),
},
}), "failed to set second boot entry")
// verify second entry
entry, err = efivarfs.GetBootEntry(&efiRW, 1)
require.NoError(t, err)
require.Equal(t, "Second Entry", entry.Description, "second boot entry description does not match")
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/second.efi")}, entry.FilePath, "second boot entry file path does not match")
// list all entries
entries, err = efivarfs.ListBootEntries(&efiRW)
require.NoError(t, err)
require.Len(t, entries, 2, "expected exactly two boot entries after adding two")
// try overwrite first entry
require.NoError(t, efivarfs.SetBootEntry(&efiRW, idx, &efivarfs.LoadOption{
Description: "First Entry Overwritten",
FilePath: efivarfs.DevicePath{
efivarfs.FilePath("/first_overwritten.efi"),
},
}), "failed to overwrite first boot entry")
// verify first entry after overwrite
entry, err = efivarfs.GetBootEntry(&efiRW, idx)
require.NoError(t, err)
require.Equal(t, "First Entry Overwritten", entry.Description, "first boot entry description does not match after overwrite")
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/first_overwritten.efi")}, entry.FilePath, "first boot entry file path does not match after overwrite")
// verify delete non-existing entry
require.ErrorIs(t, efivarfs.DeleteBootEntry(&efiRW, 42), fs.ErrNotExist, "expected ErrNoSuchEntry when deleting non-existing entry")
// delete second entry
require.NoError(t, efivarfs.DeleteBootEntry(&efiRW, 1), "failed to delete second boot entry")
// verify second entry is gone
_, err = efivarfs.GetBootEntry(&efiRW, 1)
require.ErrorIs(t, err, fs.ErrNotExist, "expected ErrNoSuchEntry when getting deleted entry")
// list entries
entries, err = efivarfs.ListBootEntries(&efiRW)
require.NoError(t, err)
require.Len(t, entries, 1, "expected exactly one boot entry after deleting one of two")
}

View File

@ -0,0 +1,80 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package efivarfs
import (
"io/fs"
"maps"
"slices"
"github.com/google/uuid"
)
// Mock is a mock implementation of ReaderWriter interface for testing purposes.
type Mock struct {
Variables map[uuid.UUID]map[string]MockVariable
}
// MockVariable represents a mock EFI variable with its attributes and data.
type MockVariable struct {
Attrs Attribute
Data []byte
}
// Write writes a variable to the given scope.
func (mock *Mock) Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error {
if mock.Variables == nil {
mock.Variables = make(map[uuid.UUID]map[string]MockVariable)
}
if mock.Variables[scope] == nil {
mock.Variables[scope] = make(map[string]MockVariable)
}
mock.Variables[scope][varName] = MockVariable{
Attrs: attrs,
Data: value,
}
return nil
}
// Delete deletes a variable from the given scope.
func (mock *Mock) Delete(scope uuid.UUID, varName string) error {
if mock.Variables == nil || mock.Variables[scope] == nil {
return fs.ErrNotExist
}
if _, exists := mock.Variables[scope][varName]; !exists {
return fs.ErrNotExist
}
delete(mock.Variables[scope], varName)
return nil
}
// Read reads a variable from the given scope.
func (mock *Mock) Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
if mock.Variables == nil || mock.Variables[scope] == nil {
return nil, 0, fs.ErrNotExist
}
variable, exists := mock.Variables[scope][varName]
if !exists {
return nil, 0, fs.ErrNotExist
}
return variable.Data, variable.Attrs, nil
}
// List lists all variable names in the given scope.
func (mock *Mock) List(scope uuid.UUID) ([]string, error) {
if mock.Variables == nil || mock.Variables[scope] == nil {
return nil, nil
}
return slices.Collect(maps.Keys(mock.Variables[scope])), nil
}

View File

@ -1,108 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
package efivarfs
import (
"encoding/binary"
"errors"
"fmt"
"os"
"sync"
)
// OSIndications is a bitset used to indicate firmware support for various
// features as well as to trigger some of these features.
// If a constant ends in Supported, it cannot be triggered, the others
// can be.
type OSIndications uint64
const (
// BootToFirmwareUI indicates that on next boot firmware should boot to a firmware-provided
// UI instead of the normal boot order.
BootToFirmwareUI = OSIndications(1 << iota)
// TimestampRevocationSupported indicates that firmware supports timestamp-based revocation and the
// "dbt" authorized timestamp database variable.
TimestampRevocationSupported
// FileCapsuleDelivery indicates that on next boot firmware should look for an EFI update
// capsule on an EFI system partition and try to install it.
FileCapsuleDelivery
// FirmwareManagementProtocolCapsuleSupported indicates that firmware supports UEFI FMP update capsules.
FirmwareManagementProtocolCapsuleSupported
// CapsuleResultVarSupported indicates that firmware supports reporting results of deferred (i.e.
// processed on next boot) capsule installs via variables.
CapsuleResultVarSupported
// StartOSRecovery indicates that firmware should skip Boot# processing on next boot
// and instead use OsRecovery# for selecting a load option.
StartOSRecovery
// StartPlatformRecovery indicates that firmware should skip Boot# processing on next boot
// and instead use PlatformRecovery# for selecting a load option.
StartPlatformRecovery
// JSONConfigDataRefresh indicates that firmware should collect the current config and report
// the data to the EFI system configuration table on next boot.
JSONConfigDataRefresh
)
// osIndicationMutex protects against race conditions in read-modify-write
// sequences on the OsIndications EFI variable.
var osIndicationsMutex sync.Mutex
// OSIndicationsSupported indicates which of the OS indication features and
// actions that the firmware supports.
func OSIndicationsSupported() (OSIndications, error) {
osIndicationsRaw, _, err := Read(ScopeGlobal, "OsIndicationsSupported")
if err != nil {
return 0, fmt.Errorf("unable to read OsIndicationsSupported: %w", err)
}
if len(osIndicationsRaw) != 8 {
return 0, fmt.Errorf("value of OsIndicationsSupported is not 8 bytes / 64 bits, is %d bytes", len(osIndicationsRaw))
}
return OSIndications(binary.LittleEndian.Uint64(osIndicationsRaw)), nil
}
// SetOSIndications sets all OS indication bits set in i in firmware. It does
// not clear any already-set bits, use ClearOSIndications for that.
func SetOSIndications(i OSIndications) error {
return modifyOSIndications(func(prev OSIndications) OSIndications {
return prev | i
})
}
// ClearOSIndications clears all OS indication bits set in i in firmware.
// Note that this effectively inverts i, bits set in i will be cleared.
func ClearOSIndications(i OSIndications) error {
return modifyOSIndications(func(prev OSIndications) OSIndications {
return prev & ^i
})
}
func modifyOSIndications(f func(prev OSIndications) OSIndications) error {
osIndicationsMutex.Lock()
defer osIndicationsMutex.Unlock()
var osIndications OSIndications
rawIn, _, err := Read(ScopeGlobal, "OsIndications")
if err == nil && len(rawIn) == 8 {
osIndications = OSIndications(binary.LittleEndian.Uint64(rawIn))
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("unable to read OsIndications variable: %w", err)
}
osIndications = f(osIndications)
var raw [8]byte
binary.LittleEndian.PutUint64(raw[:], uint64(osIndications))
if err := Write(ScopeGlobal, "OsIndications", AttrNonVolatile|AttrRuntimeAccess, raw[:]); err != nil {
return fmt.Errorf("failed to write OSIndications variable: %w", err)
}
return nil
}

View File

@ -31,8 +31,8 @@ func decodeString(varData []byte) (string, error) {
}
// ReadLoaderDevicePartUUID reads the ESP UUID from an EFI variable.
func ReadLoaderDevicePartUUID() (uuid.UUID, error) {
efiVar, _, err := Read(ScopeSystemd, "LoaderDevicePartUUID")
func ReadLoaderDevicePartUUID(rw ReaderWriter) (uuid.UUID, error) {
efiVar, _, err := rw.Read(ScopeSystemd, "LoaderDevicePartUUID")
if err != nil {
return uuid.Nil, err
}
@ -55,17 +55,15 @@ func ReadLoaderDevicePartUUID() (uuid.UUID, error) {
// 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)
// ListBootEntries lists all EFI boot entries present in the system by their index.
func ListBootEntries(rw ReaderWriter) (map[int]*LoadOption, error) {
bootEntries := make(map[int]*LoadOption)
varNames, err := rw.List(ScopeGlobal)
if err != nil {
return -1, fmt.Errorf("failed to list EFI variables: %w", err)
return nil, fmt.Errorf("failed to list EFI variables at scope %s: %w", ScopeGlobal, 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 {
@ -79,13 +77,29 @@ func AddBootEntry(be *LoadOption) (int, error) {
panic(err)
}
presentEntries[int(idx)] = true
entry, err := GetBootEntry(rw, int(idx))
if err != nil {
return nil, fmt.Errorf("failed to get boot entry %s: %w", varName, err)
}
bootEntries[int(idx)] = entry
}
return bootEntries, nil
}
// AddBootEntry creates an new EFI boot entry variable and returns its
// non-negative index on success.
func AddBootEntry(rw ReaderWriter, be *LoadOption) (int, error) {
bootEntries, err := ListBootEntries(rw)
if err != nil {
return -1, fmt.Errorf("failed to list boot entries: %w", err)
}
idx := -1
for i := range math.MaxUint16 {
if !presentEntries[i] {
if _, ok := bootEntries[i]; !ok {
idx = i
break
@ -96,7 +110,7 @@ func AddBootEntry(be *LoadOption) (int, error) {
return -1, errors.New("all 2^16 boot entry variables are occupied")
}
err = SetBootEntry(idx, be)
err = SetBootEntry(rw, idx, be)
if err != nil {
return -1, fmt.Errorf("failed to set new boot entry: %w", err)
}
@ -105,11 +119,11 @@ func AddBootEntry(be *LoadOption) (int, error) {
}
// GetBootEntry returns the boot entry at the given index.
func GetBootEntry(idx int) (*LoadOption, error) {
raw, _, err := Read(ScopeGlobal, fmt.Sprintf("Boot%04X", idx))
func GetBootEntry(rw ReaderWriter, idx int) (*LoadOption, error) {
raw, _, err := rw.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))
raw, _, err = rw.Read(ScopeGlobal, fmt.Sprintf("Boot%04x", idx))
}
if err != nil {
@ -120,21 +134,21 @@ func GetBootEntry(idx int) (*LoadOption, error) {
}
// SetBootEntry writes the given boot entry to the given index.
func SetBootEntry(idx int, be *LoadOption) error {
func SetBootEntry(rw ReaderWriter, 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)
return rw.Write(ScopeGlobal, fmt.Sprintf("Boot%04X", idx), AttrNonVolatile|AttrRuntimeAccess, bem)
}
// DeleteBootEntry deletes the boot entry at the given index.
func DeleteBootEntry(idx int) error {
err := Delete(ScopeGlobal, fmt.Sprintf("Boot%04X", idx))
func DeleteBootEntry(rw ReaderWriter, idx int) error {
err := rw.Delete(ScopeGlobal, fmt.Sprintf("Boot%04X", idx))
if errors.Is(err, fs.ErrNotExist) {
// Try non-spec-conforming lowercase entry
err = Delete(ScopeGlobal, fmt.Sprintf("Boot%04x", idx))
err = rw.Delete(ScopeGlobal, fmt.Sprintf("Boot%04x", idx))
}
return err
@ -142,13 +156,13 @@ func DeleteBootEntry(idx int) error {
// 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())
func SetBootOrder(rw ReaderWriter, ord BootOrder) error {
return rw.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")
func GetBootOrder(rw ReaderWriter) (BootOrder, error) {
raw, _, err := rw.Read(ScopeGlobal, "BootOrder")
if err != nil {
return nil, err
}
@ -163,9 +177,9 @@ func GetBootOrder() (BootOrder, error) {
// SetBootNext sets the boot entry used for the next boot only. It automatically
// resets after the next boot.
func SetBootNext(entryIdx uint16) error {
func SetBootNext(rw ReaderWriter, entryIdx uint16) error {
data := make([]byte, 2)
binary.LittleEndian.PutUint16(data, entryIdx)
return Write(ScopeGlobal, "BootNext", AttrNonVolatile|AttrRuntimeAccess, data)
return rw.Write(ScopeGlobal, "BootNext", AttrNonVolatile|AttrRuntimeAccess, data)
}