blob: 8b1fc4d79a013f9a1aa93d613218d12d2f866712 [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"
Jan Schäre479eee2024-08-21 16:01:39 +020013 "slices"
Lorenz Brunee17d832022-10-18 12:02:45 +000014 "strings"
15 "unicode/utf16"
16
17 "github.com/google/uuid"
Lorenz Brun60d6b902023-06-20 16:02:40 +020018
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020019 "source.monogon.dev/osbase/blockdev"
20 "source.monogon.dev/osbase/msguid"
Lorenz Brunee17d832022-10-18 12:02:45 +000021)
22
23var gptSignature = [8]byte{'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T'}
Jan Schäre479eee2024-08-21 16:01:39 +020024
25const gptRevision uint32 = 0x00010000 // First 2 bytes major, second 2 bytes minor
Lorenz Brunee17d832022-10-18 12:02:45 +000026
27// See UEFI Specification 2.9 Table 5-5
28type header struct {
29 Signature [8]byte
30 Revision uint32
31 HeaderSize uint32
32 HeaderCRC32 uint32
33 _ [4]byte
34
35 HeaderBlock uint64
36 AlternateHeaderBlock uint64
37 FirstUsableBlock uint64
38 LastUsableBlock uint64
39
40 ID [16]byte
41
42 PartitionEntriesStartBlock uint64
43 PartitionEntryCount uint32
44 PartitionEntrySize uint32
45 PartitionEntriesCRC32 uint32
46}
47
48// See UEFI Specification 2.9 Table 5-6
49type partition struct {
50 Type [16]byte
51 ID [16]byte
52 FirstBlock uint64
53 LastBlock uint64
54 Attributes uint64
55 Name [36]uint16
56}
57
58var (
59 PartitionTypeEFISystem = uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
60)
61
Lorenz Brunee17d832022-10-18 12:02:45 +000062// Attribute is a bitfield of attributes set on a partition. Bits 0 to 47 are
63// reserved for UEFI specification use and all current assignments are in the
64// following const block. Bits 48 to 64 are available for per-Type use by
65// the organization controlling the partition Type.
66type Attribute uint64
67
68const (
69 // AttrRequiredPartition indicates that this partition is required for the
70 // platform to function. Mostly used by vendors to mark things like recovery
71 // partitions.
72 AttrRequiredPartition = 1 << 0
73 // AttrNoBlockIOProto indicates that EFI firmware must not provide an EFI
74 // block device (EFI_BLOCK_IO_PROTOCOL) for this partition.
75 AttrNoBlockIOProto = 1 << 1
76 // AttrLegacyBIOSBootable indicates to special-purpose software outside of
77 // UEFI that this partition can be booted using a traditional PC BIOS.
78 // Don't use this unless you know that you need it specifically.
79 AttrLegacyBIOSBootable = 1 << 2
80)
81
82// PerTypeAttrs returns the top 24 bits which are reserved for custom per-Type
83// attributes. The top 8 bits of the returned uint32 are always 0.
84func (a Attribute) PerTypeAttrs() uint32 {
85 return uint32(a >> 48)
86}
87
88// SetPerTypeAttrs sets the top 24 bits which are reserved for custom per-Type
89// attributes. It does not touch the lower attributes which are specified by the
90// UEFI specification. The top 8 bits of v are silently discarded.
91func (a *Attribute) SetPerTypeAttrs(v uint32) {
92 *a &= 0x000000FF_FFFFFFFF
93 *a |= Attribute(v) << 48
94}
95
96type Partition struct {
97 // Name of the partition, will be truncated if it expands to more than 36
98 // UTF-16 code points. Not all systems can display non-BMP code points.
99 Name string
100 // Type is the type of Table partition, can either be one of the predefined
101 // constants by the UEFI specification or a custom type identifier.
102 // Note that the all-zero UUID denotes an empty partition slot, so this
103 // MUST be set to something, otherwise it is not treated as a partition.
104 Type uuid.UUID
105 // ID is a unique identifier for this specific partition. It should be
106 // changed when cloning the partition.
107 ID uuid.UUID
108 // The first logical block of the partition (inclusive)
109 FirstBlock uint64
110 // The last logical block of the partition (inclusive)
111 LastBlock uint64
112 // Bitset of attributes of this partition.
113 Attributes Attribute
Lorenz Brunad131882023-06-28 16:42:20 +0200114
115 *blockdev.Section
Lorenz Brunee17d832022-10-18 12:02:45 +0000116}
117
118// SizeBlocks returns the size of the partition in blocks
119func (p *Partition) SizeBlocks() uint64 {
120 return 1 + p.LastBlock - p.FirstBlock
121}
122
123// IsUnused checks if the partition is unused, i.e. it is nil or its type is
124// the null UUID.
125func (p *Partition) IsUnused() bool {
126 if p == nil {
127 return true
128 }
Lorenz Brunad131882023-06-28 16:42:20 +0200129 return p.Type == uuid.Nil
130}
131
132// New returns an empty table on the given block device.
133// It does not read any existing GPT on the disk (use Read for that), nor does
134// it write anything until Write is called.
135func New(b blockdev.BlockDev) (*Table, error) {
136 return &Table{
137 b: b,
138 }, nil
Lorenz Brunee17d832022-10-18 12:02:45 +0000139}
140
141type Table struct {
142 // ID is the unique identifier of this specific disk / GPT.
143 // If this is left uninitialized/all-zeroes a new random ID is automatically
144 // generated when writing.
145 ID uuid.UUID
146
147 // Data put at the start of the very first block. Gets loaded and executed
148 // by a legacy BIOS bootloader. This can be used to make GPT-partitioned
149 // disks bootable by legacy systems or display a nice error message.
150 // Maximum length is 440 bytes, if that is exceeded Write returns an error.
151 // Should be left empty if the device is not bootable and/or compatibility
152 // with BIOS booting is not required. Only useful on x86 systems.
153 BootCode []byte
154
Lorenz Brunee17d832022-10-18 12:02:45 +0000155 // Partitions contains the list of partitions in this table. This is
Lorenz Brun5a90d302023-10-09 17:38:13 +0200156 // artificially limited to 128 partitions. Holes in the partition list are
157 // represented as nil values. Call IsUnused before checking any other
158 // properties of the partition.
Lorenz Brunee17d832022-10-18 12:02:45 +0000159 Partitions []*Partition
Lorenz Brunad131882023-06-28 16:42:20 +0200160
161 b blockdev.BlockDev
Lorenz Brunee17d832022-10-18 12:02:45 +0000162}
Lorenz Brunad131882023-06-28 16:42:20 +0200163
Lorenz Brunee17d832022-10-18 12:02:45 +0000164type addOptions struct {
165 preferEnd bool
166 keepEmptyEntries bool
167 alignment int64
168}
169
170// AddOption is a bitset controlling various
171type AddOption func(*addOptions)
172
173// WithPreferEnd tries to put the partition as close to the end as possible
174// instead of as close to the start.
175func WithPreferEnd() AddOption {
176 return func(options *addOptions) {
177 options.preferEnd = true
178 }
179}
180
181// WithKeepEmptyEntries does not fill up empty entries which are followed by
182// filled ones. It always appends the partition after the last used entry.
183// Without this flag, the partition is placed in the first empty entry.
184func WithKeepEmptyEntries() AddOption {
185 return func(options *addOptions) {
186 options.keepEmptyEntries = true
187 }
188}
189
190// WithAlignment allows aligning the partition start block to a non-default
191// value. By default, these are aligned to 1MiB.
Lorenz Brun60d6b902023-06-20 16:02:40 +0200192// Only use this flag if you are certain you need it, it can cause quite severe
Lorenz Brunee17d832022-10-18 12:02:45 +0000193// performance degradation under certain conditions.
Jan Schäre479eee2024-08-21 16:01:39 +0200194func WithAlignment(alignment int64) AddOption {
Lorenz Brunee17d832022-10-18 12:02:45 +0000195 return func(options *addOptions) {
Jan Schäre479eee2024-08-21 16:01:39 +0200196 options.alignment = alignment
Lorenz Brunee17d832022-10-18 12:02:45 +0000197 }
198}
199
200// AddPartition takes a pointer to a partition and adds it, placing it into
201// the first (or last using WithPreferEnd) continuous free space which fits it.
Jan Schäre479eee2024-08-21 16:01:39 +0200202// If size is -1, the partition will fill the largest free space.
Lorenz Brunee17d832022-10-18 12:02:45 +0000203// It writes the placement information (FirstBlock, LastBlock) back to p.
204// By default, AddPartition aligns FirstBlock to 1MiB boundaries, but this can
205// be overridden using WithAlignment.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200206func (gpt *Table) AddPartition(p *Partition, size int64, options ...AddOption) error {
207 blockSize := gpt.b.BlockSize()
Lorenz Brunee17d832022-10-18 12:02:45 +0000208 var opts addOptions
209 // Align to 1MiB or the block size, whichever is bigger
210 opts.alignment = 1 * 1024 * 1024
Lorenz Brunad131882023-06-28 16:42:20 +0200211 if blockSize > opts.alignment {
212 opts.alignment = blockSize
Lorenz Brunee17d832022-10-18 12:02:45 +0000213 }
214 for _, o := range options {
215 o(&opts)
216 }
Jan Schäre479eee2024-08-21 16:01:39 +0200217 if opts.alignment <= 0 {
218 return fmt.Errorf("alignment (%d bytes) must be positive", opts.alignment)
219 }
Lorenz Brunad131882023-06-28 16:42:20 +0200220 if opts.alignment%blockSize != 0 {
221 return fmt.Errorf("requested alignment (%d bytes) is not an integer multiple of the block size (%d), unable to align", opts.alignment, blockSize)
Lorenz Brunee17d832022-10-18 12:02:45 +0000222 }
Jan Schäre479eee2024-08-21 16:01:39 +0200223 alignBlocks := opts.alignment / blockSize
Lorenz Brunad131882023-06-28 16:42:20 +0200224 if p.ID == uuid.Nil {
225 p.ID = uuid.New()
226 }
227
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200228 fs, _, err := gpt.GetFreeSpaces()
Lorenz Brunee17d832022-10-18 12:02:45 +0000229 if err != nil {
230 return fmt.Errorf("unable to determine free space: %v", err)
231 }
232 if opts.preferEnd {
233 // Reverse fs slice to start iteration at the end
Jan Schäre479eee2024-08-21 16:01:39 +0200234 slices.Reverse(fs)
Lorenz Brunee17d832022-10-18 12:02:45 +0000235 }
Jan Schäre479eee2024-08-21 16:01:39 +0200236 var largestFreeSpace int64
Lorenz Brunee17d832022-10-18 12:02:45 +0000237 for _, freeInt := range fs {
Jan Schäre479eee2024-08-21 16:01:39 +0200238 startAligned := (freeInt[0] + alignBlocks - 1) / alignBlocks * alignBlocks
239 freeBlocks := freeInt[1] - startAligned
240 if freeBlocks > largestFreeSpace {
241 largestFreeSpace = freeBlocks
Lorenz Brunee17d832022-10-18 12:02:45 +0000242 }
243 }
244
Jan Schäre479eee2024-08-21 16:01:39 +0200245 // Number of blocks the partition should occupy, rounded up.
246 blocks := (size + blockSize - 1) / blockSize
247 if size == -1 {
248 if largestFreeSpace == 0 {
249 return errors.New("no free space")
250 }
251 blocks = largestFreeSpace
252 } else if size <= 0 {
253 return fmt.Errorf("partition size (%d bytes) must be positive or the special value -1", size)
254 }
255 for _, freeInt := range fs {
256 startAligned := (freeInt[0] + alignBlocks - 1) / alignBlocks * alignBlocks
257 freeBlocks := freeInt[1] - startAligned
258 if freeBlocks < blocks {
259 continue
260 }
261 if opts.preferEnd {
262 startAligned += (freeBlocks - blocks) / alignBlocks * alignBlocks
263 }
264 p.FirstBlock = uint64(startAligned)
265 p.LastBlock = uint64(startAligned + blocks - 1)
266
267 newPartPos := -1
268 if !opts.keepEmptyEntries {
269 for i, part := range gpt.Partitions {
270 if part.IsUnused() {
271 newPartPos = i
272 break
273 }
274 }
275 }
276 if newPartPos == -1 {
277 gpt.Partitions = append(gpt.Partitions, p)
278 } else {
279 gpt.Partitions[newPartPos] = p
280 }
281 p.Section, err = blockdev.NewSection(gpt.b, int64(p.FirstBlock), int64(p.LastBlock)+1)
282 if err != nil {
283 return fmt.Errorf("failed to create blockdev Section for partition: %w", err)
284 }
285 return nil
286 }
287
288 return fmt.Errorf("no space for partition of %d blocks, largest continuous free space after alignment is %d blocks", blocks, largestFreeSpace)
Lorenz Brunee17d832022-10-18 12:02:45 +0000289}
290
291// FirstUsableBlock returns the first usable (i.e. a partition can start there)
292// block.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200293func (gpt *Table) FirstUsableBlock() int64 {
294 blockSize := gpt.b.BlockSize()
Lorenz Brunad131882023-06-28 16:42:20 +0200295 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
Lorenz Brunee17d832022-10-18 12:02:45 +0000296 return 2 + partitionEntryBlocks
297}
298
299// LastUsableBlock returns the last usable (i.e. a partition can end there)
300// block. This block is inclusive.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200301func (gpt *Table) LastUsableBlock() int64 {
302 blockSize := gpt.b.BlockSize()
Lorenz Brunad131882023-06-28 16:42:20 +0200303 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200304 return gpt.b.BlockCount() - (2 + partitionEntryBlocks)
Lorenz Brunee17d832022-10-18 12:02:45 +0000305}
306
307// GetFreeSpaces returns a slice of tuples, each containing a half-closed
308// interval of logical blocks not occupied by the GPT itself or any partition.
309// The returned intervals are always in ascending order as well as
310// non-overlapping. It also returns if it detected any overlaps between
311// partitions or partitions and the GPT. It returns an error if and only if any
312// partition has its FirstBlock before the LastBlock or exceeds the amount of
313// blocks on the block device.
314//
315// Note that the most common use cases for this function are covered by
316// AddPartition, you're encouraged to use it instead.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200317func (gpt *Table) GetFreeSpaces() ([][2]int64, bool, error) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000318 // This implements an efficient algorithm for finding free intervals given
319 // a set of potentially overlapping occupying intervals. It uses O(n*log n)
320 // time for n being the amount of intervals, i.e. partitions. It uses O(n)
321 // additional memory. This makes it de facto infinitely scalable in the
322 // context of partition tables as the size of the block device is not part
323 // of its cyclomatic complexity and O(n*log n) is tiny for even very big
324 // partition tables.
325
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200326 blockCount := gpt.b.BlockCount()
Lorenz Brunad131882023-06-28 16:42:20 +0200327
Lorenz Brunee17d832022-10-18 12:02:45 +0000328 // startBlocks contains the start blocks (inclusive) of all occupied
329 // intervals.
330 var startBlocks []int64
331 // endBlocks contains the end blocks (exclusive!) of all occupied intervals.
332 // The interval at index i is given by [startBlock[i], endBlock[i]).
333 var endBlocks []int64
334
335 // Reserve the primary GPT interval including the protective MBR.
336 startBlocks = append(startBlocks, 0)
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200337 endBlocks = append(endBlocks, gpt.FirstUsableBlock())
Lorenz Brunee17d832022-10-18 12:02:45 +0000338
339 // Reserve the alternate GPT interval (needs +1 for exclusive interval)
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200340 startBlocks = append(startBlocks, gpt.LastUsableBlock()+1)
Lorenz Brunad131882023-06-28 16:42:20 +0200341 endBlocks = append(endBlocks, blockCount)
Lorenz Brunee17d832022-10-18 12:02:45 +0000342
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200343 for i, part := range gpt.Partitions {
Lorenz Brunee17d832022-10-18 12:02:45 +0000344 if part.IsUnused() {
345 continue
346 }
347 // Bail if partition does not contain a valid interval. These are open
348 // intervals, thus part.FirstBlock == part.LastBlock denotes a valid
349 // partition with a size of one block.
350 if part.FirstBlock > part.LastBlock {
351 return nil, false, fmt.Errorf("partition %d has a LastBlock smaller than its FirstBlock, its interval is [%d, %d]", i, part.FirstBlock, part.LastBlock)
352 }
Lorenz Brunad131882023-06-28 16:42:20 +0200353 if part.FirstBlock >= uint64(blockCount) || part.LastBlock >= uint64(blockCount) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000354 return nil, false, fmt.Errorf("partition %d exceeds the block count of the block device", i)
355 }
356 startBlocks = append(startBlocks, int64(part.FirstBlock))
357 // Algorithm needs open-closed intervals, thus add +1 to the end.
358 endBlocks = append(endBlocks, int64(part.LastBlock)+1)
359 }
360 // Sort both sets of blocks independently in ascending order. Note that it
361 // is now no longer possible to extract the original intervals. Integers
362 // have no identity thus it doesn't matter if the sort is stable or not.
Jan Schäre479eee2024-08-21 16:01:39 +0200363 slices.Sort(startBlocks)
364 slices.Sort(endBlocks)
Lorenz Brunee17d832022-10-18 12:02:45 +0000365
366 var freeSpaces [][2]int64
367
368 // currentIntervals contains the number of intervals which contain the
369 // position currently being iterated over. If currentIntervals is ever
370 // bigger than 1, there is overlap within the given intervals.
371 currentIntervals := 0
372 var hasOverlap bool
373
374 // Iterate for as long as there are interval boundaries to be processed.
375 for len(startBlocks) != 0 || len(endBlocks) != 0 {
376 // Short-circuit boundary processing. If an interval ends at x and the
377 // next one starts at x (this is using half-open intervals), it would
378 // otherwise perform useless processing as well as create an empty free
379 // interval which would then need to be filtered back out.
380 if len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] == endBlocks[0] {
381 startBlocks = startBlocks[1:]
382 endBlocks = endBlocks[1:]
383 continue
384 }
385 // Pick the lowest boundary from either startBlocks or endBlocks,
386 // preferring endBlocks if they are equal. Don't try to pick from empty
387 // slices.
388 if (len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] < endBlocks[0]) || len(endBlocks) == 0 {
389 // If currentIntervals == 0 a free space region ends here.
390 // Since this algorithm creates the free space interval at the end
391 // of an occupied interval, for the first interval there is no free
392 // space entry. But in this case it's fine to just ignore it as the
393 // first interval always starts at 0 because of the GPT.
394 if currentIntervals == 0 && len(freeSpaces) != 0 {
395 freeSpaces[len(freeSpaces)-1][1] = startBlocks[0]
396 }
397 // This is the start of an interval, increase the number of active
398 // intervals.
399 currentIntervals++
400 hasOverlap = hasOverlap || currentIntervals > 1
401 // Drop processed startBlock from slice.
402 startBlocks = startBlocks[1:]
403 } else {
404 // This is the end of an interval, decrease the number of active
405 // intervals.
406 currentIntervals--
407 // If currentIntervals == 0 a free space region starts here.
408 // Same as with the startBlocks, ignore a potential free block after
409 // the final range as the GPT occupies the last blocks anyway.
410 if currentIntervals == 0 && len(startBlocks) != 0 {
411 freeSpaces = append(freeSpaces, [2]int64{endBlocks[0], 0})
412 }
413 endBlocks = endBlocks[1:]
414 }
415 }
416 return freeSpaces, hasOverlap, nil
417}
418
419// Overhead returns the number of blocks the GPT partitioning itself consumes,
420// i.e. aren't usable for user data.
421func Overhead(blockSize int64) int64 {
422 // 3 blocks + 2x 16384 bytes (partition entry space)
423 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
424 return 3 + (2 * partitionEntryBlocks)
425}
426
Lorenz Brunad131882023-06-28 16:42:20 +0200427// Write writes the two GPTs, first the alternate, then the primary to the
428// block device. If gpt.ID or any of the partition IDs are the all-zero UUID,
429// new random ones are generated and written back. If the output is supposed
430// to be reproducible, generate the UUIDs beforehand.
431func (gpt *Table) Write() error {
432 blockSize := gpt.b.BlockSize()
433 blockCount := gpt.b.BlockCount()
434 if blockSize < 512 {
Lorenz Brunee17d832022-10-18 12:02:45 +0000435 return errors.New("block size is smaller than 512 bytes, this is unsupported")
436 }
437 // Layout looks as follows:
438 // Block 0: Protective MBR
439 // Block 1: GPT Header
440 // Block 2-(16384 bytes): GPT partition entries
441 // Block (16384 bytes)-n: GPT partition entries alternate copy
442 // Block n: GPT Header alternate copy
Lorenz Brunad131882023-06-28 16:42:20 +0200443 partitionEntryCount := 128
444 if len(gpt.Partitions) > partitionEntryCount {
445 return errors.New("bigger-than default GPTs (>128 partitions) are unimplemented")
Lorenz Brunee17d832022-10-18 12:02:45 +0000446 }
447
Lorenz Brunad131882023-06-28 16:42:20 +0200448 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
449 if blockCount < 3+(2*partitionEntryBlocks) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000450 return errors.New("not enough blocks to write GPT")
451 }
452
Lorenz Brunad131882023-06-28 16:42:20 +0200453 if gpt.ID == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000454 gpt.ID = uuid.New()
455 }
456
457 partSize := binary.Size(partition{})
Lorenz Brunee17d832022-10-18 12:02:45 +0000458 var partitionEntriesData bytes.Buffer
Lorenz Brunad131882023-06-28 16:42:20 +0200459 for i := 0; i < partitionEntryCount; i++ {
Lorenz Brunee17d832022-10-18 12:02:45 +0000460 if len(gpt.Partitions) <= i || gpt.Partitions[i] == nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200461 // Write an empty entry
Lorenz Brunee17d832022-10-18 12:02:45 +0000462 partitionEntriesData.Write(make([]byte, partSize))
463 continue
464 }
465 p := gpt.Partitions[i]
Lorenz Brunad131882023-06-28 16:42:20 +0200466 if p.ID == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000467 p.ID = uuid.New()
468 }
469 rawP := partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200470 Type: msguid.From(p.Type),
471 ID: msguid.From(p.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000472 FirstBlock: p.FirstBlock,
473 LastBlock: p.LastBlock,
474 Attributes: uint64(p.Attributes),
475 }
476 nameUTF16 := utf16.Encode([]rune(p.Name))
477 // copy will automatically truncate if target is too short
478 copy(rawP.Name[:], nameUTF16)
479 binary.Write(&partitionEntriesData, binary.LittleEndian, rawP)
480 }
481
482 hdr := header{
483 Signature: gptSignature,
484 Revision: gptRevision,
485 HeaderSize: uint32(binary.Size(&header{})),
Lorenz Brun60d6b902023-06-20 16:02:40 +0200486 ID: msguid.From(gpt.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000487
Lorenz Brunad131882023-06-28 16:42:20 +0200488 PartitionEntryCount: uint32(partitionEntryCount),
Lorenz Brunee17d832022-10-18 12:02:45 +0000489 PartitionEntrySize: uint32(partSize),
490
491 FirstUsableBlock: uint64(2 + partitionEntryBlocks),
Lorenz Brunad131882023-06-28 16:42:20 +0200492 LastUsableBlock: uint64(blockCount - (2 + partitionEntryBlocks)),
Lorenz Brunee17d832022-10-18 12:02:45 +0000493 }
494 hdr.PartitionEntriesCRC32 = crc32.ChecksumIEEE(partitionEntriesData.Bytes())
495
496 hdrChecksum := crc32.NewIEEE()
497
498 // Write alternate header first, as otherwise resizes are unsafe. If the
499 // alternate is currently not at the end of the block device, it cannot
500 // be found. Thus if the write operation is aborted abnormally, the
501 // primary GPT is corrupted and the alternate cannot be found because it
502 // is not at its canonical location. Rewriting the alternate first avoids
503 // this problem.
504
505 // Alternate header
Lorenz Brunad131882023-06-28 16:42:20 +0200506 hdr.HeaderBlock = uint64(blockCount - 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000507 hdr.AlternateHeaderBlock = 1
Lorenz Brunad131882023-06-28 16:42:20 +0200508 hdr.PartitionEntriesStartBlock = uint64(blockCount - (1 + partitionEntryBlocks))
Lorenz Brunee17d832022-10-18 12:02:45 +0000509
510 hdrChecksum.Reset()
511 hdr.HeaderCRC32 = 0
512 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
513 hdr.HeaderCRC32 = hdrChecksum.Sum32()
514
Lorenz Brunad131882023-06-28 16:42:20 +0200515 for partitionEntriesData.Len()%int(blockSize) != 0 {
516 partitionEntriesData.WriteByte(0x00)
517 }
518 if _, err := gpt.b.WriteAt(partitionEntriesData.Bytes(), int64(hdr.PartitionEntriesStartBlock)*blockSize); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000519 return fmt.Errorf("failed to write alternate partition entries: %w", err)
520 }
Lorenz Brunee17d832022-10-18 12:02:45 +0000521
Lorenz Brunad131882023-06-28 16:42:20 +0200522 var hdrRaw bytes.Buffer
523 if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil {
524 return fmt.Errorf("failed to encode alternate header: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000525 }
Lorenz Brunad131882023-06-28 16:42:20 +0200526 for hdrRaw.Len()%int(blockSize) != 0 {
527 hdrRaw.WriteByte(0x00)
528 }
529 if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), (blockCount-1)*blockSize); err != nil {
530 return fmt.Errorf("failed to write alternate header: %v", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000531 }
532
533 // Primary header
Lorenz Brunee17d832022-10-18 12:02:45 +0000534 hdr.HeaderBlock = 1
Lorenz Brunad131882023-06-28 16:42:20 +0200535 hdr.AlternateHeaderBlock = uint64(blockCount - 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000536 hdr.PartitionEntriesStartBlock = 2
537
538 hdrChecksum.Reset()
539 hdr.HeaderCRC32 = 0
540 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
541 hdr.HeaderCRC32 = hdrChecksum.Sum32()
542
Lorenz Brunad131882023-06-28 16:42:20 +0200543 hdrRaw.Reset()
544
545 if err := makeProtectiveMBR(&hdrRaw, blockCount, gpt.BootCode); err != nil {
546 return fmt.Errorf("failed creating protective MBR: %w", err)
547 }
548 for hdrRaw.Len()%int(blockSize) != 0 {
549 hdrRaw.WriteByte(0x00)
550 }
551 if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil {
552 panic(err)
553 }
554 for hdrRaw.Len()%int(blockSize) != 0 {
555 hdrRaw.WriteByte(0x00)
556 }
557 hdrRaw.Write(partitionEntriesData.Bytes())
558 for hdrRaw.Len()%int(blockSize) != 0 {
559 hdrRaw.WriteByte(0x00)
Lorenz Brunee17d832022-10-18 12:02:45 +0000560 }
561
Lorenz Brunad131882023-06-28 16:42:20 +0200562 if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), 0); err != nil {
563 return fmt.Errorf("failed to write primary GPT: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000564 }
565 return nil
566}
567
Lorenz Brunad131882023-06-28 16:42:20 +0200568// Read reads a Table from a block device.
569func Read(r blockdev.BlockDev) (*Table, error) {
570 if Overhead(r.BlockSize()) > r.BlockCount() {
Lorenz Brunee17d832022-10-18 12:02:45 +0000571 return nil, errors.New("disk cannot contain a GPT as the block count is too small to store one")
572 }
Lorenz Brunad131882023-06-28 16:42:20 +0200573 zeroBlock := make([]byte, r.BlockSize())
574 if _, err := r.ReadAt(zeroBlock, 0); err != nil {
575 return nil, fmt.Errorf("failed to read first block: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000576 }
577
578 var m mbr
579 if err := binary.Read(bytes.NewReader(zeroBlock[:512]), binary.LittleEndian, &m); err != nil {
580 panic(err) // Read is from memory and with enough data
581 }
582 // The UEFI standard says that the only acceptable MBR for a GPT-partitioned
583 // device is a pure protective MBR with one partition of type 0xEE covering
584 // the entire disk. But reality is sadly not so simple. People have come up
585 // with hacks like Hybrid MBR which is basically a way to expose partitions
586 // as both GPT partitions and MBR partitions. There are also GPTs without
587 // any MBR at all.
588 // Following the standard strictly when reading means that this library
589 // would fail to read valid GPT disks where such schemes are employed.
590 // On the other hand just looking at the GPT signature is also dangerous
591 // as not all tools clear the second block where the GPT resides when
592 // writing an MBR, which results in reading a wrong/obsolete GPT.
593 // As a pragmatic solution this library treats any disk as GPT-formatted if
594 // the first block does not contain an MBR signature or at least one MBR
595 // partition has type 0xEE (GPT). It does however not care in which slot
596 // this partition is or if it begins at the start of the disk.
597 //
598 // Note that the block signatures for MBR and FAT are shared. This is a
599 // historical artifact from DOS. It is not reliably possible to
600 // differentiate the two as either has boot code where the other has meta-
601 // data and both lack any checksums. Because the MBR partition table is at
602 // the very end of the FAT bootcode section the following code always
603 // assumes that it is dealing with an MBR. This is both more likely and
604 // the 0xEE marker is rarer and thus more specific than FATs 0x00, 0x80 and
605 // 0x02.
606 var bootCode []byte
607 hasDOSBootSig := m.Signature == mbrSignature
608 if hasDOSBootSig {
609 var isGPT bool
610 for _, p := range m.PartitionRecords {
611 if p.Type == 0xEE {
612 isGPT = true
613 }
614 }
615 // Note that there is a small but non-zero chance that isGPT is true
616 // for a raw FAT filesystem if the bootcode contains a "valid" MBR.
617 // The next error message mentions that possibility.
618 if !isGPT {
619 return nil, errors.New("block device contains an MBR table without a GPT marker or a raw FAT filesystem")
620 }
621 // Trim right zeroes away as they are padded back when writing. This
622 // makes BootCode empty when it is all-zeros, making it easier to work
623 // with while still round-tripping correctly.
624 bootCode = bytes.TrimRight(m.BootCode[:], "\x00")
625 }
626 // Read the primary GPT. If it is damaged and/or broken, read the alternate.
Lorenz Brunad131882023-06-28 16:42:20 +0200627 primaryGPT, err := readSingleGPT(r, 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000628 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200629 alternateGPT, err2 := readSingleGPT(r, r.BlockCount()-1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000630 if err2 != nil {
631 return nil, fmt.Errorf("failed to read both GPTs: primary GPT (%v), secondary GPT (%v)", err, err2)
632 }
633 alternateGPT.BootCode = bootCode
634 return alternateGPT, nil
635 }
636 primaryGPT.BootCode = bootCode
637 return primaryGPT, nil
638}
639
Lorenz Brunad131882023-06-28 16:42:20 +0200640func readSingleGPT(r blockdev.BlockDev, headerBlockPos int64) (*Table, error) {
641 hdrBlock := make([]byte, r.BlockSize())
642 if _, err := r.ReadAt(hdrBlock, r.BlockSize()*headerBlockPos); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000643 return nil, fmt.Errorf("failed to read GPT header block: %w", err)
644 }
645 hdrBlockReader := bytes.NewReader(hdrBlock)
646 var hdr header
647 if err := binary.Read(hdrBlockReader, binary.LittleEndian, &hdr); err != nil {
648 panic(err) // Read from memory with enough bytes, should not fail
649 }
650 if hdr.Signature != gptSignature {
651 return nil, errors.New("no GPT signature found")
652 }
653 if hdr.HeaderSize < uint32(binary.Size(hdr)) {
654 return nil, fmt.Errorf("GPT header size is too small, likely corrupted")
655 }
Lorenz Brunad131882023-06-28 16:42:20 +0200656 if int64(hdr.HeaderSize) > r.BlockSize() {
Lorenz Brunee17d832022-10-18 12:02:45 +0000657 return nil, fmt.Errorf("GPT header size is bigger than block size, likely corrupted")
658 }
659 // Use reserved bytes to hash, but do not expose them to the user.
660 // If someone has a need to process them, they should extend this library
661 // with whatever an updated UEFI specification contains.
662 // It has been considered to store these in the user-exposed GPT struct to
663 // be able to round-trip them cleanly, but there is significant complexity
664 // and risk involved in doing so.
665 reservedBytes := hdrBlock[binary.Size(hdr):hdr.HeaderSize]
666 hdrExpectedCRC := hdr.HeaderCRC32
667 hdr.HeaderCRC32 = 0
668 hdrCRC := crc32.NewIEEE()
669 binary.Write(hdrCRC, binary.LittleEndian, &hdr)
670 hdrCRC.Write(reservedBytes)
671 if hdrCRC.Sum32() != hdrExpectedCRC {
672 return nil, fmt.Errorf("GPT header checksum mismatch, probably corrupted")
673 }
674 if hdr.HeaderBlock != uint64(headerBlockPos) {
675 return nil, errors.New("GPT header indicates wrong block")
676 }
677 if hdr.PartitionEntrySize < uint32(binary.Size(partition{})) {
678 return nil, errors.New("partition entry size too small")
679 }
Lorenz Brunad131882023-06-28 16:42:20 +0200680 if hdr.PartitionEntriesStartBlock > uint64(r.BlockCount()) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000681 return nil, errors.New("partition entry start block is out of range")
682 }
683 // Sanity-check total size of the partition entry area. Otherwise, this is a
684 // trivial DoS as it could cause allocation of gigabytes of memory.
685 // 4MiB is equivalent to around 45k partitions at the current size.
686 // I know of no operating system which would handle even a fraction of this.
687 if uint64(hdr.PartitionEntryCount)*uint64(hdr.PartitionEntrySize) > 4*1024*1024 {
688 return nil, errors.New("partition entry area bigger than 4MiB, refusing to read")
689 }
690 partitionEntryData := make([]byte, hdr.PartitionEntrySize*hdr.PartitionEntryCount)
Lorenz Brunad131882023-06-28 16:42:20 +0200691 if _, err := r.ReadAt(partitionEntryData, r.BlockSize()*int64(hdr.PartitionEntriesStartBlock)); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000692 return nil, fmt.Errorf("failed to read partition entries: %w", err)
693 }
694 if crc32.ChecksumIEEE(partitionEntryData) != hdr.PartitionEntriesCRC32 {
695 return nil, errors.New("GPT partition entry table checksum mismatch")
696 }
697 var g Table
Lorenz Brun60d6b902023-06-20 16:02:40 +0200698 g.ID = msguid.To(hdr.ID)
Lorenz Brunee17d832022-10-18 12:02:45 +0000699 for i := uint32(0); i < hdr.PartitionEntryCount; i++ {
700 entryReader := bytes.NewReader(partitionEntryData[i*hdr.PartitionEntrySize : (i+1)*hdr.PartitionEntrySize])
701 var part partition
702 if err := binary.Read(entryReader, binary.LittleEndian, &part); err != nil {
703 panic(err) // Should not happen
704 }
705 // If the partition type is the all-zero UUID, this slot counts as
706 // unused.
Lorenz Brunad131882023-06-28 16:42:20 +0200707 if part.Type == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000708 g.Partitions = append(g.Partitions, nil)
709 continue
710 }
711 g.Partitions = append(g.Partitions, &Partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200712 ID: msguid.To(part.ID),
713 Type: msguid.To(part.Type),
Lorenz Brunee17d832022-10-18 12:02:45 +0000714 Name: strings.TrimRight(string(utf16.Decode(part.Name[:])), "\x00"),
715 FirstBlock: part.FirstBlock,
716 LastBlock: part.LastBlock,
717 Attributes: Attribute(part.Attributes),
718 })
719 }
720 // Remove long list of nils at the end as it's inconvenient to work with
721 // (append doesn't work, debug prints are very long) and it round-trips
722 // correctly even without it as it gets zero-padded when writing anyway.
723 var maxValidPartition int
724 for i, p := range g.Partitions {
725 if !p.IsUnused() {
726 maxValidPartition = i
727 }
728 }
729 g.Partitions = g.Partitions[:maxValidPartition+1]
Lorenz Brunad131882023-06-28 16:42:20 +0200730 g.b = r
Lorenz Brunee17d832022-10-18 12:02:45 +0000731 return &g, nil
732}