MdeModulePkg/GptLib: Add host-based unit tests for malformed GPT input

Extend the GptLib host-based tests with negative cases that guard the
security hardening in PartitionValidGptTable(), PartitionCheckGptEntry()
and PartitionRestoreGptTable() against future regressions. These tests
exercise the shared parser, not the specific fix itself.

The new cases drive the parser with malformed GPT structures that an
attacker may present: bad signature/revision, header-size boundaries,
CRC corruption, MyLBA replay, zero/non-power-of-two entry sizes, LBA
multiplication overflow, out-of-range and overlapping entries, and
restore failure on write-protected media. The INF file header is
updated to note the added malformed coverage.

Signed-off-by: Richard Lyu <richard.lyu@suse.com>
This commit is contained in:
Richard Lyu 2026-07-07 22:56:54 +08:00 committed by mergify[bot]
parent 77585e5004
commit b1029265b1
2 changed files with 571 additions and 3 deletions

View file

@ -3,7 +3,11 @@
These tests exercise PartitionValidGptTable(), PartitionCheckGptEntry()
and PartitionRestoreGptTable() against an in-memory mock disk, covering
their behavior on well-formed GPT structures.
both well-formed GPT structures and malformed ones an attacker may
present: bad signature/revision, header size
boundaries, CRC corruption, MyLBA replay, zero/non-power-of-two entry
sizes, LBA multiplication overflow, out-of-range and overlapping
entries, and backup/primary restore behavior.
Copyright (c) 2026, SUSE LLC. All rights reserved.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent
@ -145,6 +149,421 @@ TestLargerPowerOfTwoEntrySizeIsAccepted (
return UNIT_TEST_PASSED;
}
/**
Verify that a GPT header read failure is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestHeaderReadFailureIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
UT_ASSERT_FALSE (ValidateAt (DISK_SECTORS + 10, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that an invalid GPT signature is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestInvalidSignatureIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->Header.Signature = SIGNATURE_64 ('X', 'X', 'X', 'X', 'X', 'X', 'X', 'X');
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that an invalid GPT revision is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestInvalidRevisionIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
STATIC CONST UINT32 BadRevisions[] = { 0x00000000, 0x00010001, 0x00020000, 0xFFFFFFFF };
UINTN Index;
for (Index = 0; Index < ARRAY_SIZE (BadRevisions); Index++) {
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->Header.Revision = BadRevisions[Index];
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
}
return UNIT_TEST_PASSED;
}
/**
Verify that a header size below the minimum is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestHeaderSizeBelowMinimumIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->Header.HeaderSize = TEST_GPT_HEADER_MIN_SIZE - 1;
GetPrimaryHeader ()->Header.CRC32 = 0;
GetPrimaryHeader ()->Header.CRC32 = CalculateCrc32 (GetPrimaryHeader (), TEST_GPT_HEADER_MIN_SIZE - 1);
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a zero header size is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestZeroHeaderSizeIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->Header.HeaderSize = 0;
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a header size beyond the block size is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestHeaderSizeBeyondBlockSizeIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// HeaderSize larger than the block that was read: the CRC check must
// refuse to scan past the buffer instead of reading out of bounds.
//
GetPrimaryHeader ()->Header.HeaderSize = SECTOR_SIZE + 1;
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a corrupt header CRC is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestCorruptHeaderCrcIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->Header.CRC32 ^= 0xFFFFFFFF;
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a MyLBA mismatch is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestMyLbaMismatchIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// A header claiming to live at a different LBA than the one it was read
// from (e.g. a copied/replayed header) must be rejected.
//
GetPrimaryHeader ()->MyLBA = 5;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a header reporting zero partition entries is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestZeroPartitionEntriesIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->NumberOfPartitionEntries = 0;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a partition entry size below 128 bytes is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestEntrySizeTooSmallIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
GetPrimaryHeader ()->SizeOfPartitionEntry = (UINT32)(sizeof (EFI_PARTITION_ENTRY) / 2);
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a zero partition entry size is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestZeroEntrySizeIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// Must be rejected by the size check before the later division by
// SizeOfPartitionEntry (division by zero) can be reached.
//
GetPrimaryHeader ()->SizeOfPartitionEntry = 0;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a non power-of-two partition entry size is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestNonPowerOfTwoEntrySizeIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// 192 is >= 128 but not 128 * 2^n.
//
GetPrimaryHeader ()->SizeOfPartitionEntry = 192;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that a PartitionEntryLBA causing multiplication overflow is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestEntryLbaMultiplicationOverflowIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// PartitionEntryLBA * BlockSize would wrap around UINT64. Must be rejected
// before any read is attempted with the truncated offset.
//
GetPrimaryHeader ()->PartitionEntryLBA = MAX_UINT64;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that an entry array CRC mismatch is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestEntryArrayCrcMismatchIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// Flip one byte inside an unused entry slot without refreshing the
// entry-array CRC32 recorded in the header.
//
GetDiskLba (PRIMARY_PART_HEADER_LBA + 1)[(2U * PARTITION_ENTRY_SIZE) + 7] ^= 0xA5;
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
/**
Verify that an entry array read failure is rejected.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestEntryArrayReadFailureIsRejected (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
SetupDiskWithValidPrimary ();
//
// Entry array placed so close to the end of the device that reading
// NumberOfPartitionEntries * SizeOfPartitionEntry bytes runs off the disk.
//
GetPrimaryHeader ()->PartitionEntryLBA = LAST_LBA;
UpdateGptHeaderCrc (GetPrimaryHeader ());
UT_ASSERT_FALSE (ValidateAt (PRIMARY_PART_HEADER_LBA, &Header));
return UNIT_TEST_PASSED;
}
// ---------------------------------------------------------------------------
// PartitionCheckGptEntry() tests
// ---------------------------------------------------------------------------
@ -302,6 +721,108 @@ TestEntryStrideFollowsSizeOfPartitionEntry (
return UNIT_TEST_PASSED;
}
/**
Verify that an inverted LBA range reports OutOfRange.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestInvertedLbaRangeReportsOutOfRange (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
EFI_PARTITION_ENTRY Entries[1];
EFI_PARTITION_ENTRY_STATUS Status[1];
InitCheckEntryHeader (&Header, 1, sizeof (EFI_PARTITION_ENTRY));
ZeroMem (Entries, sizeof (Entries));
ZeroMem (Status, sizeof (Status));
FillPartitionEntry (&Entries[0], &mPartitionGuid1, 100, 50);
PartitionCheckGptEntry (&Header, Entries, Status);
UT_ASSERT_TRUE (Status[0].OutOfRange);
return UNIT_TEST_PASSED;
}
/**
Verify that an entry outside the usable region reports OutOfRange.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestEntryOutsideUsableRegionReportsOutOfRange (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
EFI_PARTITION_ENTRY Entries[2];
EFI_PARTITION_ENTRY_STATUS Status[2];
InitCheckEntryHeader (&Header, 2, sizeof (EFI_PARTITION_ENTRY));
ZeroMem (Entries, sizeof (Entries));
ZeroMem (Status, sizeof (Status));
//
// Entry 0 starts before FirstUsableLBA; entry 1 ends after LastUsableLBA.
//
FillPartitionEntry (&Entries[0], &mPartitionGuid1, FIRST_USABLE_LBA - 1, 100);
FillPartitionEntry (&Entries[1], &mPartitionGuid2, 150, 201);
PartitionCheckGptEntry (&Header, Entries, Status);
UT_ASSERT_TRUE (Status[0].OutOfRange);
UT_ASSERT_TRUE (Status[1].OutOfRange);
return UNIT_TEST_PASSED;
}
/**
Verify that overlapping partition entries report Overlap.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestOverlappingEntriesReportOverlap (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER Header;
EFI_PARTITION_ENTRY Entries[3];
EFI_PARTITION_ENTRY_STATUS Status[3];
InitCheckEntryHeader (&Header, 3, sizeof (EFI_PARTITION_ENTRY));
ZeroMem (Entries, sizeof (Entries));
ZeroMem (Status, sizeof (Status));
FillPartitionEntry (&Entries[0], &mPartitionGuid1, 40, 100);
FillPartitionEntry (&Entries[1], &mPartitionGuid2, 90, 150);
FillPartitionEntry (&Entries[2], &mPartitionGuid1, 160, 200);
PartitionCheckGptEntry (&Header, Entries, Status);
UT_ASSERT_TRUE (Status[0].Overlap);
UT_ASSERT_TRUE (Status[1].Overlap);
UT_ASSERT_FALSE (Status[2].Overlap);
return UNIT_TEST_PASSED;
}
// ---------------------------------------------------------------------------
// PartitionRestoreGptTable() tests
// ---------------------------------------------------------------------------
@ -385,6 +906,34 @@ TestRestoreBackupFromPrimary (
return UNIT_TEST_PASSED;
}
/**
Verify that a restore attempt fails on write-protected media.
@param[in] Context Unit test context.
@retval UNIT_TEST_PASSED The test passed.
**/
STATIC
UNIT_TEST_STATUS
EFIAPI
TestRestoreFailsOnWriteProtectedMedia (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_PARTITION_TABLE_HEADER PrimaryHeader;
SetupDiskWithValidPrimary ();
UT_ASSERT_TRUE (ValidateAt (PRIMARY_PART_HEADER_LBA, &PrimaryHeader));
mWriteProtected = TRUE;
UT_ASSERT_FALSE (PartitionRestoreGptTable (&mBlockIo, &mDiskIo, &PrimaryHeader));
UT_ASSERT_FALSE (ValidateAt (LAST_LBA, &PrimaryHeader));
return UNIT_TEST_PASSED;
}
// ---------------------------------------------------------------------------
// Test runner
// ---------------------------------------------------------------------------
@ -433,14 +982,33 @@ UefiTestMain (
AddTestCase (ValidSuite, "Valid backup header is accepted", "ValidBackup", TestValidBackupHeaderIsAccepted, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Minimum (92-byte) header size is accepted", "MinHeaderSize", TestMinimumHeaderSizeIsAccepted, NULL, NULL, NULL);
AddTestCase (ValidSuite, "256-byte partition entry size is accepted", "EntrySize256", TestLargerPowerOfTwoEntrySizeIsAccepted, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Header read failure is rejected", "HeaderReadFailure", TestHeaderReadFailureIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Invalid signature is rejected", "InvalidSignature", TestInvalidSignatureIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Invalid revision is rejected", "InvalidRevision", TestInvalidRevisionIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Header size below minimum is rejected", "HeaderSizeTooSmall", TestHeaderSizeBelowMinimumIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Zero header size is rejected", "ZeroHeaderSize", TestZeroHeaderSizeIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Header size beyond block size is rejected", "HeaderSizeBeyondBlock", TestHeaderSizeBeyondBlockSizeIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Corrupt header CRC is rejected", "CorruptHeaderCrc", TestCorruptHeaderCrcIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "MyLBA mismatch is rejected", "MyLbaMismatch", TestMyLbaMismatchIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Zero partition entries is rejected", "ZeroEntries", TestZeroPartitionEntriesIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Entry size below 128 is rejected", "EntrySizeTooSmall", TestEntrySizeTooSmallIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Zero entry size is rejected", "ZeroEntrySize", TestZeroEntrySizeIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Non power-of-two entry size is rejected", "NonPowerOfTwoEntrySize", TestNonPowerOfTwoEntrySizeIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "PartitionEntryLBA overflow is rejected", "EntryLbaOverflow", TestEntryLbaMultiplicationOverflowIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Entry array CRC mismatch is rejected", "EntryArrayCrcMismatch", TestEntryArrayCrcMismatchIsRejected, NULL, NULL, NULL);
AddTestCase (ValidSuite, "Entry array read failure is rejected", "EntryArrayReadFailure", TestEntryArrayReadFailureIsRejected, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Valid entries report no flags", "ValidEntries", TestValidEntriesReportNoFlags, NULL, NULL, NULL);
AddTestCase (EntrySuite, "OS-specific attribute is reported", "OsSpecificAttribute", TestOsSpecificAttributeIsReported, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Unused entries are ignored", "UnusedEntriesIgnored", TestUnusedEntriesAreIgnored, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Entry stride follows SizeOfPartitionEntry", "EntryStride", TestEntryStrideFollowsSizeOfPartitionEntry, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Inverted LBA range reports OutOfRange", "InvertedRange", TestInvertedLbaRangeReportsOutOfRange, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Entry outside usable region reports OutOfRange", "OutsideUsableRegion", TestEntryOutsideUsableRegionReportsOutOfRange, NULL, NULL, NULL);
AddTestCase (EntrySuite, "Overlapping entries report Overlap", "OverlappingEntries", TestOverlappingEntriesReportOverlap, NULL, NULL, NULL);
AddTestCase (RestoreSuite, "Restore primary from backup", "RestorePrimary", TestRestorePrimaryFromBackup, NULL, NULL, NULL);
AddTestCase (RestoreSuite, "Restore backup from primary", "RestoreBackup", TestRestoreBackupFromPrimary, NULL, NULL, NULL);
AddTestCase (RestoreSuite, "Restore fails on write-protected media", "RestoreWriteProtected", TestRestoreFailsOnWriteProtectedMedia, NULL, NULL, NULL);
Status = RunAllTestSuites (Framework);

View file

@ -2,8 +2,8 @@
# Host-based unit tests for GptLib.
#
# Exercises PartitionValidGptTable(), PartitionCheckGptEntry() and
# PartitionRestoreGptTable() against an in-memory mock disk, covering their
# behavior on well-formed GPT structures.
# PartitionRestoreGptTable() against an in-memory mock disk, covering both
# well-formed GPT structures and malformed ones an attacker may present.
#
# Copyright (c) 2026, SUSE LLC. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-2-Clause-Patent