File: vars_linux.go

package info (click to toggle)
golang-github-canonical-go-efilib 1.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 6,836 kB
  • sloc: makefile: 3
file content (346 lines) | stat: -rw-r--r-- 10,225 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// Copyright 2020-2021 Canonical Ltd.
// Licensed under the LGPLv3 with static-linking exception.
// See LICENCE file for details.

package efi

import (
	"bytes"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"syscall"

	"golang.org/x/sys/unix"

	internal_unix "github.com/canonical/go-efilib/internal/unix"
)

func efivarfsPath() string {
	return "/sys/firmware/efi/efivars"
}

type varFile interface {
	io.ReadWriteCloser
	Readdir(n int) ([]os.FileInfo, error)
	Stat() (os.FileInfo, error)
	GetInodeFlags() (uint, error)
	SetInodeFlags(flags uint) error
}

func makeVarFileMutableAndTakeFile(f varFile) (restore func() error, err error) {
	const immutableFlag = 0x00000010

	flags, err := f.GetInodeFlags()
	if err != nil {
		return nil, err
	}

	if flags&immutableFlag == 0 {
		// Nothing to do
		f.Close()
		return func() error { return nil }, nil
	}

	if err := f.SetInodeFlags(flags &^ immutableFlag); err != nil {
		return nil, err
	}

	return func() error {
		defer func() {
			f.Close()
		}()
		return f.SetInodeFlags(flags)
	}, nil
}

type realVarFile struct {
	*os.File
}

func (f *realVarFile) GetInodeFlags() (uint, error) {
	flags, err := internal_unix.IoctlGetUint(int(f.Fd()), unix.FS_IOC_GETFLAGS)
	if err != nil {
		return 0, &os.PathError{Op: "ioctl", Path: f.Name(), Err: err}
	}
	return flags, nil
}

func (f *realVarFile) SetInodeFlags(flags uint) error {
	if err := internal_unix.IoctlSetPointerUint(int(f.Fd()), unix.FS_IOC_SETFLAGS, flags); err != nil {
		return &os.PathError{Op: "ioctl", Path: f.Name(), Err: err}
	}
	return nil
}

var openVarFile = func(path string, flags int, perm os.FileMode) (varFile, error) {
	f, err := os.OpenFile(path, flags, perm)
	if err != nil {
		return nil, err
	}
	return &realVarFile{f}, nil
}

var guidLength = len("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")

func probeEfivarfs() bool {
	var st unix.Statfs_t
	if err := unixStatfs(efivarfsPath(), &st); err != nil {
		return false
	}
	if uint(st.Type) != uint(unix.EFIVARFS_MAGIC) {
		return false
	}
	return true
}

func maybeRetry(n int, fn func() (bool, error)) error {
	for i := 1; ; i++ {
		retry, err := fn()
		switch {
		case i > n:
			return err
		case !retry:
			return err
		case err == nil:
			return nil
		}
	}
}

// inodeMayBeImmutable returns whether the supplied error returned from open (for
// writing) or unlink indicates that the inode is immutable. This is indicated
// when the error is EPERM.
//
// We retry for EPERM errors that occur when opening an inode for writing,
// or when unlinking its directory entry. Although we temporarily mark inodes as
// mutable before opening them to write or when unlinking, we can get this error
// as a result of race with another process that might have been writing to the
// variable (and subsequently marked the inode as immutable again when it
// finished) or may have deleted and recreated it, making the new inode immutable.
func inodeMayBeImmutable(err error) bool {
	var errno syscall.Errno
	if !errors.As(err, &errno) {
		return false
	}

	return errno == syscall.EPERM
}

func transformEfivarfsError(err error) error {
	switch {
	case errors.Is(err, os.ErrNotExist) || err == io.EOF:
		// ENOENT can come from the VFS layer during opening and unlinking, and
		// converted from EFI_NOT_FOUND errors returned from the variable
		// service. When reading a variable, if the variable doesn't exist
		// when trying to determine the size of it, the kernel converts ENOENT
		// into success with 0 bytes read which means we need to handle io.EOF
		// as well.
		return ErrVarNotExist
	case errors.Is(err, syscall.EINVAL):
		// EINVAL can come from the VFS layer during opening or unlinking due
		// to invalid or incompatible flag combinations, although we don't expect
		// that. It's also converted from EFI_INVALID_PARAMETER errors returned
		// from the variable service.
		return ErrVarInvalidParam
	case errors.Is(err, syscall.EIO):
		// EIO can come from the VFS layer during unlinking, although we don't
		// expect that. It's also converted from EFI_DEVICE_ERROR errors returned
		// from the variable service
		return ErrVarDeviceError
	case errors.Is(err, os.ErrPermission):
		// EACCESS can come from the VFS layer for the following reasons:
		// - opening a file for writing when the caller does not have write
		//   access to it.
		// - opening a file for writing when the caller does not have write
		//   access to the parent directory and a new file needs to be created.
		// - unlinking a file when the caller does not have write access to
		//   the parent directory.
		// EPERM can come from the VFS layer when opening a file for writing
		// or unlinking if the inode is immutable (see inodeMayBeImmutable).
		//
		// EACCES is also converted from EFI_SECURITY_VIOLATION errors returned
		// from the variable service.
		return ErrVarPermission
	case errors.Is(err, syscall.ENOSPC):
		// ENOSPC is converted from EFI_OUT_OF_RESOURCES errors returned from
		// the variable service.
		return ErrVarInsufficientSpace
	case errors.Is(err, syscall.EROFS):
		// EROFS is converted from EFI_WRITE_PROTECTED errors returned from the
		// variable service.
		return ErrVarWriteProtected
	default:
		return err
	}
}

