From b66140e102982eebde4b4478bd8339710baf3b13 Mon Sep 17 00:00:00 2001 From: Devedse Date: Sat, 4 Jul 2026 13:25:37 +0200 Subject: [PATCH] Fix GPT protective MBR to start at LBA 1 per UEFI spec 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. --- .../Partitions/GuidPartitionTable.cs | 12 ++++- .../Partitions/GuidPartitionTableTest.cs | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Library/DiscUtils.Core/Partitions/GuidPartitionTable.cs b/Library/DiscUtils.Core/Partitions/GuidPartitionTable.cs index 31186b526..54b86d555 100644 --- a/Library/DiscUtils.Core/Partitions/GuidPartitionTable.cs +++ b/Library/DiscUtils.Core/Partitions/GuidPartitionTable.cs @@ -108,7 +108,17 @@ public static GuidPartitionTable Initialize(Stream disk, Geometry diskGeometry) { // Create the protective MBR partition record. var pt = BiosPartitionTable.Initialize(disk, diskGeometry); - pt.CreatePrimaryByCylinder(0, diskGeometry.Cylinders - 1, BiosPartitionTypes.GptProtective, false); + + // The UEFI spec ("Protective MBR") requires the 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. A cylinder + // aligned record starts at LBA 63 instead, which the Linux kernel + // (block/partitions/efi.c pmbr_part_valid, requires starting_lba == 1) + // and EDK2 firmware (MdeModulePkg PartitionDxe, UNPACK_UINT32(StartingLBA) == 1) + // reject, causing the whole GPT to be ignored. Use a sector-based record + // starting at LBA 1 covering the rest of the disk instead. + var sectorCount = disk.Length / diskGeometry.BytesPerSector; + pt.CreatePrimaryBySector(1, Math.Min(sectorCount - 1, uint.MaxValue), BiosPartitionTypes.GptProtective, false); // Create the GPT headers, and blank-out the entry areas const int EntryCount = 128; diff --git a/Tests/LibraryTests/Partitions/GuidPartitionTableTest.cs b/Tests/LibraryTests/Partitions/GuidPartitionTableTest.cs index 6caa3dda1..06ebf7e4f 100644 --- a/Tests/LibraryTests/Partitions/GuidPartitionTableTest.cs +++ b/Tests/LibraryTests/Partitions/GuidPartitionTableTest.cs @@ -20,7 +20,9 @@ // DEALINGS IN THE SOFTWARE. // +using System; using System.IO; +using DiscUtils; using DiscUtils.Partitions; using DiscUtils.Streams; using DiscUtils.Vdi; @@ -181,4 +183,54 @@ public void Delete() Assert.Equal(sectorCount[2], table[1].SectorCount); } + [Fact] + public void ProtectiveMbrIsSpecCompliant() + { + var ms = new MemoryStream(); + ms.SetLength(3 * 1024 * 1024); + GuidPartitionTable.Initialize(ms, Geometry.FromCapacity(ms.Length)); + + ms.Position = 0; + var sector = new byte[512]; + ms.ReadExactly(sector, 0, sector.Length); + + // Boot signature. + Assert.Equal(0x55, sector[510]); + Assert.Equal(0xAA, sector[511]); + + // Exactly one of the four partition records must be the 0xEE protective + // record; the other three must be empty (type 0x00). + var protectiveIndex = -1; + var protectiveCount = 0; + var emptyCount = 0; + for (var i = 0; i < 4; ++i) + { + var type = sector[0x1BE + 16 * i + 4]; + if (type == 0xEE) + { + protectiveIndex = i; + ++protectiveCount; + } + else if (type == 0x00) + { + ++emptyCount; + } + } + + Assert.Equal(1, protectiveCount); + Assert.Equal(3, emptyCount); + + var record = sector.AsSpan(0x1BE + 16 * protectiveIndex, 16); + + // Status byte must be non-bootable. + Assert.Equal(0, record[0]); + + // UEFI spec: StartingLBA == 1 and SizeInLBA == disk size in sectors - 1. + var startingLba = EndianUtilities.ToUInt32LittleEndian(record.Slice(8)); + var sizeInLba = EndianUtilities.ToUInt32LittleEndian(record.Slice(12)); + + Assert.Equal(1U, startingLba); + Assert.Equal((uint)(ms.Length / 512 - 1), sizeInLba); + } + }