blob: 1606fd64ddfd41fdba86c00cb462ab03164be7e6 [file] [log] [blame]
Lorenz Brunca1cff02023-06-26 17:52:44 +02001package efivarfs
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "math"
9 "strings"
10
11 "github.com/google/uuid"
12
13 "source.monogon.dev/metropolis/pkg/msguid"
14)
15
16// DevicePath represents a path consisting of one or more elements to an
17// entity implementing an EFI protocol. It's very broadly used inside EFI
18// for representing all sorts of abstract paths. In the context of this
19// package it is used to represent paths to EFI loaders.
20// See https://uefi.org/specs/UEFI/2.10/10_Protocols_Device_Path_Protocol.html
21// for more information.
22type DevicePath []DevicePathElem
23
24// DevicePathElem is a common interface for all UEFI device path elements.
25type DevicePathElem interface {
26 typ() uint8
27 subType() uint8
28 data() ([]byte, error)
29}
30
31type pathElemUnmarshalFunc func([]byte) (DevicePathElem, error)
32
33// PartitionMBR matches a drive or partition formatted with legacy MBR
34// (Master Boot Record).
35type PartitionMBR struct {
36 // DiskSignature contains a 4-byte signature identifying the drive, located
37 // just after the 440 bytes of boot sector loading code.
38 // Note that since MBR does not have per-partition signatures, this is
39 // combined with PartitionNumber to select a partition.
40 DiskSignature [4]byte
41}
42
43func (p PartitionMBR) partitionSignature() (sig [16]byte) {
44 copy(sig[:4], p.DiskSignature[:])
45 return
46}
47
48func (p PartitionMBR) partitionFormat() uint8 {
49 return 0x01
50}
51
52func (p PartitionMBR) signatureType() uint8 {
53 return 0x01
54}
55
56// PartitionGPT matches a partition on a drive formatted with GPT.
57type PartitionGPT struct {
58 // UUID of the partition to be matched. Conversion into mixed-endian format
59 // is taken care of, a standard big-endian UUID can be put in here.
60 PartitionUUID uuid.UUID
61}
62
63func (p PartitionGPT) partitionSignature() [16]byte {
64 return msguid.From(p.PartitionUUID)
65}
66
67func (p PartitionGPT) partitionFormat() uint8 {
68 return 0x02
69}
70
71func (p PartitionGPT) signatureType() uint8 {
72 return 0x02
73}
74
75// PartitionUnknown is being used to represent unknown partitioning schemas or
76// combinations of PartitionFormat/SignatureType. It contains raw uninterpreted
77// data.
78type PartitionUnknown struct {
79 PartitionSignature [16]byte
80 PartitionFormat uint8
81 SignatureType uint8
82}
83
84func (p PartitionUnknown) partitionSignature() [16]byte {
85 return p.PartitionSignature
86}
87
88func (p PartitionUnknown) partitionFormat() uint8 {
89 return p.PartitionFormat
90}
91
92func (p PartitionUnknown) signatureType() uint8 {
93 return p.SignatureType
94}
95
96type PartitionMatch interface {
97 partitionSignature() [16]byte
98 partitionFormat() uint8
99 signatureType() uint8
100}
101
102// HardDrivePath matches whole drives or partitions on GPT/MBR formatted
103// drives.
104type HardDrivePath struct {
105 // Partition number, starting at 1. If zero or unset, the whole drive is
106 // selected.
107 PartitionNumber uint32
108 // Block address at which the partition starts. Not used for matching
109 // partitions in EDK2.
110 PartitionStartBlock uint64
111 // Number of blocks occupied by the partition starting from the
112 // PartitionStartBlock. Not used for matching partitions in EDK2.
113 PartitionSizeBlocks uint64
114 // PartitionMatch is used to match drive or partition signatures.
115 // Use PartitionMBR and PartitionGPT types here.
116 PartitionMatch PartitionMatch
117}
118
119func (h *HardDrivePath) typ() uint8 {
120 return 4
121}
122
123func (h *HardDrivePath) subType() uint8 {
124 return 1
125}
126
127func (h *HardDrivePath) data() ([]byte, error) {
128 out := make([]byte, 38)
129 le := binary.LittleEndian
130 le.PutUint32(out[0:4], h.PartitionNumber)
131 le.PutUint64(out[4:12], h.PartitionStartBlock)
132 le.PutUint64(out[12:20], h.PartitionSizeBlocks)
133 if h.PartitionMatch == nil {
134 return nil, errors.New("PartitionMatch needs to be set")
135 }
136 sig := h.PartitionMatch.partitionSignature()
137 copy(out[20:36], sig[:])
138 out[36] = h.PartitionMatch.partitionFormat()
139 out[37] = h.PartitionMatch.signatureType()
140 return out, nil
141}
142
143func unmarshalHardDrivePath(data []byte) (DevicePathElem, error) {
144 var h HardDrivePath
145 if len(data) != 38 {
146 return nil, fmt.Errorf("invalid HardDrivePath element, expected 38 bytes, got %d", len(data))
147 }
148 le := binary.LittleEndian
149 h.PartitionNumber = le.Uint32(data[0:4])
150 h.PartitionStartBlock = le.Uint64(data[4:12])
151 h.PartitionSizeBlocks = le.Uint64(data[12:20])
152 partitionFormat := data[36]
153 signatureType := data[37]
154 var rawSig [16]byte
155 copy(rawSig[:], data[20:36])
156 switch {
157 case partitionFormat == 1 && signatureType == 1:
158 // MBR
159 var mbr PartitionMBR
160 copy(mbr.DiskSignature[:], rawSig[:4])
161 h.PartitionMatch = mbr
162 case partitionFormat == 2 && signatureType == 2:
163 // GPT
164 h.PartitionMatch = PartitionGPT{
165 PartitionUUID: msguid.To(rawSig),
166 }
167 default:
168 // Unknown
169 h.PartitionMatch = PartitionUnknown{
170 PartitionSignature: rawSig,
171 PartitionFormat: partitionFormat,
172 SignatureType: signatureType,
173 }
174 }
175 return &h, nil
176}
177
178// FilePath contains a backslash-separated path or part of a path to a file on
179// a filesystem.
180type FilePath string
181
182func (f FilePath) typ() uint8 {
183 return 4
184}
185
186func (f FilePath) subType() uint8 {
187 return 4
188}
189
190func (f FilePath) data() ([]byte, error) {
191 if strings.IndexByte(string(f), 0x00) != -1 {
192 return nil, fmt.Errorf("contains invalid null bytes")
193 }
194 withBackslashes := bytes.ReplaceAll([]byte(f), []byte(`/`), []byte(`\`))
195 out, err := Encoding.NewEncoder().Bytes(withBackslashes)
196 if err != nil {
197 return nil, fmt.Errorf("failed to encode FilePath to UTF-16: %w", err)
198 }
199 return append(out, 0x00, 0x00), nil
200}
201
202func unmarshalFilePath(data []byte) (DevicePathElem, error) {
203 if len(data) < 2 {
204 return nil, fmt.Errorf("FilePath must be at least 2 bytes because of UTF-16 null terminator")
205 }
206 out, err := Encoding.NewDecoder().Bytes(data)
207 if err != nil {
208 return nil, fmt.Errorf("error decoding FilePath UTF-16 string: %w", err)
209 }
210 nullIdx := bytes.IndexByte(out, 0x00)
211 if nullIdx != len(out)-1 {
212 return nil, fmt.Errorf("FilePath not properly null-terminated")
213 }
214 withoutBackslashes := strings.Replace(string(out[:len(out)-1]), `\`, `/`, -1)
215 return FilePath(withoutBackslashes), nil
216}
217
218// Map key contains type and subtype
219var pathElementUnmarshalMap = map[[2]byte]pathElemUnmarshalFunc{
220 {4, 1}: unmarshalHardDrivePath,
221 {4, 4}: unmarshalFilePath,
222}
223
224// UnknownPath is a generic structure for all types of path elements not
225// understood by this library. The UEFI-specified set of path element
226// types is vast and mostly unused, this generic type allows for parsing as
227// well as pass-through of not-understood path elements.
228type UnknownPath struct {
229 TypeVal uint8
230 SubTypeVal uint8
231 DataVal []byte
232}
233
234func (u UnknownPath) typ() uint8 {
235 return u.TypeVal
236}
237
238func (u UnknownPath) subType() uint8 {
239 return u.SubTypeVal
240}
241
242func (u UnknownPath) data() ([]byte, error) {
243 return u.DataVal, nil
244}
245
246// Marshal encodes the device path in binary form.
247func (d DevicePath) Marshal() ([]byte, error) {
248 var buf []byte
249 for _, p := range d {
250 buf = append(buf, p.typ(), p.subType())
251 elemBuf, err := p.data()
252 if err != nil {
253 return nil, fmt.Errorf("failed marshaling path element: %w", err)
254 }
255 // 4 is size of header which is included in length field
256 if len(elemBuf)+4 > math.MaxUint16 {
257 return nil, fmt.Errorf("path element payload over maximum size")
258 }
259 buf = append16(buf, uint16(len(elemBuf)+4))
260 buf = append(buf, elemBuf...)
261 }
262 // End of device path (Type 0x7f, SubType 0xFF)
263 buf = append(buf, 0x7f, 0xff, 0x04, 0x00)
264 return buf, nil
265}
266
267// UnmarshalDevicePath parses a binary device path.
268func UnmarshalDevicePath(data []byte) (DevicePath, error) {
269 rest := data
270 var p DevicePath
271 for {
272 if len(rest) < 4 {
273 if len(rest) != 0 {
274 return nil, fmt.Errorf("dangling bytes at the end of device path: %x", rest)
275 }
276 break
277 }
278 t := rest[0]
279 subT := rest[1]
280 dataLen := binary.LittleEndian.Uint16(rest[2:4])
281 if int(dataLen) > len(rest) {
282 return nil, fmt.Errorf("path element larger than rest of buffer: %d > %d", dataLen, len(rest))
283 }
284 if dataLen < 4 {
285 return nil, fmt.Errorf("path element must be at least 4 bytes (header), length indicates %d", dataLen)
286 }
287 elemData := rest[4:dataLen]
288 rest = rest[dataLen:]
289
290 unmarshal, ok := pathElementUnmarshalMap[[2]byte{t, subT}]
291 if !ok {
292 p = append(p, &UnknownPath{
293 TypeVal: t,
294 SubTypeVal: subT,
295 DataVal: elemData,
296 })
297 continue
298 }
299 elem, err := unmarshal(elemData)
300 if err != nil {
301 return nil, fmt.Errorf("failed decoding path element %d: %w", len(p), err)
302 }
303 p = append(p, elem)
304 }
305 var endOfPathIdx int
306 for i, e := range p {
307 if e.typ() == 0x7f && e.subType() == 0xff {
308 endOfPathIdx = i
309 break
310 }
311 }
312 switch {
313 case len(p) == 0:
314 return nil, errors.New("empty DevicePath without End Of Path element")
315 case endOfPathIdx == -1:
316 return nil, fmt.Errorf("got DevicePath with %d elements, but without End Of Path element", len(p))
317 case endOfPathIdx != len(p)-1:
318 return nil, fmt.Errorf("got DevicePath with %d elements with End Of Path element at %d (wanted as last element)", len(p), endOfPathIdx)
319 }
320 p = p[:len(p)-1]
321
322 return p, nil
323}