func writeEfivarfsFile(path string, attrs VariableAttributes, data []byte) (retry bool, err error) {
	// Open for reading to make the inode mutable
	r, err := openVarFile(path, os.O_RDONLY, 0)
	switch {
	case errors.Is(err, os.ErrNotExist):
		// It's not an error if the variable doesn't exist.
	case err != nil:
		return false, transformEfivarfsError(err)
	default:
		restoreImmutable, err := makeVarFileMutableAndTakeFile(r)
		if err != nil {
			r.Close()
			return false, transformEfivarfsError(err)
		}

		defer restoreImmutable()
	}

	if len(data) == 0 {
		// short-cut for unauthenticated variable delete - efivarfs will perform a
		// zero-byte write to delete the variable if we unlink the entry here.
		if attrs&(AttributeAuthenticatedWriteAccess|AttributeTimeBasedAuthenticatedWriteAccess|AttributeEnhancedAuthenticatedAccess) > 0 {
			// If the supplied attributes are incompatible with the variable,
			// the variable service will return EFI_INVALID_PARAMETER and
			// we'll get EINVAL back. If the supplied attributes are correct
			// but we perform a zero-byte write to an authenticated variable,
			// the variable service will return EFI_SECURITY_VIOLATION, but
			// the kernel also turns this into EINVAL. Instead, we generate
			// an appropriate error if the supplied attributes indicate that
			// the variable is authenticated.
			return false, ErrVarPermission
		}
		if err := removeVarFile(path); err != nil {
			switch {
			case errors.Is(err, os.ErrNotExist):
				// It's not an error if the variable doesn't exist.
				return false, nil
			case inodeMayBeImmutable(err):
				// Try again
				return true, transformEfivarfsError(err)
			default:
				// Don't try again
				return false, transformEfivarfsError(err)
			}
		}
		return false, nil
	}

	flags := os.O_WRONLY | os.O_CREATE
	if attrs&AttributeAppendWrite != 0 {
		flags |= os.O_APPEND
	}

	w, err := openVarFile(path, flags, 0644)
	switch {
	case inodeMayBeImmutable(err):
		// Try again
		return true, transformEfivarfsError(err)
	case err != nil:
		// Don't try again
		return false, transformEfivarfsError(err)
	}
	defer w.Close()

	var buf bytes.Buffer
	binary.Write(&buf, binary.LittleEndian, attrs)
	buf.Write(data)

	_, err = buf.WriteTo(w)
	return false, transformEfivarfsError(err)
}

type efivarfsVarsBackend struct{}

func (v efivarfsVarsBackend) Get(name string, guid GUID) (VariableAttributes, []byte, error) {
	path := filepath.Join(efivarfsPath(), fmt.Sprintf("%s-%s", name, guid))
	f, err := openVarFile(path, os.O_RDONLY, 0)
	if err != nil {
		return 0, nil, transformEfivarfsError(err)
	}
	defer f.Close()

	// Read the entire payload in a single read, as that's how
	// GetVariable works and is the only way the kernel can obtain
	// the variable contents. If we perform multiple reads, the
	// kernel still has to obtain the entire variable contents
	// each time. To do this, we need to know the size of the variable
	// contents, which we can obtain from the inode.
	fi, err := f.Stat()
	if err != nil {
		return 0, nil, err
	}
	if fi.Size() < 4 {
		return 0, nil, ErrVarNotExist
	}

	buf := make([]byte, fi.Size())
	if _, err := f.Read(buf); err != nil {
		return 0, nil, transformEfivarfsError(err)
	}

	return VariableAttributes(binary.LittleEndian.Uint32(buf)), buf[4:], nil
}

func (v efivarfsVarsBackend) Set(name string, guid GUID, attrs VariableAttributes, data []byte) error {
	path := filepath.Join(efivarfsPath(), fmt.Sprintf("%s-%s", name, guid))
	return maybeRetry(4, func() (bool, error) { return writeEfivarfsFile(path, attrs, data) })
}

func (v efivarfsVarsBackend) List() ([]VariableDescriptor, error) {
	f, err := openVarFile(efivarfsPath(), os.O_RDONLY, 0)
	switch {
	case errors.Is(err, os.ErrNotExist):
		return nil, ErrVarsUnavailable
	case err != nil:
		return nil, transformEfivarfsError(err)
	}
	defer f.Close()

	dirents, err := f.Readdir(-1)
	if err != nil {
		return nil, err
	}

	var entries []VariableDescriptor

	for _, dirent := range dirents {
		if !dirent.Mode().IsRegular() {
			// Skip non-regular files
			continue
		}
		if len(dirent.Name()) < guidLength+1 {
			// Skip files with a basename that isn't long enough
			// to contain a GUID and a hyphen
			continue
		}
		if dirent.Name()[len(dirent.Name())-guidLength-1] != '-' {
			// Skip files where the basename doesn't contain a
			// hyphen between the name and GUID
			continue
		}
		if dirent.Size() == 0 {
			// Skip files with zero size. These are variables that
			// have been deleted by writing an empty payload
			continue
		}

		name := dirent.Name()[:len(dirent.Name())-guidLength-1]
		guid, err := DecodeGUIDString(dirent.Name()[len(name)+1:])
		if err != nil {
			continue
		}

		entries = append(entries, VariableDescriptor{Name: name, GUID: guid})
	}

	return entries, nil
}

func addDefaultVarsBackend(ctx context.Context) context.Context {
	if !probeEfivarfs() {
		return withVarsBackend(ctx, nullVarsBackend{})
	}
	return withVarsBackend(ctx, efivarfsVarsBackend{})
}