Skip to content

Fix GPT protective MBR to start at LBA 1 per UEFI spec#71

Merged
Olof-Lagerkvist merged 1 commit into
LTRData:LTRData.DiscUtils-initialfrom
devedse:fix/gpt-protective-mbr-starts-at-lba-1
Jul 4, 2026
Merged

Fix GPT protective MBR to start at LBA 1 per UEFI spec#71
Olof-Lagerkvist merged 1 commit into
LTRData:LTRData.DiscUtils-initialfrom
devedse:fix/gpt-protective-mbr-starts-at-lba-1

Conversation

@devedse

@devedse devedse commented Jul 4, 2026

Copy link
Copy Markdown

Problem

GuidPartitionTable.Initialize created the GPT protective MBR with CreatePrimaryByCylinder, which cylinder-aligns the 0xEE record. That places its StartingLBA at 63 (the first head boundary of the fake CHS geometry) and gives it a SizeInLBA that ends on a cylinder boundary short of the disk end.

The UEFI spec ("Protective MBR", table "Protective MBR Partition Record") requires the single 0xEE record to have:

  • StartingLBA exactly 1 (the LBA of the GPT header), and
  • SizeInLBA equal to the size of the disk minus one, capped at 0xFFFFFFFF.

Because DiscUtils wrote StartingLBA = 63, the GPT was rejected outright by:

  • Linux kernelblock/partitions/efi.c pmbr_part_valid() returns invalid unless starting_lba == 1, so the kernel ignores the GPT completely (losetup -P, blkid, and mount see zero partitions; the msdos fallback scanner also bails when it sees a 0xEE type).
  • EDK2 / UEFI firmwareMdeModulePkg PartitionDxe has the same check (UNPACK_UINT32(StartingLBA) == 1), so disks initialized by DiscUtils could never be EFI-booted.
  • gdisk warns: "0xEE partition doesn't start on sector 1".

Wrong vs. right (bytes at offset 0x1BE, the first MBR partition record)

Wrong (before — cylinder aligned, StartingLBA = 63):

00 02 00 00 EE FE FF FF  3F 00 00 00  ...   <- StartingLBA = 0x0000003F (63)
                         └──────────┘

Right (after — sector based, StartingLBA = 1):

00 00 02 00 EE FE FF FF  01 00 00 00  ...   <- StartingLBA = 0x00000001 (1)
                         └──────────┘

SizeInLBA (the following 4 bytes) becomes min(diskSectors - 1, 0xFFFFFFFF).

Fix

Replace the cylinder-based call with a sector-based record starting at LBA 1 covering the rest of the disk:

var sectorCount = disk.Length / diskGeometry.BytesPerSector;
pt.CreatePrimaryBySector(1, Math.Min(sectorCount - 1, uint.MaxValue), BiosPartitionTypes.GptProtective, false);

BiosPartitionTable.CreatePrimaryBySector already fills in the CHS fields and caps oversized CHS at the 1023/254/63 tuple.

How to reproduce / test

Unit test added (ProtectiveMbrIsSpecCompliant in Tests/LibraryTests/Partitions/GuidPartitionTableTest.cs): initializes a GPT on a 3 MB MemoryStream, reads sector 0, and asserts the boot signature (0x55 0xAA), that exactly one record is type 0xEE (the other three 0x00), that its status byte is 0, that StartingLBA == 1, and that SizeInLBA == diskSectors - 1.

Manual reproduction with sgdisk (Linux), before vs. after this patch:

# Create a 256 MB image and initialize a GPT with DiscUtils, then:
sgdisk -v disk.img
# Before: warns about the 0xEE partition not starting on sector 1
# After:  "No problems found. N free sectors ..."

# Inspect the protective MBR record directly:
od -A d -t x1 -j 446 -N 16 disk.img
# The StartingLBA field (bytes 8..11 of the record) must read: 01 00 00 00

Optionally, losetup -P + blkid/lsblk on the image: before the patch the kernel sees no partitions; after, the GPT partitions appear.

The protective MBR was created with CreatePrimaryByCylinder, which cylinder-aligns the 0xEE record so its StartingLBA is 63 instead of 1 and its SizeInLBA ends short of the disk end. The UEFI spec (Protective MBR) requires the 0xEE record to have StartingLBA exactly 1 and SizeInLBA equal to the disk size minus one, capped at 0xFFFFFFFF.

The Linux kernel enforces this in block/partitions/efi.c pmbr_part_valid() (starting_lba must equal 1) and EDK2 firmware (MdeModulePkg PartitionDxe, UNPACK_UINT32(StartingLBA) == 1) does the same, so disks initialized by DiscUtils were rejected by both and could not be EFI-booted; gdisk also warned the 0xEE partition did not start on sector 1.

Replace the cylinder-based call with CreatePrimaryBySector(1, min(sectorCount - 1, uint.MaxValue), ...), which already handles the CHS fields and caps oversized CHS at 1023/254/63. Adds a ProtectiveMbrIsSpecCompliant unit test asserting the record starts at LBA 1 and spans the disk.
@Olof-Lagerkvist Olof-Lagerkvist merged commit 937d113 into LTRData:LTRData.DiscUtils-initial Jul 4, 2026
3 checks passed
@devedse devedse deleted the fix/gpt-protective-mbr-starts-at-lba-1 branch July 4, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants