blob: 81c50f70f319e3749a0b8096e818e892242ea1cc [file] [log] [blame]
Lorenz Brunee17d832022-10-18 12:02:45 +00001// Package gpt implements reading and writing GUID Partition Tables as specified
2// in the UEFI Specification. It only implements up to 128 partitions per table
3// (same as most other implementations) as more would require a dynamic table
4// size, significantly complicating the code for little gain.
5package gpt
6
7import (
8 "bytes"
9 "encoding/binary"
10 "errors"
11 "fmt"
12 "hash/crc32"
13 "io"
14 "sort"
15 "strings"
16 "unicode/utf16"
17
18 "github.com/google/uuid"
Lorenz Brun60d6b902023-06-20 16:02:40 +020019
20 "source.monogon.dev/metropolis/pkg/msguid"
Lorenz Brunee17d832022-10-18 12:02:45 +000021)
22
23var gptSignature = [8]byte{'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T'}
24var gptRevision uint32 = 0x00010000 // First 2 bytes major, second 2 bytes minor
25
26// See UEFI Specification 2.9 Table 5-5
27type header struct {
28 Signature [8]byte
29 Revision uint32
30 HeaderSize uint32
31 HeaderCRC32 uint32
32 _ [4]byte
33
34 HeaderBlock uint64
35 AlternateHeaderBlock uint64
36 FirstUsableBlock uint64
37 LastUsableBlock uint64
38
39 ID [16]byte
40
41 PartitionEntriesStartBlock uint64
42 PartitionEntryCount uint32
43 PartitionEntrySize uint32
44 PartitionEntriesCRC32 uint32
45}
46
47// See UEFI Specification 2.9 Table 5-6
48type partition struct {
49 Type [16]byte
50 ID [16]byte
51 FirstBlock uint64
52 LastBlock uint64
53 Attributes uint64
54 Name [36]uint16
55}
56
57var (
58 PartitionTypeEFISystem = uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
59)
60
61var zeroUUID = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
62
63// Attribute is a bitfield of attributes set on a partition. Bits 0 to 47 are
64// reserved for UEFI specification use and all current assignments are in the
65// following const block. Bits 48 to 64 are available for per-Type use by
66// the organization controlling the partition Type.
67type Attribute uint64
68
69const (
70 // AttrRequiredPartition indicates that this partition is required for the
71 // platform to function. Mostly used by vendors to mark things like recovery
72 // partitions.
73 AttrRequiredPartition = 1 << 0
74 // AttrNoBlockIOProto indicates that EFI firmware must not provide an EFI
75 // block device (EFI_BLOCK_IO_PROTOCOL) for this partition.
76 AttrNoBlockIOProto = 1 << 1
77 // AttrLegacyBIOSBootable indicates to special-purpose software outside of
78 // UEFI that this partition can be booted using a traditional PC BIOS.
79 // Don't use this unless you know that you need it specifically.
80 AttrLegacyBIOSBootable = 1 << 2
81)
82
83// PerTypeAttrs returns the top 24 bits which are reserved for custom per-Type
84// attributes. The top 8 bits of the returned uint32 are always 0.
85func (a Attribute) PerTypeAttrs() uint32 {
86 return uint32(a >> 48)
87}
88
89// SetPerTypeAttrs sets the top 24 bits which are reserved for custom per-Type
90// attributes. It does not touch the lower attributes which are specified by the
91// UEFI specification. The top 8 bits of v are silently discarded.
92func (a *Attribute) SetPerTypeAttrs(v uint32) {
93 *a &= 0x000000FF_FFFFFFFF
94 *a |= Attribute(v) << 48
95}
96
97type Partition struct {
98 // Name of the partition, will be truncated if it expands to more than 36
99 // UTF-16 code points. Not all systems can display non-BMP code points.
100 Name string
101 // Type is the type of Table partition, can either be one of the predefined
102 // constants by the UEFI specification or a custom type identifier.
103 // Note that the all-zero UUID denotes an empty partition slot, so this
104 // MUST be set to something, otherwise it is not treated as a partition.
105 Type uuid.UUID
106 // ID is a unique identifier for this specific partition. It should be
107 // changed when cloning the partition.
108 ID uuid.UUID
109 // The first logical block of the partition (inclusive)
110 FirstBlock uint64
111 // The last logical block of the partition (inclusive)
112 LastBlock uint64
113 // Bitset of attributes of this partition.
114 Attributes Attribute
115}
116
117// SizeBlocks returns the size of the partition in blocks
118func (p *Partition) SizeBlocks() uint64 {
119 return 1 + p.LastBlock - p.FirstBlock
120}
121
122// IsUnused checks if the partition is unused, i.e. it is nil or its type is
123// the null UUID.
124func (p *Partition) IsUnused() bool {
125 if p == nil {
126 return true
127 }
128 return p.Type == zeroUUID
129}
130
131type Table struct {
132 // ID is the unique identifier of this specific disk / GPT.
133 // If this is left uninitialized/all-zeroes a new random ID is automatically
134 // generated when writing.
135 ID uuid.UUID
136
137 // Data put at the start of the very first block. Gets loaded and executed
138 // by a legacy BIOS bootloader. This can be used to make GPT-partitioned
139 // disks bootable by legacy systems or display a nice error message.
140 // Maximum length is 440 bytes, if that is exceeded Write returns an error.
141 // Should be left empty if the device is not bootable and/or compatibility
142 // with BIOS booting is not required. Only useful on x86 systems.
143 BootCode []byte
144
145 // BlockSize contains the logical block size of the block device. It must
146 // be a power of two equal to or larger than 512.
147 BlockSize int64
148 // BlockCount contains the number of logical blocks on the block device.
149 // BlockCount times BlockSize is the size of the block device in bytes.
150 BlockCount int64
151
152 // Partitions contains the list of partitions in this table. This is
153 // artificially limited to 128 partitions.
154 Partitions []*Partition
155}
156type addOptions struct {
157 preferEnd bool
158 keepEmptyEntries bool
159 alignment int64
160}
161
162// AddOption is a bitset controlling various
163type AddOption func(*addOptions)
164
165// WithPreferEnd tries to put the partition as close to the end as possible
166// instead of as close to the start.
167func WithPreferEnd() AddOption {
168 return func(options *addOptions) {
169 options.preferEnd = true
170 }
171}
172
173// WithKeepEmptyEntries does not fill up empty entries which are followed by
174// filled ones. It always appends the partition after the last used entry.
175// Without this flag, the partition is placed in the first empty entry.
176func WithKeepEmptyEntries() AddOption {
177 return func(options *addOptions) {
178 options.keepEmptyEntries = true
179 }
180}
181
182// WithAlignment allows aligning the partition start block to a non-default
183// value. By default, these are aligned to 1MiB.
Lorenz Brun60d6b902023-06-20 16:02:40 +0200184// Only use this flag if you are certain you need it, it can cause quite severe
Lorenz Brunee17d832022-10-18 12:02:45 +0000185// performance degradation under certain conditions.
186func WithAlignment(alignmenet int64) AddOption {
187 return func(options *addOptions) {
188 options.alignment = alignmenet
189 }
190}
191
192// AddPartition takes a pointer to a partition and adds it, placing it into
193// the first (or last using WithPreferEnd) continuous free space which fits it.
194// It writes the placement information (FirstBlock, LastBlock) back to p.
195// By default, AddPartition aligns FirstBlock to 1MiB boundaries, but this can
196// be overridden using WithAlignment.
197func (g *Table) AddPartition(p *Partition, size int64, options ...AddOption) error {
198 if g.BlockSize < 512 {
199 return errors.New("block size is smaller than 512 bytes, this is unsupported")
200 }
201 var opts addOptions
202 // Align to 1MiB or the block size, whichever is bigger
203 opts.alignment = 1 * 1024 * 1024
204 if g.BlockSize > opts.alignment {
205 opts.alignment = g.BlockSize
206 }
207 for _, o := range options {
208 o(&opts)
209 }
210 if opts.alignment%g.BlockSize != 0 {
211 return fmt.Errorf("requested alignment (%d bytes) is not an integer multiple of the block size (%d), unable to align", opts.alignment, g.BlockSize)
212 }
213 // Number of blocks the partition should occupy, rounded up.
214 blocks := (size + g.BlockSize - 1) / g.BlockSize
215 fs, _, err := g.GetFreeSpaces()
216 if err != nil {
217 return fmt.Errorf("unable to determine free space: %v", err)
218 }
219 if opts.preferEnd {
220 // Reverse fs slice to start iteration at the end
221 for i, j := 0, len(fs)-1; i < j; i, j = i+1, j-1 {
222 fs[i], fs[j] = fs[j], fs[i]
223 }
224 }
225 var maxFreeBlocks int64
226 for _, freeInt := range fs {
227 start := freeInt[0]
228 end := freeInt[1]
229 freeBlocks := end - start
230 // Align start properly
231 paddingBlocks := (-start) % (opts.alignment / g.BlockSize)
232 freeBlocks -= paddingBlocks
233 start += paddingBlocks
234 if maxFreeBlocks < freeBlocks {
235 maxFreeBlocks = freeBlocks
236 }
237 if freeBlocks >= blocks {
238 if !opts.preferEnd {
239 p.FirstBlock = uint64(start)
240 p.LastBlock = uint64(start + blocks)
241 } else {
242 // Realign FirstBlock. This will always succeed as
243 // there is enough space to align to the start.
244 moveLeft := (end - blocks - 1) % (opts.alignment / g.BlockSize)
245 p.FirstBlock = uint64(end - (blocks + 1 + moveLeft))
246 p.LastBlock = uint64(end - (1 + moveLeft))
247 }
248 newPartPos := -1
249 if !opts.keepEmptyEntries {
250 for i, part := range g.Partitions {
251 if part.IsUnused() {
252 newPartPos = i
253 break
254 }
255 }
256 }
257 if newPartPos == -1 {
258 g.Partitions = append(g.Partitions, p)
259 } else {
260 g.Partitions[newPartPos] = p
261 }
262 return nil
263 }
264 }
265
266 return fmt.Errorf("no space for partition of %d blocks, largest continuous free space after alignment is %d blocks", blocks, maxFreeBlocks)
267}
268
269// FirstUsableBlock returns the first usable (i.e. a partition can start there)
270// block.
271func (g *Table) FirstUsableBlock() int64 {
272 partitionEntryBlocks := (16384 + g.BlockSize - 1) / g.BlockSize
273 return 2 + partitionEntryBlocks
274}
275
276// LastUsableBlock returns the last usable (i.e. a partition can end there)
277// block. This block is inclusive.
278func (g *Table) LastUsableBlock() int64 {
279 partitionEntryBlocks := (16384 + g.BlockSize - 1) / g.BlockSize
280 return g.BlockCount - (2 + partitionEntryBlocks)
281}
282
283// GetFreeSpaces returns a slice of tuples, each containing a half-closed
284// interval of logical blocks not occupied by the GPT itself or any partition.
285// The returned intervals are always in ascending order as well as
286// non-overlapping. It also returns if it detected any overlaps between
287// partitions or partitions and the GPT. It returns an error if and only if any
288// partition has its FirstBlock before the LastBlock or exceeds the amount of
289// blocks on the block device.
290//
291// Note that the most common use cases for this function are covered by
292// AddPartition, you're encouraged to use it instead.
293func (g *Table) GetFreeSpaces() ([][2]int64, bool, error) {
294 // This implements an efficient algorithm for finding free intervals given
295 // a set of potentially overlapping occupying intervals. It uses O(n*log n)
296 // time for n being the amount of intervals, i.e. partitions. It uses O(n)
297 // additional memory. This makes it de facto infinitely scalable in the
298 // context of partition tables as the size of the block device is not part
299 // of its cyclomatic complexity and O(n*log n) is tiny for even very big
300 // partition tables.
301
302 // startBlocks contains the start blocks (inclusive) of all occupied
303 // intervals.
304 var startBlocks []int64
305 // endBlocks contains the end blocks (exclusive!) of all occupied intervals.
306 // The interval at index i is given by [startBlock[i], endBlock[i]).
307 var endBlocks []int64
308
309 // Reserve the primary GPT interval including the protective MBR.
310 startBlocks = append(startBlocks, 0)
311 endBlocks = append(endBlocks, g.FirstUsableBlock())
312
313 // Reserve the alternate GPT interval (needs +1 for exclusive interval)
314 startBlocks = append(startBlocks, g.LastUsableBlock()+1)
315 endBlocks = append(endBlocks, g.BlockCount)
316
317 for i, part := range g.Partitions {
318 if part.IsUnused() {
319 continue
320 }
321 // Bail if partition does not contain a valid interval. These are open
322 // intervals, thus part.FirstBlock == part.LastBlock denotes a valid
323 // partition with a size of one block.
324 if part.FirstBlock > part.LastBlock {
325 return nil, false, fmt.Errorf("partition %d has a LastBlock smaller than its FirstBlock, its interval is [%d, %d]", i, part.FirstBlock, part.LastBlock)
326 }
327 if part.FirstBlock >= uint64(g.BlockCount) || part.LastBlock >= uint64(g.BlockCount) {
328 return nil, false, fmt.Errorf("partition %d exceeds the block count of the block device", i)
329 }
330 startBlocks = append(startBlocks, int64(part.FirstBlock))
331 // Algorithm needs open-closed intervals, thus add +1 to the end.
332 endBlocks = append(endBlocks, int64(part.LastBlock)+1)
333 }
334 // Sort both sets of blocks independently in ascending order. Note that it
335 // is now no longer possible to extract the original intervals. Integers
336 // have no identity thus it doesn't matter if the sort is stable or not.
337 sort.Slice(startBlocks, func(i, j int) bool { return startBlocks[i] < startBlocks[j] })
338 sort.Slice(endBlocks, func(i, j int) bool { return endBlocks[i] < endBlocks[j] })
339
340 var freeSpaces [][2]int64
341
342 // currentIntervals contains the number of intervals which contain the
343 // position currently being iterated over. If currentIntervals is ever
344 // bigger than 1, there is overlap within the given intervals.
345 currentIntervals := 0
346 var hasOverlap bool
347
348 // Iterate for as long as there are interval boundaries to be processed.
349 for len(startBlocks) != 0 || len(endBlocks) != 0 {
350 // Short-circuit boundary processing. If an interval ends at x and the
351 // next one starts at x (this is using half-open intervals), it would
352 // otherwise perform useless processing as well as create an empty free
353 // interval which would then need to be filtered back out.
354 if len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] == endBlocks[0] {
355 startBlocks = startBlocks[1:]
356 endBlocks = endBlocks[1:]
357 continue
358 }
359 // Pick the lowest boundary from either startBlocks or endBlocks,
360 // preferring endBlocks if they are equal. Don't try to pick from empty
361 // slices.
362 if (len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] < endBlocks[0]) || len(endBlocks) == 0 {
363 // If currentIntervals == 0 a free space region ends here.
364 // Since this algorithm creates the free space interval at the end
365 // of an occupied interval, for the first interval there is no free
366 // space entry. But in this case it's fine to just ignore it as the
367 // first interval always starts at 0 because of the GPT.
368 if currentIntervals == 0 && len(freeSpaces) != 0 {
369 freeSpaces[len(freeSpaces)-1][1] = startBlocks[0]
370 }
371 // This is the start of an interval, increase the number of active
372 // intervals.
373 currentIntervals++
374 hasOverlap = hasOverlap || currentIntervals > 1
375 // Drop processed startBlock from slice.
376 startBlocks = startBlocks[1:]
377 } else {
378 // This is the end of an interval, decrease the number of active
379 // intervals.
380 currentIntervals--
381 // If currentIntervals == 0 a free space region starts here.
382 // Same as with the startBlocks, ignore a potential free block after
383 // the final range as the GPT occupies the last blocks anyway.
384 if currentIntervals == 0 && len(startBlocks) != 0 {
385 freeSpaces = append(freeSpaces, [2]int64{endBlocks[0], 0})
386 }
387 endBlocks = endBlocks[1:]
388 }
389 }
390 return freeSpaces, hasOverlap, nil
391}
392
393// Overhead returns the number of blocks the GPT partitioning itself consumes,
394// i.e. aren't usable for user data.
395func Overhead(blockSize int64) int64 {
396 // 3 blocks + 2x 16384 bytes (partition entry space)
397 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
398 return 3 + (2 * partitionEntryBlocks)
399}
400
401// Write writes a list of GPT partitions with a protective MBR to the given
402// WriteSeeker. It must have a defined end, i.e. w.Seek(-x, io.SeekEnd) must
403// seek to x bytes before the end of the disk. If gpt.ID or any of the
404// partition IDs are the all-zero UUID, a new random one is generated and
405// written back. If the output is supposed to be reproducible, generate the
406// UUIDs beforehand.
407func Write(w io.WriteSeeker, gpt *Table) error {
408 if gpt.BlockSize < 512 {
409 return errors.New("block size is smaller than 512 bytes, this is unsupported")
410 }
411 // Layout looks as follows:
412 // Block 0: Protective MBR
413 // Block 1: GPT Header
414 // Block 2-(16384 bytes): GPT partition entries
415 // Block (16384 bytes)-n: GPT partition entries alternate copy
416 // Block n: GPT Header alternate copy
417 if len(gpt.Partitions) > 128 {
418 return errors.New("Bigger-than default GPTs (>128 partitions) are unimplemented")
419 }
420
421 partitionEntryBlocks := (16384 + gpt.BlockSize - 1) / gpt.BlockSize
422 if gpt.BlockCount < 3+(2*partitionEntryBlocks) {
423 return errors.New("not enough blocks to write GPT")
424 }
425
426 if gpt.ID == zeroUUID {
427 gpt.ID = uuid.New()
428 }
429
430 partSize := binary.Size(partition{})
431 slotCount := 128
432
433 var partitionEntriesData bytes.Buffer
434 for i := 0; i < slotCount; i++ {
435 if len(gpt.Partitions) <= i || gpt.Partitions[i] == nil {
436 partitionEntriesData.Write(make([]byte, partSize))
437 continue
438 }
439 p := gpt.Partitions[i]
440 if p.ID == zeroUUID {
441 p.ID = uuid.New()
442 }
443 rawP := partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200444 Type: msguid.From(p.Type),
445 ID: msguid.From(p.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000446 FirstBlock: p.FirstBlock,
447 LastBlock: p.LastBlock,
448 Attributes: uint64(p.Attributes),
449 }
450 nameUTF16 := utf16.Encode([]rune(p.Name))
451 // copy will automatically truncate if target is too short
452 copy(rawP.Name[:], nameUTF16)
453 binary.Write(&partitionEntriesData, binary.LittleEndian, rawP)
454 }
455
456 hdr := header{
457 Signature: gptSignature,
458 Revision: gptRevision,
459 HeaderSize: uint32(binary.Size(&header{})),
Lorenz Brun60d6b902023-06-20 16:02:40 +0200460 ID: msguid.From(gpt.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000461
462 PartitionEntryCount: uint32(slotCount),
463 PartitionEntrySize: uint32(partSize),
464
465 FirstUsableBlock: uint64(2 + partitionEntryBlocks),
466 LastUsableBlock: uint64(gpt.BlockCount - (2 + partitionEntryBlocks)),
467 }
468 hdr.PartitionEntriesCRC32 = crc32.ChecksumIEEE(partitionEntriesData.Bytes())
469
470 hdrChecksum := crc32.NewIEEE()
471
472 // Write alternate header first, as otherwise resizes are unsafe. If the
473 // alternate is currently not at the end of the block device, it cannot
474 // be found. Thus if the write operation is aborted abnormally, the
475 // primary GPT is corrupted and the alternate cannot be found because it
476 // is not at its canonical location. Rewriting the alternate first avoids
477 // this problem.
478
479 // Alternate header
480 if _, err := w.Seek((gpt.LastUsableBlock()+1)*gpt.BlockSize, io.SeekStart); err != nil {
481 return fmt.Errorf("failed to seek to end of block device: %w", err)
482 }
483 hdr.HeaderBlock = uint64(gpt.BlockCount - 1)
484 hdr.AlternateHeaderBlock = 1
485 hdr.PartitionEntriesStartBlock = uint64(gpt.BlockCount - (1 + partitionEntryBlocks))
486
487 hdrChecksum.Reset()
488 hdr.HeaderCRC32 = 0
489 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
490 hdr.HeaderCRC32 = hdrChecksum.Sum32()
491
492 if _, err := w.Write(partitionEntriesData.Bytes()); err != nil {
493 return fmt.Errorf("failed to write alternate partition entries: %w", err)
494 }
495 if _, err := w.Seek((gpt.BlockCount-1)*gpt.BlockSize, io.SeekStart); err != nil {
496 return fmt.Errorf("failed to seek to end of block device: %w", err)
497 }
498
499 if err := binary.Write(w, binary.LittleEndian, &hdr); err != nil {
500 return fmt.Errorf("failed to write alternate header: %w", err)
501 }
502 if _, err := w.Write(make([]byte, gpt.BlockSize-int64(binary.Size(hdr)))); err != nil {
503 return fmt.Errorf("failed to write padding: %v", err)
504 }
505
506 // Primary header
507 if _, err := w.Seek(0, io.SeekStart); err != nil {
508 return fmt.Errorf("failed to seek to end of block device: %w", err)
509 }
510 hdr.HeaderBlock = 1
511 hdr.AlternateHeaderBlock = uint64(gpt.BlockCount - 1)
512 hdr.PartitionEntriesStartBlock = 2
513
514 hdrChecksum.Reset()
515 hdr.HeaderCRC32 = 0
516 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
517 hdr.HeaderCRC32 = hdrChecksum.Sum32()
518
519 if err := makeProtectiveMBR(w, gpt.BlockCount, gpt.BootCode); err != nil {
520 return fmt.Errorf("failed to write first block: %w", err)
521 }
522
523 if err := binary.Write(w, binary.LittleEndian, &hdr); err != nil {
524 return fmt.Errorf("failed to write primary header: %w", err)
525 }
526 if _, err := w.Write(make([]byte, gpt.BlockSize-int64(binary.Size(hdr)))); err != nil {
527 return fmt.Errorf("failed to write padding: %v", err)
528 }
529 if _, err := w.Write(partitionEntriesData.Bytes()); err != nil {
530 return fmt.Errorf("failed to write primary partition entries: %w", err)
531 }
532 return nil
533}
534
535// Read reads a Table from a block device given its block size and count.
536func Read(r io.ReadSeeker, blockSize int64, blockCount int64) (*Table, error) {
537 if Overhead(blockSize) > blockCount {
538 return nil, errors.New("disk cannot contain a GPT as the block count is too small to store one")
539 }
540 if _, err := r.Seek(0, io.SeekStart); err != nil {
541 return nil, fmt.Errorf("failed to seek to block 0: %w", err)
542 }
543 zeroBlock := make([]byte, blockSize)
544 if _, err := io.ReadFull(r, zeroBlock); err != nil {
545 return nil, fmt.Errorf("failed to read first two blocks: %w", err)
546 }
547
548 var m mbr
549 if err := binary.Read(bytes.NewReader(zeroBlock[:512]), binary.LittleEndian, &m); err != nil {
550 panic(err) // Read is from memory and with enough data
551 }
552 // The UEFI standard says that the only acceptable MBR for a GPT-partitioned
553 // device is a pure protective MBR with one partition of type 0xEE covering
554 // the entire disk. But reality is sadly not so simple. People have come up
555 // with hacks like Hybrid MBR which is basically a way to expose partitions
556 // as both GPT partitions and MBR partitions. There are also GPTs without
557 // any MBR at all.
558 // Following the standard strictly when reading means that this library
559 // would fail to read valid GPT disks where such schemes are employed.
560 // On the other hand just looking at the GPT signature is also dangerous
561 // as not all tools clear the second block where the GPT resides when
562 // writing an MBR, which results in reading a wrong/obsolete GPT.
563 // As a pragmatic solution this library treats any disk as GPT-formatted if
564 // the first block does not contain an MBR signature or at least one MBR
565 // partition has type 0xEE (GPT). It does however not care in which slot
566 // this partition is or if it begins at the start of the disk.
567 //
568 // Note that the block signatures for MBR and FAT are shared. This is a
569 // historical artifact from DOS. It is not reliably possible to
570 // differentiate the two as either has boot code where the other has meta-
571 // data and both lack any checksums. Because the MBR partition table is at
572 // the very end of the FAT bootcode section the following code always
573 // assumes that it is dealing with an MBR. This is both more likely and
574 // the 0xEE marker is rarer and thus more specific than FATs 0x00, 0x80 and
575 // 0x02.
576 var bootCode []byte
577 hasDOSBootSig := m.Signature == mbrSignature
578 if hasDOSBootSig {
579 var isGPT bool
580 for _, p := range m.PartitionRecords {
581 if p.Type == 0xEE {
582 isGPT = true
583 }
584 }
585 // Note that there is a small but non-zero chance that isGPT is true
586 // for a raw FAT filesystem if the bootcode contains a "valid" MBR.
587 // The next error message mentions that possibility.
588 if !isGPT {
589 return nil, errors.New("block device contains an MBR table without a GPT marker or a raw FAT filesystem")
590 }
591 // Trim right zeroes away as they are padded back when writing. This
592 // makes BootCode empty when it is all-zeros, making it easier to work
593 // with while still round-tripping correctly.
594 bootCode = bytes.TrimRight(m.BootCode[:], "\x00")
595 }
596 // Read the primary GPT. If it is damaged and/or broken, read the alternate.
597 primaryGPT, err := readSingleGPT(r, blockSize, blockCount, 1)
598 if err != nil {
599 alternateGPT, err2 := readSingleGPT(r, blockSize, blockCount, blockCount-1)
600 if err2 != nil {
601 return nil, fmt.Errorf("failed to read both GPTs: primary GPT (%v), secondary GPT (%v)", err, err2)
602 }
603 alternateGPT.BootCode = bootCode
604 return alternateGPT, nil
605 }
606 primaryGPT.BootCode = bootCode
607 return primaryGPT, nil
608}
609
610func readSingleGPT(r io.ReadSeeker, blockSize int64, blockCount int64, headerBlockPos int64) (*Table, error) {
611 if _, err := r.Seek(blockSize*headerBlockPos, io.SeekStart); err != nil {
612 return nil, fmt.Errorf("failed to seek to block %d: %w", headerBlockPos, err)
613 }
614 hdrBlock := make([]byte, blockSize)
615 if _, err := io.ReadFull(r, hdrBlock); err != nil {
616 return nil, fmt.Errorf("failed to read GPT header block: %w", err)
617 }
618 hdrBlockReader := bytes.NewReader(hdrBlock)
619 var hdr header
620 if err := binary.Read(hdrBlockReader, binary.LittleEndian, &hdr); err != nil {
621 panic(err) // Read from memory with enough bytes, should not fail
622 }
623 if hdr.Signature != gptSignature {
624 return nil, errors.New("no GPT signature found")
625 }
626 if hdr.HeaderSize < uint32(binary.Size(hdr)) {
627 return nil, fmt.Errorf("GPT header size is too small, likely corrupted")
628 }
629 if int64(hdr.HeaderSize) > blockSize {
630 return nil, fmt.Errorf("GPT header size is bigger than block size, likely corrupted")
631 }
632 // Use reserved bytes to hash, but do not expose them to the user.
633 // If someone has a need to process them, they should extend this library
634 // with whatever an updated UEFI specification contains.
635 // It has been considered to store these in the user-exposed GPT struct to
636 // be able to round-trip them cleanly, but there is significant complexity
637 // and risk involved in doing so.
638 reservedBytes := hdrBlock[binary.Size(hdr):hdr.HeaderSize]
639 hdrExpectedCRC := hdr.HeaderCRC32
640 hdr.HeaderCRC32 = 0
641 hdrCRC := crc32.NewIEEE()
642 binary.Write(hdrCRC, binary.LittleEndian, &hdr)
643 hdrCRC.Write(reservedBytes)
644 if hdrCRC.Sum32() != hdrExpectedCRC {
645 return nil, fmt.Errorf("GPT header checksum mismatch, probably corrupted")
646 }
647 if hdr.HeaderBlock != uint64(headerBlockPos) {
648 return nil, errors.New("GPT header indicates wrong block")
649 }
650 if hdr.PartitionEntrySize < uint32(binary.Size(partition{})) {
651 return nil, errors.New("partition entry size too small")
652 }
653 if hdr.PartitionEntriesStartBlock > uint64(blockCount) {
654 return nil, errors.New("partition entry start block is out of range")
655 }
656 // Sanity-check total size of the partition entry area. Otherwise, this is a
657 // trivial DoS as it could cause allocation of gigabytes of memory.
658 // 4MiB is equivalent to around 45k partitions at the current size.
659 // I know of no operating system which would handle even a fraction of this.
660 if uint64(hdr.PartitionEntryCount)*uint64(hdr.PartitionEntrySize) > 4*1024*1024 {
661 return nil, errors.New("partition entry area bigger than 4MiB, refusing to read")
662 }
663 partitionEntryData := make([]byte, hdr.PartitionEntrySize*hdr.PartitionEntryCount)
664 if _, err := r.Seek(blockSize*int64(hdr.PartitionEntriesStartBlock), io.SeekStart); err != nil {
665 return nil, fmt.Errorf("failed to seek to partition entry start block: %w", err)
666 }
667 if _, err := io.ReadFull(r, partitionEntryData); err != nil {
668 return nil, fmt.Errorf("failed to read partition entries: %w", err)
669 }
670 if crc32.ChecksumIEEE(partitionEntryData) != hdr.PartitionEntriesCRC32 {
671 return nil, errors.New("GPT partition entry table checksum mismatch")
672 }
673 var g Table
Lorenz Brun60d6b902023-06-20 16:02:40 +0200674 g.ID = msguid.To(hdr.ID)
Lorenz Brunee17d832022-10-18 12:02:45 +0000675 g.BlockSize = blockSize
676 g.BlockCount = blockCount
677 for i := uint32(0); i < hdr.PartitionEntryCount; i++ {
678 entryReader := bytes.NewReader(partitionEntryData[i*hdr.PartitionEntrySize : (i+1)*hdr.PartitionEntrySize])
679 var part partition
680 if err := binary.Read(entryReader, binary.LittleEndian, &part); err != nil {
681 panic(err) // Should not happen
682 }
683 // If the partition type is the all-zero UUID, this slot counts as
684 // unused.
685 if part.Type == zeroUUID {
686 g.Partitions = append(g.Partitions, nil)
687 continue
688 }
689 g.Partitions = append(g.Partitions, &Partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200690 ID: msguid.To(part.ID),
691 Type: msguid.To(part.Type),
Lorenz Brunee17d832022-10-18 12:02:45 +0000692 Name: strings.TrimRight(string(utf16.Decode(part.Name[:])), "\x00"),
693 FirstBlock: part.FirstBlock,
694 LastBlock: part.LastBlock,
695 Attributes: Attribute(part.Attributes),
696 })
697 }
698 // Remove long list of nils at the end as it's inconvenient to work with
699 // (append doesn't work, debug prints are very long) and it round-trips
700 // correctly even without it as it gets zero-padded when writing anyway.
701 var maxValidPartition int
702 for i, p := range g.Partitions {
703 if !p.IsUnused() {
704 maxValidPartition = i
705 }
706 }
707 g.Partitions = g.Partitions[:maxValidPartition+1]
708 return &g, nil
709}