Skip to content

Commit cd82ee2

Browse files
smirashanduur
authored andcommitted
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> (cherry picked from commit ea4ed16)
1 parent 996d97d commit cd82ee2

File tree

8 files changed

+330
-177
lines changed

8 files changed

+330
-177
lines changed

internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
"github.com/siderolabs/gen/xslices"
1414
"github.com/siderolabs/go-blockdevice/v2/blkid"
15-
"golang.org/x/sys/unix"
1615

1716
"github.com/siderolabs/talos/internal/pkg/efivarfs"
1817
"github.com/siderolabs/talos/pkg/machinery/constants"
@@ -37,7 +36,14 @@ const (
3736

3837
// ReadVariable reads a SystemdBoot EFI variable.
3938
func ReadVariable(name string) (string, error) {
40-
data, _, err := efivarfs.Read(efivarfs.ScopeSystemd, name)
39+
efi, err := efivarfs.NewFilesystemReaderWriter(false)
40+
if err != nil {
41+
return "", fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
42+
}
43+
44+
defer efi.Close() //nolint:errcheck
45+
46+
data, _, err := efi.Read(efivarfs.ScopeSystemd, name)
4147
if err != nil {
4248
// if the variable does not exist, return an empty string
4349
if errors.Is(err, os.ErrNotExist) {
@@ -65,12 +71,12 @@ func ReadVariable(name string) (string, error) {
6571

6672
// WriteVariable reads a SystemdBoot EFI variable.
6773
func WriteVariable(name, value string) error {
68-
// mount EFI vars as rw
69-
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
70-
return err
74+
efi, err := efivarfs.NewFilesystemReaderWriter(true)
75+
if err != nil {
76+
return fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
7177
}
7278

73-
defer unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "") //nolint:errcheck
79+
defer efi.Close() //nolint:errcheck
7480

7581
out := make([]byte, (len(value)+1)*2)
7682

@@ -83,7 +89,7 @@ func WriteVariable(name, value string) error {
8389

8490
out = append(out[:n], 0, 0)
8591

86-
return efivarfs.Write(efivarfs.ScopeSystemd, name, efivarfs.AttrBootserviceAccess|efivarfs.AttrRuntimeAccess|efivarfs.AttrNonVolatile, out)
92+
return efi.Write(efivarfs.ScopeSystemd, name, efivarfs.AttrBootserviceAccess|efivarfs.AttrRuntimeAccess|efivarfs.AttrNonVolatile, out)
8793
}
8894

8995
// 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 {
9298
//
9399
//nolint:gocyclo
94100
func CreateBootEntry(installDisk, sdBootFilePath string) error {
95-
// mount EFI vars as rw
96-
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
97-
return err
101+
efi, err := efivarfs.NewFilesystemReaderWriter(true)
102+
if err != nil {
103+
return fmt.Errorf("failed to create efivarfs reader/writer: %w", err)
98104
}
99105

100-
defer unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "") //nolint:errcheck
106+
defer efi.Close() //nolint:errcheck
101107

102-
rawBootOrderData, _, err := efivarfs.Read(efivarfs.ScopeGlobal, "BootOrder")
108+
rawBootOrderData, _, err := efi.Read(efivarfs.ScopeGlobal, "BootOrder")
103109
if err != nil {
104110
return fmt.Errorf("failed to read BootOrder: %w", err)
105111
}
@@ -113,7 +119,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
113119
talosBootIndex := len(bootOrder)
114120

115121
for _, idx := range bootOrder {
116-
bootEntry, err := efivarfs.GetBootEntry(int(idx))
122+
bootEntry, err := efivarfs.GetBootEntry(efi, int(idx))
117123
if err != nil {
118124
return fmt.Errorf("failed to get boot entry %d: %w", idx, err)
119125
}
@@ -150,7 +156,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
150156
return fmt.Errorf("EFI partition UUID not found on install disk %q", installDisk)
151157
}
152158

153-
if err := efivarfs.SetBootEntry(talosBootIndex, &efivarfs.LoadOption{
159+
if err := efivarfs.SetBootEntry(efi, talosBootIndex, &efivarfs.LoadOption{
154160
Description: TalosBootEntryDescription,
155161
FilePath: efivarfs.DevicePath{
156162
&efivarfs.HardDrivePath{
@@ -167,7 +173,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
167173
return fmt.Errorf("failed to set boot entry %d: %w", talosBootIndex, err)
168174
}
169175

170-
currentBootOrder, err := efivarfs.GetBootOrder()
176+
currentBootOrder, err := efivarfs.GetBootOrder(efi)
171177
if err != nil {
172178
return fmt.Errorf("failed to get current BootOrder: %w", err)
173179
}
@@ -177,7 +183,7 @@ func CreateBootEntry(installDisk, sdBootFilePath string) error {
177183
return nil
178184
}
179185

180-
if err := efivarfs.SetBootOrder(slices.Concat([]uint16{uint16(talosBootIndex)}, currentBootOrder)); err != nil {
186+
if err := efivarfs.SetBootOrder(efi, slices.Concat([]uint16{uint16(talosBootIndex)}, currentBootOrder)); err != nil {
181187
return fmt.Errorf("failed to set BootOrder: %w", err)
182188
}
183189

internal/pkg/efivarfs/boot.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func (e *LoadOption) Marshal() ([]byte, error) {
7474
attrs |= 0x01
7575
}
7676

77-
data = append32(data, attrs)
77+
data = binary.LittleEndian.AppendUint32(data, attrs)
7878

7979
filePathRaw, err := e.FilePath.Marshal()
8080
if err != nil {
@@ -94,7 +94,7 @@ func (e *LoadOption) Marshal() ([]byte, error) {
9494
return nil, fmt.Errorf("failed marshaling FilePath/ExtraPath: value too big (%d)", len(filePathRaw))
9595
}
9696

97-
data = append16(data, uint16(len(filePathRaw)))
97+
data = binary.LittleEndian.AppendUint16(data, uint16(len(filePathRaw)))
9898

9999
if strings.IndexByte(e.Description, 0x00) != -1 {
100100
return nil, fmt.Errorf("failed to encode Description: contains invalid null bytes")
@@ -178,8 +178,9 @@ type BootOrder []uint16
178178
// Marshal generates the binary representation of a BootOrder.
179179
func (t *BootOrder) Marshal() []byte {
180180
var out []byte
181+
181182
for _, v := range *t {
182-
out = append16(out, v)
183+
out = binary.LittleEndian.AppendUint16(out, v)
183184
}
184185

185186
return out
@@ -195,24 +196,8 @@ func UnmarshalBootOrder(d []byte) (BootOrder, error) {
195196

196197
out := make(BootOrder, l)
197198
for i := range l {
198-
out[i] = uint16(d[2*i]) | uint16(d[2*i+1])<<8
199+
out[i] = binary.LittleEndian.Uint16(d[i*2:])
199200
}
200201

201202
return out, nil
202203
}
203-
204-
func append16(d []byte, v uint16) []byte {
205-
return append(d,
206-
byte(v&0xFF),
207-
byte(v>>8&0xFF),
208-
)
209-
}
210-
211-
func append32(d []byte, v uint32) []byte {
212-
return append(d,
213-
byte(v&0xFF),
214-
byte(v>>8&0xFF),
215-
byte(v>>16&0xFF),
216-
byte(v>>24&0xFF),
217-
)
218-
}

internal/pkg/efivarfs/devicepath.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type PartitionMBR struct {
5050
func (p PartitionMBR) partitionSignature() (sig [16]byte) {
5151
copy(sig[:4], p.DiskSignature[:])
5252

53-
return
53+
return sig
5454
}
5555

5656
func (p PartitionMBR) partitionFormat() uint8 {
@@ -283,7 +283,7 @@ func (d DevicePath) Marshal() ([]byte, error) {
283283
return nil, fmt.Errorf("path element payload over maximum size")
284284
}
285285

286-
buf = append16(buf, uint16(len(elemBuf)+4))
286+
buf = binary.LittleEndian.AppendUint16(buf, uint16(len(elemBuf)+4))
287287
buf = append(buf, elemBuf...)
288288
}
289289
// End of device path (Type 0x7f, SubType 0xFF)

internal/pkg/efivarfs/efivarfs.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import (
2323

2424
"github.com/g0rbe/go-chattr"
2525
"github.com/google/uuid"
26+
"golang.org/x/sys/unix"
2627
"golang.org/x/text/encoding/unicode"
28+
29+
"github.com/siderolabs/talos/pkg/machinery/constants"
2730
)
2831

2932
const (
@@ -87,8 +90,48 @@ func varPath(scope uuid.UUID, varName string) string {
8790
return fmt.Sprintf("/sys/firmware/efi/efivars/%s-%s", varName, scope.String())
8891
}
8992

93+
// ReaderWriter is an interface for reading and writing EFI variables.
94+
type ReaderWriter interface {
95+
Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error
96+
Delete(scope uuid.UUID, varName string) error
97+
Read(scope uuid.UUID, varName string) ([]byte, Attribute, error)
98+
List(scope uuid.UUID) ([]string, error)
99+
}
100+
101+
// FilesystemReaderWriter implements ReaderWriter using the efivars Linux filesystem.
102+
type FilesystemReaderWriter struct {
103+
write bool
104+
}
105+
106+
// NewFilesystemReaderWriter creates a new FilesystemReaderWriter.
107+
func NewFilesystemReaderWriter(write bool) (*FilesystemReaderWriter, error) {
108+
if write {
109+
if err := unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT, ""); err != nil {
110+
return nil, err
111+
}
112+
}
113+
114+
return &FilesystemReaderWriter{
115+
write: write,
116+
}, nil
117+
}
118+
119+
// Close unmounts efivarfs if the FilesystemReaderWriter was created with write
120+
// access.
121+
func (rw *FilesystemReaderWriter) Close() error {
122+
if rw.write {
123+
return unix.Mount("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_REMOUNT|unix.MS_RDONLY, "")
124+
}
125+
126+
return nil
127+
}
128+
90129
// Write writes the value of the named variable in the given scope.
91-
func Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error {
130+
func (rw *FilesystemReaderWriter) Write(scope uuid.UUID, varName string, attrs Attribute, value []byte) error {
131+
if !rw.write {
132+
return errors.New("efivarfs was opened read-only")
133+
}
134+
92135
// Ref: https://docs.kernel.org/filesystems/efivarfs.html
93136
// Remove immutable attribute from the efivarfs file if it exists
94137
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
140183
}
141184

142185
// Read reads the value of the named variable in the given scope.
143-
func Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
186+
func (rw *FilesystemReaderWriter) Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
144187
val, err := os.ReadFile(varPath(scope, varName))
145188
if err != nil {
146189
e := err
@@ -162,7 +205,7 @@ func Read(scope uuid.UUID, varName string) ([]byte, Attribute, error) {
162205

163206
// List lists all variable names present for a given scope sorted by their names
164207
// in Go's "native" string sort order.
165-
func List(scope uuid.UUID) ([]string, error) {
208+
func (rw *FilesystemReaderWriter) List(scope uuid.UUID) ([]string, error) {
166209
vars, err := os.ReadDir(Path)
167210
if err != nil {
168211
return nil, fmt.Errorf("failed to list variable directory: %w", err)
@@ -189,6 +232,10 @@ func List(scope uuid.UUID) ([]string, error) {
189232

190233
// Delete deletes the given variable name in the given scope. Use with care,
191234
// some firmware fails to boot if variables it uses are deleted.
192-
func Delete(scope uuid.UUID, varName string) error {
235+
func (rw *FilesystemReaderWriter) Delete(scope uuid.UUID, varName string) error {
236+
if !rw.write {
237+
return errors.New("efivarfs was opened read-only")
238+
}
239+
193240
return os.Remove(varPath(scope, varName))
194241
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package efivarfs_test
6+
7+
import (
8+
"encoding/binary"
9+
"io/fs"
10+
"testing"
11+
12+
"github.com/google/uuid"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/siderolabs/talos/internal/pkg/efivarfs"
16+
)
17+
18+
func TestBootOrder(t *testing.T) {
19+
t.Parallel()
20+
21+
var bootOrderEntries []byte
22+
23+
for _, entry := range []int{1, 0, 2, 3} {
24+
bootOrderEntries = binary.LittleEndian.AppendUint16(bootOrderEntries, uint16(entry))
25+
}
26+
27+
efiRW := efivarfs.Mock{
28+
Variables: map[uuid.UUID]map[string]efivarfs.MockVariable{
29+
efivarfs.ScopeGlobal: {
30+
"BootOrder": {
31+
Attrs: 0,
32+
Data: bootOrderEntries,
33+
},
34+
},
35+
},
36+
}
37+
38+
vars, err := efiRW.List(efivarfs.ScopeGlobal)
39+
require.NoError(t, err)
40+
41+
require.Contains(t, vars, "BootOrder", "variable BootOrder not found")
42+
43+
bootOrder, err := efivarfs.GetBootOrder(&efiRW)
44+
require.NoError(t, err)
45+
46+
require.Equal(t, efivarfs.BootOrder([]uint16{1, 0, 2, 3}), bootOrder, "BootOrder does not match expected value")
47+
48+
require.NoError(t, efivarfs.SetBootOrder(&efiRW, efivarfs.BootOrder([]uint16{1, 0, 3})))
49+
50+
bootOrder, err = efivarfs.GetBootOrder(&efiRW)
51+
require.NoError(t, err)
52+
53+
require.Equal(t, efivarfs.BootOrder([]uint16{1, 0, 3}), bootOrder, "BootOrder does not match expected value after SetBootOrder")
54+
}
55+
56+
func TestBootEntries(t *testing.T) {
57+
t.Parallel()
58+
59+
efiRW := efivarfs.Mock{}
60+
61+
// no entries yet
62+
entries, err := efivarfs.ListBootEntries(&efiRW)
63+
require.NoError(t, err)
64+
require.Empty(t, len(entries), "expected no boot entries in empty mock")
65+
66+
// create first entry
67+
idx, err := efivarfs.AddBootEntry(&efiRW, &efivarfs.LoadOption{
68+
Description: "First Entry",
69+
FilePath: efivarfs.DevicePath{
70+
efivarfs.FilePath("/first.efi"),
71+
},
72+
})
73+
require.NoError(t, err)
74+
require.Equal(t, 0, idx, "first boot entry index should be 0")
75+
76+
// verify first entry
77+
entry, err := efivarfs.GetBootEntry(&efiRW, idx)
78+
require.NoError(t, err)
79+
require.Equal(t, "First Entry", entry.Description, "first boot entry description does not match")
80+
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/first.efi")}, entry.FilePath, "first boot entry file path does not match")
81+
82+
// create second entry
83+
require.NoError(t, efivarfs.SetBootEntry(&efiRW, 1, &efivarfs.LoadOption{
84+
Description: "Second Entry",
85+
FilePath: efivarfs.DevicePath{
86+
efivarfs.FilePath("/second.efi"),
87+
},
88+
}), "failed to set second boot entry")
89+
90+
// verify second entry
91+
entry, err = efivarfs.GetBootEntry(&efiRW, 1)
92+
require.NoError(t, err)
93+
require.Equal(t, "Second Entry", entry.Description, "second boot entry description does not match")
94+
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/second.efi")}, entry.FilePath, "second boot entry file path does not match")
95+
96+
// list all entries
97+
entries, err = efivarfs.ListBootEntries(&efiRW)
98+
require.NoError(t, err)
99+
require.Len(t, entries, 2, "expected exactly two boot entries after adding two")
100+
101+
// try overwrite first entry
102+
require.NoError(t, efivarfs.SetBootEntry(&efiRW, idx, &efivarfs.LoadOption{
103+
Description: "First Entry Overwritten",
104+
FilePath: efivarfs.DevicePath{
105+
efivarfs.FilePath("/first_overwritten.efi"),
106+
},
107+
}), "failed to overwrite first boot entry")
108+
109+
// verify first entry after overwrite
110+
entry, err = efivarfs.GetBootEntry(&efiRW, idx)
111+
require.NoError(t, err)
112+
require.Equal(t, "First Entry Overwritten", entry.Description, "first boot entry description does not match after overwrite")
113+
require.Equal(t, efivarfs.DevicePath{efivarfs.FilePath("/first_overwritten.efi")}, entry.FilePath, "first boot entry file path does not match after overwrite")
114+
115+
// verify delete non-existing entry
116+
require.ErrorIs(t, efivarfs.DeleteBootEntry(&efiRW, 42), fs.ErrNotExist, "expected ErrNoSuchEntry when deleting non-existing entry")
117+
118+
// delete second entry
119+
require.NoError(t, efivarfs.DeleteBootEntry(&efiRW, 1), "failed to delete second boot entry")
120+
121+
// verify second entry is gone
122+
_, err = efivarfs.GetBootEntry(&efiRW, 1)
123+
require.ErrorIs(t, err, fs.ErrNotExist, "expected ErrNoSuchEntry when getting deleted entry")
124+
125+
// list entries
126+
entries, err = efivarfs.ListBootEntries(&efiRW)
127+
require.NoError(t, err)
128+
require.Len(t, entries, 1, "expected exactly one boot entry after deleting one of two")
129+
}

0 commit comments

Comments
 (0)