blob: ca8566afd332c13c3d00f378a9f114daa7cff2bc [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"
Lorenz Brunee17d832022-10-18 12:02:45 +000013 "sort"
14 "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'}
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
Lorenz Brunee17d832022-10-18 12:02:45 +000061// Attribute is a bitfield of attributes set on a partition. Bits 0 to 47 are
62// reserved for UEFI specification use and all current assignments are in the
63// following const block. Bits 48 to 64 are available for per-Type use by
64// the organization controlling the partition Type.
65type Attribute uint64
66
67const (
68 // AttrRequiredPartition indicates that this partition is required for the
69 // platform to function. Mostly used by vendors to mark things like recovery
70 // partitions.
71 AttrRequiredPartition = 1 << 0
72 // AttrNoBlockIOProto indicates that EFI firmware must not provide an EFI
73 // block device (EFI_BLOCK_IO_PROTOCOL) for this partition.
74 AttrNoBlockIOProto = 1 << 1
75 // AttrLegacyBIOSBootable indicates to special-purpose software outside of
76 // UEFI that this partition can be booted using a traditional PC BIOS.
77 // Don't use this unless you know that you need it specifically.
78 AttrLegacyBIOSBootable = 1 << 2
79)
80
81// PerTypeAttrs returns the top 24 bits which are reserved for custom per-Type
82// attributes. The top 8 bits of the returned uint32 are always 0.
83func (a Attribute) PerTypeAttrs() uint32 {
84 return uint32(a >> 48)
85}
86
87// SetPerTypeAttrs sets the top 24 bits which are reserved for custom per-Type
88// attributes. It does not touch the lower attributes which are specified by the
89// UEFI specification. The top 8 bits of v are silently discarded.
90func (a *Attribute) SetPerTypeAttrs(v uint32) {
91 *a &= 0x000000FF_FFFFFFFF
92 *a |= Attribute(v) << 48
93}
94
95type Partition struct {
96 // Name of the partition, will be truncated if it expands to more than 36
97 // UTF-16 code points. Not all systems can display non-BMP code points.
98 Name string
99 // Type is the type of Table partition, can either be one of the predefined
100 // constants by the UEFI specification or a custom type identifier.
101 // Note that the all-zero UUID denotes an empty partition slot, so this
102 // MUST be set to something, otherwise it is not treated as a partition.
103 Type uuid.UUID
104 // ID is a unique identifier for this specific partition. It should be
105 // changed when cloning the partition.
106 ID uuid.UUID
107 // The first logical block of the partition (inclusive)
108 FirstBlock uint64
109 // The last logical block of the partition (inclusive)
110 LastBlock uint64
111 // Bitset of attributes of this partition.
112 Attributes Attribute
Lorenz Brunad131882023-06-28 16:42:20 +0200113
114 *blockdev.Section
Lorenz Brunee17d832022-10-18 12:02:45 +0000115}
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 }
Lorenz Brunad131882023-06-28 16:42:20 +0200128 return p.Type == uuid.Nil
129}
130
131// New returns an empty table on the given block device.
132// It does not read any existing GPT on the disk (use Read for that), nor does
133// it write anything until Write is called.
134func New(b blockdev.BlockDev) (*Table, error) {
135 return &Table{
136 b: b,
137 }, nil
Lorenz Brunee17d832022-10-18 12:02:45 +0000138}
139
140type Table struct {
141 // ID is the unique identifier of this specific disk / GPT.
142 // If this is left uninitialized/all-zeroes a new random ID is automatically
143 // generated when writing.
144 ID uuid.UUID
145
146 // Data put at the start of the very first block. Gets loaded and executed
147 // by a legacy BIOS bootloader. This can be used to make GPT-partitioned
148 // disks bootable by legacy systems or display a nice error message.
149 // Maximum length is 440 bytes, if that is exceeded Write returns an error.
150 // Should be left empty if the device is not bootable and/or compatibility
151 // with BIOS booting is not required. Only useful on x86 systems.
152 BootCode []byte
153
Lorenz Brunee17d832022-10-18 12:02:45 +0000154 // Partitions contains the list of partitions in this table. This is
Lorenz Brun5a90d302023-10-09 17:38:13 +0200155 // artificially limited to 128 partitions. Holes in the partition list are
156 // represented as nil values. Call IsUnused before checking any other
157 // properties of the partition.
Lorenz Brunee17d832022-10-18 12:02:45 +0000158 Partitions []*Partition
Lorenz Brunad131882023-06-28 16:42:20 +0200159
160 b blockdev.BlockDev
Lorenz Brunee17d832022-10-18 12:02:45 +0000161}
Lorenz Brunad131882023-06-28 16:42:20 +0200162
Lorenz Brunee17d832022-10-18 12:02:45 +0000163type addOptions struct {
164 preferEnd bool
165 keepEmptyEntries bool
166 alignment int64
167}
168
169// AddOption is a bitset controlling various
170type AddOption func(*addOptions)
171
172// WithPreferEnd tries to put the partition as close to the end as possible
173// instead of as close to the start.
174func WithPreferEnd() AddOption {
175 return func(options *addOptions) {
176 options.preferEnd = true
177 }
178}
179
180// WithKeepEmptyEntries does not fill up empty entries which are followed by
181// filled ones. It always appends the partition after the last used entry.
182// Without this flag, the partition is placed in the first empty entry.
183func WithKeepEmptyEntries() AddOption {
184 return func(options *addOptions) {
185 options.keepEmptyEntries = true
186 }
187}
188
189// WithAlignment allows aligning the partition start block to a non-default
190// value. By default, these are aligned to 1MiB.
Lorenz Brun60d6b902023-06-20 16:02:40 +0200191// Only use this flag if you are certain you need it, it can cause quite severe
Lorenz Brunee17d832022-10-18 12:02:45 +0000192// performance degradation under certain conditions.
193func WithAlignment(alignmenet int64) AddOption {
194 return func(options *addOptions) {
195 options.alignment = alignmenet
196 }
197}
198
199// AddPartition takes a pointer to a partition and adds it, placing it into
200// the first (or last using WithPreferEnd) continuous free space which fits it.
201// It writes the placement information (FirstBlock, LastBlock) back to p.
202// By default, AddPartition aligns FirstBlock to 1MiB boundaries, but this can
203// be overridden using WithAlignment.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200204func (gpt *Table) AddPartition(p *Partition, size int64, options ...AddOption) error {
205 blockSize := gpt.b.BlockSize()
Lorenz Brunee17d832022-10-18 12:02:45 +0000206 var opts addOptions
207 // Align to 1MiB or the block size, whichever is bigger
208 opts.alignment = 1 * 1024 * 1024
Lorenz Brunad131882023-06-28 16:42:20 +0200209 if blockSize > opts.alignment {
210 opts.alignment = blockSize
Lorenz Brunee17d832022-10-18 12:02:45 +0000211 }
212 for _, o := range options {
213 o(&opts)
214 }
Lorenz Brunad131882023-06-28 16:42:20 +0200215 if opts.alignment%blockSize != 0 {
216 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 +0000217 }
Lorenz Brunad131882023-06-28 16:42:20 +0200218 if p.ID == uuid.Nil {
219 p.ID = uuid.New()
220 }
221
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200222 fs, _, err := gpt.GetFreeSpaces()
Lorenz Brunee17d832022-10-18 12:02:45 +0000223 if err != nil {
224 return fmt.Errorf("unable to determine free space: %v", err)
225 }
226 if opts.preferEnd {
227 // Reverse fs slice to start iteration at the end
228 for i, j := 0, len(fs)-1; i < j; i, j = i+1, j-1 {
229 fs[i], fs[j] = fs[j], fs[i]
230 }
231 }
Lorenz Brunad131882023-06-28 16:42:20 +0200232 // Number of blocks the partition should occupy, rounded up.
233 blocks := (size + blockSize - 1) / blockSize
234 if size == -1 {
235 var largestFreeSpace int64
236 for _, freeInt := range fs {
237 intSz := freeInt[1] - freeInt[0]
238 if intSz > largestFreeSpace {
239 largestFreeSpace = intSz
240 }
241 }
242 blocks = largestFreeSpace
243 }
Lorenz Brunee17d832022-10-18 12:02:45 +0000244 var maxFreeBlocks int64
245 for _, freeInt := range fs {
246 start := freeInt[0]
247 end := freeInt[1]
248 freeBlocks := end - start
249 // Align start properly
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200250 alignTo := opts.alignment / blockSize
Lorenz Brunad131882023-06-28 16:42:20 +0200251 // Go doesn't implement the euclidean modulus, thus this construction
252 // is necessary.
253 paddingBlocks := ((alignTo - start) % alignTo) % alignTo
Lorenz Brunee17d832022-10-18 12:02:45 +0000254 freeBlocks -= paddingBlocks
255 start += paddingBlocks
256 if maxFreeBlocks < freeBlocks {
257 maxFreeBlocks = freeBlocks
258 }
259 if freeBlocks >= blocks {
260 if !opts.preferEnd {
261 p.FirstBlock = uint64(start)
Lorenz Brunad131882023-06-28 16:42:20 +0200262 p.LastBlock = uint64(start + blocks - 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000263 } else {
264 // Realign FirstBlock. This will always succeed as
265 // there is enough space to align to the start.
Lorenz Brunad131882023-06-28 16:42:20 +0200266 moveLeft := (end - blocks - 1) % (opts.alignment / blockSize)
Lorenz Brunee17d832022-10-18 12:02:45 +0000267 p.FirstBlock = uint64(end - (blocks + 1 + moveLeft))
Lorenz Brunad131882023-06-28 16:42:20 +0200268 p.LastBlock = uint64(end - (2 + moveLeft))
Lorenz Brunee17d832022-10-18 12:02:45 +0000269 }
270 newPartPos := -1
271 if !opts.keepEmptyEntries {
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200272 for i, part := range gpt.Partitions {
Lorenz Brunee17d832022-10-18 12:02:45 +0000273 if part.IsUnused() {
274 newPartPos = i
275 break
276 }
277 }
278 }
279 if newPartPos == -1 {
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200280 gpt.Partitions = append(gpt.Partitions, p)
Lorenz Brunee17d832022-10-18 12:02:45 +0000281 } else {
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200282 gpt.Partitions[newPartPos] = p
Lorenz Brunee17d832022-10-18 12:02:45 +0000283 }
Jan Schära6da1712024-08-21 15:12:11 +0200284 p.Section, err = blockdev.NewSection(gpt.b, int64(p.FirstBlock), int64(p.LastBlock)+1)
285 if err != nil {
286 return fmt.Errorf("failed to create blockdev Section for partition: %w", err)
287 }
Lorenz Brunee17d832022-10-18 12:02:45 +0000288 return nil
289 }
290 }
291
292 return fmt.Errorf("no space for partition of %d blocks, largest continuous free space after alignment is %d blocks", blocks, maxFreeBlocks)
293}
294
295// FirstUsableBlock returns the first usable (i.e. a partition can start there)
296// block.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200297func (gpt *Table) FirstUsableBlock() int64 {
298 blockSize := gpt.b.BlockSize()
Lorenz Brunad131882023-06-28 16:42:20 +0200299 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
Lorenz Brunee17d832022-10-18 12:02:45 +0000300 return 2 + partitionEntryBlocks
301}
302
303// LastUsableBlock returns the last usable (i.e. a partition can end there)
304// block. This block is inclusive.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200305func (gpt *Table) LastUsableBlock() int64 {
306 blockSize := gpt.b.BlockSize()
Lorenz Brunad131882023-06-28 16:42:20 +0200307 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200308 return gpt.b.BlockCount() - (2 + partitionEntryBlocks)
Lorenz Brunee17d832022-10-18 12:02:45 +0000309}
310
311// GetFreeSpaces returns a slice of tuples, each containing a half-closed
312// interval of logical blocks not occupied by the GPT itself or any partition.
313// The returned intervals are always in ascending order as well as
314// non-overlapping. It also returns if it detected any overlaps between
315// partitions or partitions and the GPT. It returns an error if and only if any
316// partition has its FirstBlock before the LastBlock or exceeds the amount of
317// blocks on the block device.
318//
319// Note that the most common use cases for this function are covered by
320// AddPartition, you're encouraged to use it instead.
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200321func (gpt *Table) GetFreeSpaces() ([][2]int64, bool, error) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000322 // This implements an efficient algorithm for finding free intervals given
323 // a set of potentially overlapping occupying intervals. It uses O(n*log n)
324 // time for n being the amount of intervals, i.e. partitions. It uses O(n)
325 // additional memory. This makes it de facto infinitely scalable in the
326 // context of partition tables as the size of the block device is not part
327 // of its cyclomatic complexity and O(n*log n) is tiny for even very big
328 // partition tables.
329
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200330 blockCount := gpt.b.BlockCount()
Lorenz Brunad131882023-06-28 16:42:20 +0200331
Lorenz Brunee17d832022-10-18 12:02:45 +0000332 // startBlocks contains the start blocks (inclusive) of all occupied
333 // intervals.
334 var startBlocks []int64
335 // endBlocks contains the end blocks (exclusive!) of all occupied intervals.
336 // The interval at index i is given by [startBlock[i], endBlock[i]).
337 var endBlocks []int64
338
339 // Reserve the primary GPT interval including the protective MBR.
340 startBlocks = append(startBlocks, 0)
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200341 endBlocks = append(endBlocks, gpt.FirstUsableBlock())
Lorenz Brunee17d832022-10-18 12:02:45 +0000342
343 // Reserve the alternate GPT interval (needs +1 for exclusive interval)
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200344 startBlocks = append(startBlocks, gpt.LastUsableBlock()+1)
Lorenz Brunad131882023-06-28 16:42:20 +0200345 endBlocks = append(endBlocks, blockCount)
Lorenz Brunee17d832022-10-18 12:02:45 +0000346
Tim Windelschmidt0c57d342024-04-11 01:38:47 +0200347 for i, part := range gpt.Partitions {
Lorenz Brunee17d832022-10-18 12:02:45 +0000348 if part.IsUnused() {
349 continue
350 }
351 // Bail if partition does not contain a valid interval. These are open
352 // intervals, thus part.FirstBlock == part.LastBlock denotes a valid
353 // partition with a size of one block.
354 if part.FirstBlock > part.LastBlock {
355 return nil, false, fmt.Errorf("partition %d has a LastBlock smaller than its FirstBlock, its interval is [%d, %d]", i, part.FirstBlock, part.LastBlock)
356 }
Lorenz Brunad131882023-06-28 16:42:20 +0200357 if part.FirstBlock >= uint64(blockCount) || part.LastBlock >= uint64(blockCount) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000358 return nil, false, fmt.Errorf("partition %d exceeds the block count of the block device", i)
359 }
360 startBlocks = append(startBlocks, int64(part.FirstBlock))
361 // Algorithm needs open-closed intervals, thus add +1 to the end.
362 endBlocks = append(endBlocks, int64(part.LastBlock)+1)
363 }
364 // Sort both sets of blocks independently in ascending order. Note that it
365 // is now no longer possible to extract the original intervals. Integers
366 // have no identity thus it doesn't matter if the sort is stable or not.
367 sort.Slice(startBlocks, func(i, j int) bool { return startBlocks[i] < startBlocks[j] })
368 sort.Slice(endBlocks, func(i, j int) bool { return endBlocks[i] < endBlocks[j] })
369
370 var freeSpaces [][2]int64
371
372 // currentIntervals contains the number of intervals which contain the
373 // position currently being iterated over. If currentIntervals is ever
374 // bigger than 1, there is overlap within the given intervals.
375 currentIntervals := 0
376 var hasOverlap bool
377
378 // Iterate for as long as there are interval boundaries to be processed.
379 for len(startBlocks) != 0 || len(endBlocks) != 0 {
380 // Short-circuit boundary processing. If an interval ends at x and the
381 // next one starts at x (this is using half-open intervals), it would
382 // otherwise perform useless processing as well as create an empty free
383 // interval which would then need to be filtered back out.
384 if len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] == endBlocks[0] {
385 startBlocks = startBlocks[1:]
386 endBlocks = endBlocks[1:]
387 continue
388 }
389 // Pick the lowest boundary from either startBlocks or endBlocks,
390 // preferring endBlocks if they are equal. Don't try to pick from empty
391 // slices.
392 if (len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] < endBlocks[0]) || len(endBlocks) == 0 {
393 // If currentIntervals == 0 a free space region ends here.
394 // Since this algorithm creates the free space interval at the end
395 // of an occupied interval, for the first interval there is no free
396 // space entry. But in this case it's fine to just ignore it as the
397 // first interval always starts at 0 because of the GPT.
398 if currentIntervals == 0 && len(freeSpaces) != 0 {
399 freeSpaces[len(freeSpaces)-1][1] = startBlocks[0]
400 }
401 // This is the start of an interval, increase the number of active
402 // intervals.
403 currentIntervals++
404 hasOverlap = hasOverlap || currentIntervals > 1
405 // Drop processed startBlock from slice.
406 startBlocks = startBlocks[1:]
407 } else {
408 // This is the end of an interval, decrease the number of active
409 // intervals.
410 currentIntervals--
411 // If currentIntervals == 0 a free space region starts here.
412 // Same as with the startBlocks, ignore a potential free block after
413 // the final range as the GPT occupies the last blocks anyway.
414 if currentIntervals == 0 && len(startBlocks) != 0 {
415 freeSpaces = append(freeSpaces, [2]int64{endBlocks[0], 0})
416 }
417 endBlocks = endBlocks[1:]
418 }
419 }
420 return freeSpaces, hasOverlap, nil
421}
422
423// Overhead returns the number of blocks the GPT partitioning itself consumes,
424// i.e. aren't usable for user data.
425func Overhead(blockSize int64) int64 {
426 // 3 blocks + 2x 16384 bytes (partition entry space)
427 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
428 return 3 + (2 * partitionEntryBlocks)
429}
430
Lorenz Brunad131882023-06-28 16:42:20 +0200431// Write writes the two GPTs, first the alternate, then the primary to the
432// block device. If gpt.ID or any of the partition IDs are the all-zero UUID,
433// new random ones are generated and written back. If the output is supposed
434// to be reproducible, generate the UUIDs beforehand.
435func (gpt *Table) Write() error {
436 blockSize := gpt.b.BlockSize()
437 blockCount := gpt.b.BlockCount()
438 if blockSize < 512 {
Lorenz Brunee17d832022-10-18 12:02:45 +0000439 return errors.New("block size is smaller than 512 bytes, this is unsupported")
440 }
441 // Layout looks as follows:
442 // Block 0: Protective MBR
443 // Block 1: GPT Header
444 // Block 2-(16384 bytes): GPT partition entries
445 // Block (16384 bytes)-n: GPT partition entries alternate copy
446 // Block n: GPT Header alternate copy
Lorenz Brunad131882023-06-28 16:42:20 +0200447 partitionEntryCount := 128
448 if len(gpt.Partitions) > partitionEntryCount {
449 return errors.New("bigger-than default GPTs (>128 partitions) are unimplemented")
Lorenz Brunee17d832022-10-18 12:02:45 +0000450 }
451
Lorenz Brunad131882023-06-28 16:42:20 +0200452 partitionEntryBlocks := (16384 + blockSize - 1) / blockSize
453 if blockCount < 3+(2*partitionEntryBlocks) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000454 return errors.New("not enough blocks to write GPT")
455 }
456
Lorenz Brunad131882023-06-28 16:42:20 +0200457 if gpt.ID == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000458 gpt.ID = uuid.New()
459 }
460
461 partSize := binary.Size(partition{})
Lorenz Brunee17d832022-10-18 12:02:45 +0000462 var partitionEntriesData bytes.Buffer
Lorenz Brunad131882023-06-28 16:42:20 +0200463 for i := 0; i < partitionEntryCount; i++ {
Lorenz Brunee17d832022-10-18 12:02:45 +0000464 if len(gpt.Partitions) <= i || gpt.Partitions[i] == nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200465 // Write an empty entry
Lorenz Brunee17d832022-10-18 12:02:45 +0000466 partitionEntriesData.Write(make([]byte, partSize))
467 continue
468 }
469 p := gpt.Partitions[i]
Lorenz Brunad131882023-06-28 16:42:20 +0200470 if p.ID == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000471 p.ID = uuid.New()
472 }
473 rawP := partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200474 Type: msguid.From(p.Type),
475 ID: msguid.From(p.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000476 FirstBlock: p.FirstBlock,
477 LastBlock: p.LastBlock,
478 Attributes: uint64(p.Attributes),
479 }
480 nameUTF16 := utf16.Encode([]rune(p.Name))
481 // copy will automatically truncate if target is too short
482 copy(rawP.Name[:], nameUTF16)
483 binary.Write(&partitionEntriesData, binary.LittleEndian, rawP)
484 }
485
486 hdr := header{
487 Signature: gptSignature,
488 Revision: gptRevision,
489 HeaderSize: uint32(binary.Size(&header{})),
Lorenz Brun60d6b902023-06-20 16:02:40 +0200490 ID: msguid.From(gpt.ID),
Lorenz Brunee17d832022-10-18 12:02:45 +0000491
Lorenz Brunad131882023-06-28 16:42:20 +0200492 PartitionEntryCount: uint32(partitionEntryCount),
Lorenz Brunee17d832022-10-18 12:02:45 +0000493 PartitionEntrySize: uint32(partSize),
494
495 FirstUsableBlock: uint64(2 + partitionEntryBlocks),
Lorenz Brunad131882023-06-28 16:42:20 +0200496 LastUsableBlock: uint64(blockCount - (2 + partitionEntryBlocks)),
Lorenz Brunee17d832022-10-18 12:02:45 +0000497 }
498 hdr.PartitionEntriesCRC32 = crc32.ChecksumIEEE(partitionEntriesData.Bytes())
499
500 hdrChecksum := crc32.NewIEEE()
501
502 // Write alternate header first, as otherwise resizes are unsafe. If the
503 // alternate is currently not at the end of the block device, it cannot
504 // be found. Thus if the write operation is aborted abnormally, the
505 // primary GPT is corrupted and the alternate cannot be found because it
506 // is not at its canonical location. Rewriting the alternate first avoids
507 // this problem.
508
509 // Alternate header
Lorenz Brunad131882023-06-28 16:42:20 +0200510 hdr.HeaderBlock = uint64(blockCount - 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000511 hdr.AlternateHeaderBlock = 1
Lorenz Brunad131882023-06-28 16:42:20 +0200512 hdr.PartitionEntriesStartBlock = uint64(blockCount - (1 + partitionEntryBlocks))
Lorenz Brunee17d832022-10-18 12:02:45 +0000513
514 hdrChecksum.Reset()
515 hdr.HeaderCRC32 = 0
516 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
517 hdr.HeaderCRC32 = hdrChecksum.Sum32()
518
Lorenz Brunad131882023-06-28 16:42:20 +0200519 for partitionEntriesData.Len()%int(blockSize) != 0 {
520 partitionEntriesData.WriteByte(0x00)
521 }
522 if _, err := gpt.b.WriteAt(partitionEntriesData.Bytes(), int64(hdr.PartitionEntriesStartBlock)*blockSize); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000523 return fmt.Errorf("failed to write alternate partition entries: %w", err)
524 }
Lorenz Brunee17d832022-10-18 12:02:45 +0000525
Lorenz Brunad131882023-06-28 16:42:20 +0200526 var hdrRaw bytes.Buffer
527 if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil {
528 return fmt.Errorf("failed to encode alternate header: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000529 }
Lorenz Brunad131882023-06-28 16:42:20 +0200530 for hdrRaw.Len()%int(blockSize) != 0 {
531 hdrRaw.WriteByte(0x00)
532 }
533 if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), (blockCount-1)*blockSize); err != nil {
534 return fmt.Errorf("failed to write alternate header: %v", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000535 }
536
537 // Primary header
Lorenz Brunee17d832022-10-18 12:02:45 +0000538 hdr.HeaderBlock = 1
Lorenz Brunad131882023-06-28 16:42:20 +0200539 hdr.AlternateHeaderBlock = uint64(blockCount - 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000540 hdr.PartitionEntriesStartBlock = 2
541
542 hdrChecksum.Reset()
543 hdr.HeaderCRC32 = 0
544 binary.Write(hdrChecksum, binary.LittleEndian, &hdr)
545 hdr.HeaderCRC32 = hdrChecksum.Sum32()
546
Lorenz Brunad131882023-06-28 16:42:20 +0200547 hdrRaw.Reset()
548
549 if err := makeProtectiveMBR(&hdrRaw, blockCount, gpt.BootCode); err != nil {
550 return fmt.Errorf("failed creating protective MBR: %w", err)
551 }
552 for hdrRaw.Len()%int(blockSize) != 0 {
553 hdrRaw.WriteByte(0x00)
554 }
555 if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil {
556 panic(err)
557 }
558 for hdrRaw.Len()%int(blockSize) != 0 {
559 hdrRaw.WriteByte(0x00)
560 }
561 hdrRaw.Write(partitionEntriesData.Bytes())
562 for hdrRaw.Len()%int(blockSize) != 0 {
563 hdrRaw.WriteByte(0x00)
Lorenz Brunee17d832022-10-18 12:02:45 +0000564 }
565
Lorenz Brunad131882023-06-28 16:42:20 +0200566 if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), 0); err != nil {
567 return fmt.Errorf("failed to write primary GPT: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000568 }
569 return nil
570}
571
Lorenz Brunad131882023-06-28 16:42:20 +0200572// Read reads a Table from a block device.
573func Read(r blockdev.BlockDev) (*Table, error) {
574 if Overhead(r.BlockSize()) > r.BlockCount() {
Lorenz Brunee17d832022-10-18 12:02:45 +0000575 return nil, errors.New("disk cannot contain a GPT as the block count is too small to store one")
576 }
Lorenz Brunad131882023-06-28 16:42:20 +0200577 zeroBlock := make([]byte, r.BlockSize())
578 if _, err := r.ReadAt(zeroBlock, 0); err != nil {
579 return nil, fmt.Errorf("failed to read first block: %w", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000580 }
581
582 var m mbr
583 if err := binary.Read(bytes.NewReader(zeroBlock[:512]), binary.LittleEndian, &m); err != nil {
584 panic(err) // Read is from memory and with enough data
585 }
586 // The UEFI standard says that the only acceptable MBR for a GPT-partitioned
587 // device is a pure protective MBR with one partition of type 0xEE covering
588 // the entire disk. But reality is sadly not so simple. People have come up
589 // with hacks like Hybrid MBR which is basically a way to expose partitions
590 // as both GPT partitions and MBR partitions. There are also GPTs without
591 // any MBR at all.
592 // Following the standard strictly when reading means that this library
593 // would fail to read valid GPT disks where such schemes are employed.
594 // On the other hand just looking at the GPT signature is also dangerous
595 // as not all tools clear the second block where the GPT resides when
596 // writing an MBR, which results in reading a wrong/obsolete GPT.
597 // As a pragmatic solution this library treats any disk as GPT-formatted if
598 // the first block does not contain an MBR signature or at least one MBR
599 // partition has type 0xEE (GPT). It does however not care in which slot
600 // this partition is or if it begins at the start of the disk.
601 //
602 // Note that the block signatures for MBR and FAT are shared. This is a
603 // historical artifact from DOS. It is not reliably possible to
604 // differentiate the two as either has boot code where the other has meta-
605 // data and both lack any checksums. Because the MBR partition table is at
606 // the very end of the FAT bootcode section the following code always
607 // assumes that it is dealing with an MBR. This is both more likely and
608 // the 0xEE marker is rarer and thus more specific than FATs 0x00, 0x80 and
609 // 0x02.
610 var bootCode []byte
611 hasDOSBootSig := m.Signature == mbrSignature
612 if hasDOSBootSig {
613 var isGPT bool
614 for _, p := range m.PartitionRecords {
615 if p.Type == 0xEE {
616 isGPT = true
617 }
618 }
619 // Note that there is a small but non-zero chance that isGPT is true
620 // for a raw FAT filesystem if the bootcode contains a "valid" MBR.
621 // The next error message mentions that possibility.
622 if !isGPT {
623 return nil, errors.New("block device contains an MBR table without a GPT marker or a raw FAT filesystem")
624 }
625 // Trim right zeroes away as they are padded back when writing. This
626 // makes BootCode empty when it is all-zeros, making it easier to work
627 // with while still round-tripping correctly.
628 bootCode = bytes.TrimRight(m.BootCode[:], "\x00")
629 }
630 // Read the primary GPT. If it is damaged and/or broken, read the alternate.
Lorenz Brunad131882023-06-28 16:42:20 +0200631 primaryGPT, err := readSingleGPT(r, 1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000632 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +0200633 alternateGPT, err2 := readSingleGPT(r, r.BlockCount()-1)
Lorenz Brunee17d832022-10-18 12:02:45 +0000634 if err2 != nil {
635 return nil, fmt.Errorf("failed to read both GPTs: primary GPT (%v), secondary GPT (%v)", err, err2)
636 }
637 alternateGPT.BootCode = bootCode
638 return alternateGPT, nil
639 }
640 primaryGPT.BootCode = bootCode
641 return primaryGPT, nil
642}
643
Lorenz Brunad131882023-06-28 16:42:20 +0200644func readSingleGPT(r blockdev.BlockDev, headerBlockPos int64) (*Table, error) {
645 hdrBlock := make([]byte, r.BlockSize())
646 if _, err := r.ReadAt(hdrBlock, r.BlockSize()*headerBlockPos); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000647 return nil, fmt.Errorf("failed to read GPT header block: %w", err)
648 }
649 hdrBlockReader := bytes.NewReader(hdrBlock)
650 var hdr header
651 if err := binary.Read(hdrBlockReader, binary.LittleEndian, &hdr); err != nil {
652 panic(err) // Read from memory with enough bytes, should not fail
653 }
654 if hdr.Signature != gptSignature {
655 return nil, errors.New("no GPT signature found")
656 }
657 if hdr.HeaderSize < uint32(binary.Size(hdr)) {
658 return nil, fmt.Errorf("GPT header size is too small, likely corrupted")
659 }
Lorenz Brunad131882023-06-28 16:42:20 +0200660 if int64(hdr.HeaderSize) > r.BlockSize() {
Lorenz Brunee17d832022-10-18 12:02:45 +0000661 return nil, fmt.Errorf("GPT header size is bigger than block size, likely corrupted")
662 }
663 // Use reserved bytes to hash, but do not expose them to the user.
664 // If someone has a need to process them, they should extend this library
665 // with whatever an updated UEFI specification contains.
666 // It has been considered to store these in the user-exposed GPT struct to
667 // be able to round-trip them cleanly, but there is significant complexity
668 // and risk involved in doing so.
669 reservedBytes := hdrBlock[binary.Size(hdr):hdr.HeaderSize]
670 hdrExpectedCRC := hdr.HeaderCRC32
671 hdr.HeaderCRC32 = 0
672 hdrCRC := crc32.NewIEEE()
673 binary.Write(hdrCRC, binary.LittleEndian, &hdr)
674 hdrCRC.Write(reservedBytes)
675 if hdrCRC.Sum32() != hdrExpectedCRC {
676 return nil, fmt.Errorf("GPT header checksum mismatch, probably corrupted")
677 }
678 if hdr.HeaderBlock != uint64(headerBlockPos) {
679 return nil, errors.New("GPT header indicates wrong block")
680 }
681 if hdr.PartitionEntrySize < uint32(binary.Size(partition{})) {
682 return nil, errors.New("partition entry size too small")
683 }
Lorenz Brunad131882023-06-28 16:42:20 +0200684 if hdr.PartitionEntriesStartBlock > uint64(r.BlockCount()) {
Lorenz Brunee17d832022-10-18 12:02:45 +0000685 return nil, errors.New("partition entry start block is out of range")
686 }
687 // Sanity-check total size of the partition entry area. Otherwise, this is a
688 // trivial DoS as it could cause allocation of gigabytes of memory.
689 // 4MiB is equivalent to around 45k partitions at the current size.
690 // I know of no operating system which would handle even a fraction of this.
691 if uint64(hdr.PartitionEntryCount)*uint64(hdr.PartitionEntrySize) > 4*1024*1024 {
692 return nil, errors.New("partition entry area bigger than 4MiB, refusing to read")
693 }
694 partitionEntryData := make([]byte, hdr.PartitionEntrySize*hdr.PartitionEntryCount)
Lorenz Brunad131882023-06-28 16:42:20 +0200695 if _, err := r.ReadAt(partitionEntryData, r.BlockSize()*int64(hdr.PartitionEntriesStartBlock)); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000696 return nil, fmt.Errorf("failed to read partition entries: %w", err)
697 }
698 if crc32.ChecksumIEEE(partitionEntryData) != hdr.PartitionEntriesCRC32 {
699 return nil, errors.New("GPT partition entry table checksum mismatch")
700 }
701 var g Table
Lorenz Brun60d6b902023-06-20 16:02:40 +0200702 g.ID = msguid.To(hdr.ID)
Lorenz Brunee17d832022-10-18 12:02:45 +0000703 for i := uint32(0); i < hdr.PartitionEntryCount; i++ {
704 entryReader := bytes.NewReader(partitionEntryData[i*hdr.PartitionEntrySize : (i+1)*hdr.PartitionEntrySize])
705 var part partition
706 if err := binary.Read(entryReader, binary.LittleEndian, &part); err != nil {
707 panic(err) // Should not happen
708 }
709 // If the partition type is the all-zero UUID, this slot counts as
710 // unused.
Lorenz Brunad131882023-06-28 16:42:20 +0200711 if part.Type == uuid.Nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000712 g.Partitions = append(g.Partitions, nil)
713 continue
714 }
715 g.Partitions = append(g.Partitions, &Partition{
Lorenz Brun60d6b902023-06-20 16:02:40 +0200716 ID: msguid.To(part.ID),
717 Type: msguid.To(part.Type),
Lorenz Brunee17d832022-10-18 12:02:45 +0000718 Name: strings.TrimRight(string(utf16.Decode(part.Name[:])), "\x00"),
719 FirstBlock: part.FirstBlock,
720 LastBlock: part.LastBlock,
721 Attributes: Attribute(part.Attributes),
722 })
723 }
724 // Remove long list of nils at the end as it's inconvenient to work with
725 // (append doesn't work, debug prints are very long) and it round-trips
726 // correctly even without it as it gets zero-padded when writing anyway.
727 var maxValidPartition int
728 for i, p := range g.Partitions {
729 if !p.IsUnused() {
730 maxValidPartition = i
731 }
732 }
733 g.Partitions = g.Partitions[:maxValidPartition+1]
Lorenz Brunad131882023-06-28 16:42:20 +0200734 g.b = r
Lorenz Brunee17d832022-10-18 12:02:45 +0000735 return &g, nil
736}