From ea4ed165ad860a5beea17ca2d404bdaa6e5ad933 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Wed, 24 Sep 2025 14:15:43 +0400 Subject: [PATCH] refactor: efivarfs mock and tests Refactor efivarfs and add tests. Signed-off-by: Andrey Smirnov Signed-off-by: Noel Georgi --- .../v1alpha1/bootloader/sdboot/efivars.go | 38 +++--- internal/pkg/efivarfs/boot.go | 25 +--- internal/pkg/efivarfs/devicepath.go | 4 +- internal/pkg/efivarfs/efivarfs.go | 55 +++++++- internal/pkg/efivarfs/efivarfs_test.go | 129 ++++++++++++++++++ internal/pkg/efivarfs/mock.go | 80 +++++++++++ internal/pkg/efivarfs/osindications.go | 108 --------------- internal/pkg/efivarfs/variables.go | 68 +++++---- 8 files changed, 330 insertions(+), 177 deletions(-) create mode 100644 internal/pkg/efivarfs/efivarfs_test.go create mode 100644 internal/pkg/efivarfs/mock.go delete mode 100644 internal/pkg/efivarfs/osindications.go diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go index 563b37804..3e35d58ee 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go @@ -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) } diff --git a/internal/pkg/efivarfs/boot.go b/internal/pkg/efivarfs/boot.go index 76ab1bd3f..aefb31aff 100644 --- a/internal/pkg/efivarfs/boot.go +++ b/internal/pkg/efivarfs/boot.go @@ -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), - ) -} diff --git a/internal/pkg/efivarfs/devicepath.go b/internal/pkg/efivarfs/devicepath.go index cee116b0b..214790cdf 100644 --- a/internal/pkg/efivarfs/devicepath.go +++ b/internal/pkg/efivarfs/devicepath.go @@ -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) diff --git a/internal/pkg/efivarfs/efivarfs.go b/internal/pkg/efivarfs/efivarfs.go index 20af21f0a..54071b1bd 100644 --- a/internal/pkg/efivarfs/efivarfs.go +++ b/internal/pkg/efivarfs/efivarfs.go @@ -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)) } diff --git a/internal/pkg/efivarfs/efivarfs_test.go b/internal/pkg/efivarfs/efivarfs_test.go new file mode 100644 index 000000000..7a524cdfa --- /dev/null +++ b/internal/pkg/efivarfs/efivarfs_test.go @@ -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") +} diff --git a/internal/pkg/efivarfs/mock.go b/internal/pkg/efivarfs/mock.go new file mode 100644 index 000000000..5a4a304fd --- /dev/null +++ b/internal/pkg/efivarfs/mock.go @@ -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 +} diff --git a/internal/pkg/efivarfs/osindications.go b/internal/pkg/efivarfs/osindications.go deleted file mode 100644 index 5dcff2ec8..000000000 --- a/internal/pkg/efivarfs/osindications.go +++ /dev/null @@ -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 -} diff --git a/internal/pkg/efivarfs/variables.go b/internal/pkg/efivarfs/variables.go index a075e8f20..8e63147de 100644 --- a/internal/pkg/efivarfs/variables.go +++ b/internal/pkg/efivarfs/variables.go @@ -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) }