Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 1 | // 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. |
| 5 | package gpt |
| 6 | |
| 7 | import ( |
| 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 19 | |
| 20 | "source.monogon.dev/metropolis/pkg/msguid" |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 21 | ) |
| 22 | |
| 23 | var gptSignature = [8]byte{'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T'} |
| 24 | var gptRevision uint32 = 0x00010000 // First 2 bytes major, second 2 bytes minor |
| 25 | |
| 26 | // See UEFI Specification 2.9 Table 5-5 |
| 27 | type 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 |
| 48 | type 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 | |
| 57 | var ( |
| 58 | PartitionTypeEFISystem = uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B") |
| 59 | ) |
| 60 | |
| 61 | var 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. |
| 67 | type Attribute uint64 |
| 68 | |
| 69 | const ( |
| 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. |
| 85 | func (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. |
| 92 | func (a *Attribute) SetPerTypeAttrs(v uint32) { |
| 93 | *a &= 0x000000FF_FFFFFFFF |
| 94 | *a |= Attribute(v) << 48 |
| 95 | } |
| 96 | |
| 97 | type 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 |
| 118 | func (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. |
| 124 | func (p *Partition) IsUnused() bool { |
| 125 | if p == nil { |
| 126 | return true |
| 127 | } |
| 128 | return p.Type == zeroUUID |
| 129 | } |
| 130 | |
| 131 | type 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 | } |
| 156 | type addOptions struct { |
| 157 | preferEnd bool |
| 158 | keepEmptyEntries bool |
| 159 | alignment int64 |
| 160 | } |
| 161 | |
| 162 | // AddOption is a bitset controlling various |
| 163 | type 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. |
| 167 | func 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. |
| 176 | func 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 184 | // Only use this flag if you are certain you need it, it can cause quite severe |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 185 | // performance degradation under certain conditions. |
| 186 | func 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. |
| 197 | func (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. |
| 271 | func (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. |
| 278 | func (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. |
| 293 | func (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. |
| 395 | func 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. |
| 407 | func 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 444 | Type: msguid.From(p.Type), |
| 445 | ID: msguid.From(p.ID), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 446 | 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 460 | ID: msguid.From(gpt.ID), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 461 | |
| 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. |
| 536 | func 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 | |
| 610 | func 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 674 | g.ID = msguid.To(hdr.ID) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 675 | 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 Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 690 | ID: msguid.To(part.ID), |
| 691 | Type: msguid.To(part.Type), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 692 | 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 | } |