diff --git a/MdeModulePkg/Core/Pei/Memory/MemoryServices.c b/MdeModulePkg/Core/Pei/Memory/MemoryServices.c
index 5b35657da5a9..3343da500ccc 100644
--- a/MdeModulePkg/Core/Pei/Memory/MemoryServices.c
+++ b/MdeModulePkg/Core/Pei/Memory/MemoryServices.c
@@ -809,6 +809,20 @@ PeiAllocatePages (
}
}
+ if (!PrivateData->PeiMemoryInstalled && (PrivateData->FreeMemorySearchHintCount != 0)) {
+ // Reuse temporary-memory pages before consuming the limited free-memory top.
+ Status = FindFreeMemoryFromMemoryAllocationHob (
+ (CONST EFI_PEI_SERVICES **)PeiServices,
+ MemoryType,
+ ALIGN_VALUE (Pages, EFI_SIZE_TO_PAGES (Granularity)),
+ Granularity,
+ Memory
+ );
+ if (!EFI_ERROR (Status)) {
+ return Status;
+ }
+ }
+
//
// We will attempt up to two times here.
// If we don't have memory bins, we will only attempt to allocate from the PHIT.
@@ -849,6 +863,9 @@ PeiAllocatePages (
Padding & ~(UINTN)EFI_PAGE_MASK,
EfiConventionalMemory
);
+ if (!PrivateData->PeiMemoryInstalled && (PrivateData->FreeMemorySearchHintCount < MAX_UINTN)) {
+ PrivateData->FreeMemorySearchHintCount++;
+ }
}
//
@@ -1051,6 +1068,10 @@ PeiFreePages (
if (MemoryAllocationHob != NULL) {
UpdateOrSplitMemoryAllocationHob (PeiServices, MemoryAllocationHob, Memory, Bytes, EfiConventionalMemory);
FreeMemoryAllocationHob (PrivateData, MemoryAllocationHob);
+ if (!PrivateData->PeiMemoryInstalled && (PrivateData->FreeMemorySearchHintCount < MAX_UINTN)) {
+ PrivateData->FreeMemorySearchHintCount++;
+ }
+
return EFI_SUCCESS;
} else {
return EFI_NOT_FOUND;
diff --git a/MdeModulePkg/Core/Pei/PeiMain.h b/MdeModulePkg/Core/Pei/PeiMain.h
index 611998a20cc6..cb7e7d72387c 100644
--- a/MdeModulePkg/Core/Pei/PeiMain.h
+++ b/MdeModulePkg/Core/Pei/PeiMain.h
@@ -353,6 +353,11 @@ struct _PEI_CORE_INSTANCE {
// This is used for the memory bin feature, if enabled, to track bin locations.
//
EFI_MEMORY_TYPE_STATISTICS *MemoryTypeStatistics;
+
+ ///
+ /// Count pre-memory events that may produce reusable conventional-memory ranges.
+ ///
+ UINTN FreeMemorySearchHintCount;
};
///
diff --git a/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTest.cpp b/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTest.cpp
new file mode 100644
index 000000000000..cfbdee0cbcc9
--- /dev/null
+++ b/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTest.cpp
@@ -0,0 +1,666 @@
+/** @file
+ Unit tests for PEI Core page allocation.
+
+ Test cases:
+ - ReusesFreedRangeAfterFreePagesInPreMemory:
+ reuses a freed range before consuming temporary-memory top-down pages.
+ - SkipsPreMemorySearchWithoutFreeMemoryEvents:
+ skips the early HOB scan when no free-memory event has occurred.
+ - FallsBackToFreeMemoryTopWhenFreedRangesAreTooSmall:
+ top-down allocation when free HOBs are too small.
+ - KeepsTopDownAllocationFirstAfterPermanentMemoryInstall:
+ leaves post-install allocation order unchanged.
+ - RejectsInvalidMemoryTypeWithoutChangingFreeMemoryTop:
+ rejects invalid types without changing the PHIT.
+ - UsesConfiguredMemoryBinBeforeFreedRangesOutsideTheBin:
+ prefers the configured bin to ranges outside it.
+ - MergesFreeRangesAcrossConfiguredMemoryBinsAsFinalFallback:
+ unrestricted fallback can reuse adjacent free ranges from separate bins.
+ - ReusesFreedRangeFromDifferentMemoryBinAsFinalFallback:
+ final fallback reuses a range from another type bin.
+ - ReusesFreedRangeOutsidePhitAsFinalFallback:
+ final fallback reuses an out-of-PHIT range without bins.
+
+ Copyright (c) 2026, Intel Corporation. All rights reserved.
+ SPDX-License-Identifier: BSD-2-Clause-Patent
+**/
+
+#include
+#include
+
+extern "C" {
+ #include "PeiMain.h"
+ #include "MemoryBin.h"
+ #include
+}
+
+using namespace testing;
+
+/** Maximum number of synthetic allocation HOBs used by the fixture. **/
+static constexpr UINTN MAX_MEMORY_ALLOCATION_HOBS = 4;
+/** Top of the PHIT free-memory range used by allocation tests. **/
+static constexpr EFI_PHYSICAL_ADDRESS FREE_MEMORY_TOP = 0x10000000;
+
+/**
+ Fixture for exercising PEI Core page allocation with controlled HOB state.
+
+ The fixture provides mock HOB lookup over synthetic allocation HOBs and a
+ valid PEI core instance/PHIT for calls to PeiAllocatePages().
+**/
+class PeiMemoryServicesTest : public ::testing::Test {
+protected:
+ // Intercepts production HOB lookups and maps them to the fixture HOBs.
+ StrictMock HobLib;
+ // PEI core state consumed by PeiAllocatePages().
+ PEI_CORE_INSTANCE PrivateData;
+ // Handoff information table containing the default free-memory range.
+ EFI_HOB_HANDOFF_INFO_TABLE HobList;
+ // Per-type address ranges used by the configured memory-bin test.
+ EFI_MEMORY_TYPE_STATISTICS MemoryTypeStatistics[EfiMaxMemoryType + 1];
+ // Synthetic memory allocation HOBs returned by the mocked HOB library.
+ EFI_HOB_MEMORY_ALLOCATION MemoryAllocationHobs[MAX_MEMORY_ALLOCATION_HOBS];
+ // Reusable HOB consumed when the allocator creates a memory allocation HOB.
+ EFI_HOB_MEMORY_ALLOCATION UnusedHob;
+ // Number of valid entries in MemoryAllocationHobs.
+ UINTN MemoryAllocationHobCount;
+
+ /**
+ Initialize the fixture's PEI state and mock HOB lookup behavior.
+ **/
+ VOID
+ SetUp (
+ ) override
+ {
+ ZeroMem (&PrivateData, sizeof (PrivateData));
+ ZeroMem (&HobList, sizeof (HobList));
+ ZeroMem (MemoryTypeStatistics, sizeof (MemoryTypeStatistics));
+ ZeroMem (MemoryAllocationHobs, sizeof (MemoryAllocationHobs));
+ ZeroMem (&UnusedHob, sizeof (UnusedHob));
+
+ PrivateData.Signature = PEI_CORE_HANDLE_SIGNATURE;
+ PrivateData.HobList.Raw = (UINT8 *)&HobList;
+ PrivateData.PeiMemoryInstalled = TRUE;
+ HobList.EfiFreeMemoryTop = FREE_MEMORY_TOP;
+ HobList.EfiFreeMemoryBottom = FREE_MEMORY_TOP - (8 * DEFAULT_PAGE_ALLOCATION_GRANULARITY);
+ UnusedHob.Header.HobType = EFI_HOB_TYPE_UNUSED;
+ UnusedHob.Header.HobLength = sizeof (UnusedHob);
+ MemoryAllocationHobCount = 0;
+
+ EXPECT_CALL (HobLib, GetFirstHob (_))
+ .Times (AnyNumber ())
+ .WillRepeatedly (
+ [this](UINT16 Type) {
+ return FindFirstHob (Type);
+ }
+ );
+ EXPECT_CALL (HobLib, GetNextHob (_, _))
+ .Times (AnyNumber ())
+ .WillRepeatedly (
+ [this](UINT16 Type, CONST VOID *HobStart) {
+ return FindNextHob (Type, HobStart);
+ }
+ );
+ }
+
+ /**
+ Return the PEI services pointer associated with the fixture instance.
+
+ @return The PEI services pointer expected by PeiAllocatePages().
+ **/
+ CONST EFI_PEI_SERVICES **
+ PeiServices (
+ )
+ {
+ return (CONST EFI_PEI_SERVICES **)&PrivateData.Ps;
+ }
+
+ /**
+ Add a synthetic conventional-memory allocation HOB to the fixture.
+
+ @param[in] BaseAddress Start address of the free range.
+ @param[in] Length Length of the free range in bytes.
+
+ @return The newly initialized memory allocation HOB.
+ **/
+ EFI_HOB_MEMORY_ALLOCATION *
+ AddFreeRange (
+ IN EFI_PHYSICAL_ADDRESS BaseAddress,
+ IN UINT64 Length
+ )
+ {
+ EFI_HOB_MEMORY_ALLOCATION *Hob;
+
+ Hob = &MemoryAllocationHobs[MemoryAllocationHobCount++];
+ ZeroMem (Hob, sizeof (*Hob));
+ Hob->Header.HobType = EFI_HOB_TYPE_MEMORY_ALLOCATION;
+ Hob->Header.HobLength = sizeof (*Hob);
+ Hob->AllocDescriptor.MemoryBaseAddress = BaseAddress;
+ Hob->AllocDescriptor.MemoryLength = Length;
+ Hob->AllocDescriptor.MemoryType = EfiConventionalMemory;
+ return Hob;
+ }
+
+private:
+
+ /**
+ Find the first fixture HOB matching the requested type.
+
+ @param[in] Type HOB type to find.
+
+ @return The first matching HOB, or NULL if none is available.
+ **/
+ VOID *
+ FindFirstHob (
+ IN UINT16 Type
+ )
+ {
+ if ((Type == EFI_HOB_TYPE_MEMORY_ALLOCATION) && (MemoryAllocationHobCount != 0)) {
+ return &MemoryAllocationHobs[0];
+ }
+
+ if (Type == EFI_HOB_TYPE_UNUSED) {
+ return &UnusedHob;
+ }
+
+ return NULL;
+ }
+
+ /**
+ Find the fixture HOB beginning at HobStart if it matches Type.
+
+ @param[in] Type HOB type to find.
+ @param[in] HobStart Starting HOB address supplied by HobLib.
+
+ @return The matching HOB, or NULL if none is available.
+ **/
+ VOID *
+ FindNextHob (
+ IN UINT16 Type,
+ IN CONST VOID *HobStart
+ )
+ {
+ UINTN Index;
+
+ if (Type == EFI_HOB_TYPE_MEMORY_ALLOCATION) {
+ for (Index = 0; Index < MemoryAllocationHobCount; Index++) {
+ if (HobStart == &MemoryAllocationHobs[Index]) {
+ return &MemoryAllocationHobs[Index];
+ }
+ }
+ }
+
+ return NULL;
+ }
+};
+
+/**
+ Verify that allocation reuses the smallest suitable freed range first.
+
+ The PHIT free-memory top remains unchanged when a freed range can satisfy
+ the request.
+**/
+TEST_F (PeiMemoryServicesTest, ReusesFreedRangeAfterFreePagesInPreMemory) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ PrivateData.PeiMemoryInstalled = FALSE;
+ FreedRange = AddFreeRange (HobList.EfiFreeMemoryBottom, 2 * Granularity);
+ FreedRange->AllocDescriptor.MemoryType = EfiLoaderData;
+
+ Status = PeiFreePages (PeiServices (), FreedRange->AllocDescriptor.MemoryBaseAddress, EFI_SIZE_TO_PAGES (2 * Granularity));
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (PrivateData.FreeMemorySearchHintCount, (UINTN)1);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, HobList.EfiFreeMemoryBottom + Granularity);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, FREE_MEMORY_TOP);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryBaseAddress, Memory);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryLength, Granularity);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiLoaderData);
+ EXPECT_EQ (UnusedHob.Header.HobType, EFI_HOB_TYPE_MEMORY_ALLOCATION);
+ EXPECT_EQ (UnusedHob.AllocDescriptor.MemoryBaseAddress, HobList.EfiFreeMemoryBottom);
+ EXPECT_EQ (UnusedHob.AllocDescriptor.MemoryLength, Granularity);
+}
+
+/**
+ Verify that a pre-memory allocation skips the early HOB search without a hint.
+
+ The existing top-down path is used while the free range remains untouched.
+**/
+TEST_F (PeiMemoryServicesTest, SkipsPreMemorySearchWithoutFreeMemoryEvents) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreeRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ PrivateData.PeiMemoryInstalled = FALSE;
+ FreeRange = AddFreeRange (HobList.EfiFreeMemoryBottom, 2 * Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (PrivateData.FreeMemorySearchHintCount, (UINTN)0);
+ EXPECT_EQ (Memory, FREE_MEMORY_TOP - Granularity);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, Memory);
+ EXPECT_EQ (FreeRange->AllocDescriptor.MemoryBaseAddress, HobList.EfiFreeMemoryBottom);
+ EXPECT_EQ (FreeRange->AllocDescriptor.MemoryLength, 2 * Granularity);
+ EXPECT_EQ (FreeRange->AllocDescriptor.MemoryType, EfiConventionalMemory);
+}
+
+/**
+ Verify post-install allocation remains top-down-first.
+
+ A suitable free HOB is left untouched while the PHIT can satisfy the request.
+**/
+TEST_F (PeiMemoryServicesTest, KeepsTopDownAllocationFirstAfterPermanentMemoryInstall) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ FreedRange = AddFreeRange (HobList.EfiFreeMemoryBottom, 2 * Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, FREE_MEMORY_TOP - Granularity);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, Memory);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryBaseAddress, HobList.EfiFreeMemoryBottom);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryLength, 2 * Granularity);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiConventionalMemory);
+}
+
+/**
+ Verify that undersized freed ranges leave allocation to the PHIT free-memory top.
+
+ The request is rounded to the configured page-allocation granularity before
+ the PHIT range is updated.
+**/
+TEST_F (PeiMemoryServicesTest, FallsBackToFreeMemoryTopWhenFreedRangesAreTooSmall) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINTN GranularityPages;
+ UINTN RequestedPages;
+ UINTN AllocatedPages;
+
+ GranularityPages = EFI_SIZE_TO_PAGES (DEFAULT_PAGE_ALLOCATION_GRANULARITY);
+ RequestedPages = GranularityPages + 1;
+ AllocatedPages = ALIGN_VALUE (RequestedPages, GranularityPages);
+ FreedRange = AddFreeRange (0x40000000, DEFAULT_PAGE_ALLOCATION_GRANULARITY);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, RequestedPages, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, FREE_MEMORY_TOP - (AllocatedPages * EFI_PAGE_SIZE));
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, Memory);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiConventionalMemory);
+ EXPECT_EQ (UnusedHob.Header.HobType, EFI_HOB_TYPE_MEMORY_ALLOCATION);
+ EXPECT_EQ (UnusedHob.AllocDescriptor.MemoryBaseAddress, Memory);
+ EXPECT_EQ (UnusedHob.AllocDescriptor.MemoryLength, AllocatedPages * EFI_PAGE_SIZE);
+}
+
+/**
+ Verify that an invalid memory type is rejected without changing free memory.
+**/
+TEST_F (PeiMemoryServicesTest, RejectsInvalidMemoryTypeWithoutChangingFreeMemoryTop) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_STATUS Status;
+
+ Memory = 0;
+ Status = PeiAllocatePages (PeiServices (), EfiConventionalMemory, 1, &Memory);
+
+ EXPECT_EQ (Status, EFI_INVALID_PARAMETER);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, FREE_MEMORY_TOP);
+ EXPECT_EQ (Memory, (EFI_PHYSICAL_ADDRESS)0);
+}
+
+/**
+ Verify that a configured memory bin retains priority over freed ranges elsewhere.
+
+ A suitable freed range outside the bin must remain unused while the request
+ can be satisfied from the configured bin.
+**/
+TEST_F (PeiMemoryServicesTest, UsesConfiguredMemoryBinBeforeFreedRangesOutsideTheBin) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ PrivateData.MemoryTypeInformationInitialized = TRUE;
+ PrivateData.MemoryTypeStatistics = MemoryTypeStatistics;
+ MemoryTypeStatistics[EfiLoaderData].BaseAddress = 0x60000000 - (2 * Granularity);
+ MemoryTypeStatistics[EfiLoaderData].MaximumAddress = 0x60000000;
+ FreedRange = AddFreeRange (0x40000000, 2 * Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, (EFI_PHYSICAL_ADDRESS)(0x60000000 - Granularity));
+ EXPECT_EQ (MemoryTypeStatistics[EfiLoaderData].MaximumAddress, Memory);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, FREE_MEMORY_TOP);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiConventionalMemory);
+ EXPECT_EQ (UnusedHob.Header.HobType, EFI_HOB_TYPE_MEMORY_ALLOCATION);
+ EXPECT_EQ (UnusedHob.AllocDescriptor.MemoryBaseAddress, Memory);
+}
+
+/**
+ Verify the final fallback can reuse adjacent free HOBs across memory bins.
+
+ Bounded searches stay within their configured ranges, while the final
+ unrestricted search can satisfy the request using both adjacent ranges.
+**/
+TEST_F (PeiMemoryServicesTest, MergesFreeRangesAcrossConfiguredMemoryBinsAsFinalFallback) {
+ EFI_HOB_MEMORY_ALLOCATION *FirstRange;
+ EFI_HOB_MEMORY_ALLOCATION *SecondRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+ EFI_PHYSICAL_ADDRESS BinBase;
+ EFI_PHYSICAL_ADDRESS BinBoundary;
+ EFI_PHYSICAL_ADDRESS BinTop;
+ EFI_PHYSICAL_ADDRESS Memory;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ BinBase = 0x60000000;
+ BinBoundary = BinBase + Granularity;
+ BinTop = BinBoundary + Granularity;
+ PrivateData.MemoryTypeInformationInitialized = TRUE;
+ PrivateData.MemoryTypeStatistics = MemoryTypeStatistics;
+ MemoryTypeStatistics[EfiLoaderData].BaseAddress = BinBase;
+ MemoryTypeStatistics[EfiLoaderData].MaximumAddress = BinBoundary - 1;
+ MemoryTypeStatistics[EfiLoaderData].NumberOfPages = EFI_SIZE_TO_PAGES (Granularity);
+ MemoryTypeStatistics[EfiBootServicesData].BaseAddress = BinBoundary;
+ MemoryTypeStatistics[EfiBootServicesData].MaximumAddress = BinTop - 1;
+ MemoryTypeStatistics[EfiBootServicesData].NumberOfPages = EFI_SIZE_TO_PAGES (Granularity);
+ HobList.EfiFreeMemoryBottom = FREE_MEMORY_TOP;
+ HobList.EfiFreeMemoryTop = FREE_MEMORY_TOP;
+ FirstRange = AddFreeRange (BinBase, Granularity);
+ SecondRange = AddFreeRange (BinBoundary, Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, EFI_SIZE_TO_PAGES (2 * Granularity), &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, BinBase);
+ EXPECT_EQ (FirstRange->Header.HobType, EFI_HOB_TYPE_UNUSED);
+ EXPECT_EQ (SecondRange->AllocDescriptor.MemoryBaseAddress, Memory);
+ EXPECT_EQ (SecondRange->AllocDescriptor.MemoryLength, 2 * Granularity);
+ EXPECT_EQ (SecondRange->AllocDescriptor.MemoryType, EfiLoaderData);
+}
+
+/**
+ Verify the final unrestricted search can reuse a range from another type bin.
+
+ The requested type bin and PHIT are exhausted, while a suitable free range
+ remains in the EfiBootServicesData bin.
+**/
+TEST_F (PeiMemoryServicesTest, ReusesFreedRangeFromDifferentMemoryBinAsFinalFallback) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+ EFI_PHYSICAL_ADDRESS OtherBinBase;
+ EFI_PHYSICAL_ADDRESS OtherBinTop;
+ EFI_PHYSICAL_ADDRESS RequestedBinTop;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ OtherBinBase = 0x70000000;
+ OtherBinTop = OtherBinBase + (2 * Granularity);
+ RequestedBinTop = 0x60000000;
+ PrivateData.MemoryTypeInformationInitialized = TRUE;
+ PrivateData.MemoryTypeStatistics = MemoryTypeStatistics;
+ MemoryTypeStatistics[EfiLoaderData].BaseAddress = RequestedBinTop;
+ MemoryTypeStatistics[EfiLoaderData].MaximumAddress = RequestedBinTop;
+ MemoryTypeStatistics[EfiBootServicesData].BaseAddress = OtherBinBase;
+ MemoryTypeStatistics[EfiBootServicesData].MaximumAddress = OtherBinTop;
+ HobList.EfiFreeMemoryBottom = FREE_MEMORY_TOP;
+ FreedRange = AddFreeRange (OtherBinBase, 2 * Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, OtherBinBase + Granularity);
+ EXPECT_EQ (MemoryTypeStatistics[EfiLoaderData].MaximumAddress, RequestedBinTop);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, FREE_MEMORY_TOP);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryBaseAddress, Memory);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryLength, Granularity);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiLoaderData);
+}
+
+/**
+ Verify the final unrestricted search can reuse a free range outside the PHIT.
+
+ The PHIT has no available top-down memory, so allocation succeeds only if
+ the final free-HOB search is not restricted to the PHIT range.
+**/
+TEST_F (PeiMemoryServicesTest, ReusesFreedRangeOutsidePhitAsFinalFallback) {
+ EFI_PHYSICAL_ADDRESS Memory;
+ EFI_HOB_MEMORY_ALLOCATION *FreedRange;
+ EFI_STATUS Status;
+ UINT64 Granularity;
+ EFI_PHYSICAL_ADDRESS RangeBase;
+
+ Granularity = DEFAULT_PAGE_ALLOCATION_GRANULARITY;
+ RangeBase = FREE_MEMORY_TOP + Granularity;
+ HobList.EfiFreeMemoryBottom = FREE_MEMORY_TOP;
+ FreedRange = AddFreeRange (RangeBase, 2 * Granularity);
+
+ Status = PeiAllocatePages (PeiServices (), EfiLoaderData, 1, &Memory);
+
+ ASSERT_EQ (Status, EFI_SUCCESS);
+ EXPECT_EQ (Memory, RangeBase + Granularity);
+ EXPECT_EQ (HobList.EfiFreeMemoryTop, FREE_MEMORY_TOP);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryBaseAddress, Memory);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryLength, Granularity);
+ EXPECT_EQ (FreedRange->AllocDescriptor.MemoryType, EfiLoaderData);
+}
+
+/**
+ Provide a test stub for PEI Core HOB creation.
+
+ This entry point is unrelated to the AllocatePages() paths under test.
+
+ @param[in] PeiServices PEI services table pointer.
+ @param[in] Type Type of HOB to create.
+ @param[in] Length Requested HOB length.
+ @param[out] Hob Receives the created HOB pointer.
+
+ @retval EFI_OUT_OF_RESOURCES HOB creation is not supported by this stub.
+**/
+extern "C" EFI_STATUS
+EFIAPI
+PeiCreateHob (
+ IN CONST EFI_PEI_SERVICES **PeiServices,
+ IN UINT16 Type,
+ IN UINT16 Length,
+ IN OUT VOID **Hob
+ )
+{
+ (VOID)PeiServices;
+ (VOID)Type;
+ (VOID)Length;
+ (VOID)Hob;
+ return EFI_OUT_OF_RESOURCES;
+}
+
+/**
+ Provide a test stub for the PEI service-table HOB creation service.
+
+ This entry point is referenced by unrelated pool-allocation code.
+
+ @param[in] Type Type of HOB to create.
+ @param[in] Length Requested HOB length.
+ @param[out] Hob Receives the created HOB pointer.
+
+ @retval EFI_OUT_OF_RESOURCES HOB creation is not supported by this stub.
+**/
+extern "C" EFI_STATUS
+EFIAPI
+PeiServicesCreateHob (
+ IN UINT16 Type,
+ IN UINT16 Length,
+ OUT VOID **Hob
+ )
+{
+ (VOID)Type;
+ (VOID)Length;
+ (VOID)Hob;
+ return EFI_OUT_OF_RESOURCES;
+}
+
+/**
+ Provide a test stub for memory-type information discovery.
+
+ @param[out] MemoryTypeInformation Receives the discovered memory types.
+
+ @retval EFI_NOT_FOUND Memory-type discovery is outside this test's scope.
+**/
+extern "C" EFI_STATUS
+EFIAPI
+PopulateMemoryTypeInformation (
+ IN EFI_MEMORY_TYPE_INFORMATION *MemoryTypeInformation
+ )
+{
+ (VOID)MemoryTypeInformation;
+ return EFI_NOT_FOUND;
+}
+
+/**
+ Provide a test stub for memory-type information resource-HOB lookup.
+
+ @param[in] HobStart Start of the HOB list.
+ @param[in] MemoryTypeInformation Memory-type information to locate.
+
+ @return NULL because resource-HOB lookup is outside this test's scope.
+**/
+extern "C" EFI_HOB_RESOURCE_DESCRIPTOR *
+EFIAPI
+GetMemoryTypeInformationResourceHob (
+ IN VOID **HobStart,
+ IN EFI_MEMORY_TYPE_INFORMATION *MemoryTypeInformation
+ )
+{
+ (VOID)HobStart;
+ (VOID)MemoryTypeInformation;
+ return NULL;
+}
+
+/**
+ Provide a test stub for memory-bin range initialization.
+
+ @param[in] Start Start of the memory-bin range.
+ @param[in] Length Length of the memory-bin range.
+ @param[in] MemoryTypeInformation Memory-type allocation counts.
+ @param[in, out] MemoryTypeInformationInitialized Memory-bin initialization state.
+ @param[out] MemoryTypeStatistics Memory-bin address statistics.
+ @param[in, out] DefaultMaximumAddress Default bin maximum address.
+**/
+extern "C" VOID
+EFIAPI
+CoreSetMemoryTypeInformationRange (
+ IN EFI_PHYSICAL_ADDRESS Start,
+ IN UINT64 Length,
+ IN EFI_MEMORY_TYPE_INFORMATION *MemoryTypeInformation,
+ IN BOOLEAN *MemoryTypeInformationInitialized,
+ IN EFI_MEMORY_TYPE_STATISTICS *MemoryTypeStatistics,
+ IN EFI_PHYSICAL_ADDRESS *DefaultMaximumAddress
+ )
+{
+ (VOID)Start;
+ (VOID)Length;
+ (VOID)MemoryTypeInformation;
+ (VOID)MemoryTypeInformationInitialized;
+ (VOID)MemoryTypeStatistics;
+ (VOID)DefaultMaximumAddress;
+}
+
+/**
+ Provide a test stub for memory-bin allocation.
+
+ @param[in, out] MemoryTypeInformationInitialized Memory-bin initialization state.
+ @param[in] MemoryTypeInformation Memory-type allocation counts.
+ @param[out] MemoryTypeStatistics Memory-bin address statistics.
+ @param[in, out] DefaultMaximumAddress Default bin maximum address.
+ @param[in] CreateHob Whether to create the resource HOB.
+**/
+extern "C" VOID
+EFIAPI
+AllocateMemoryTypeInformationBins (
+ IN BOOLEAN *MemoryTypeInformationInitialized,
+ IN EFI_MEMORY_TYPE_INFORMATION *MemoryTypeInformation,
+ IN EFI_MEMORY_TYPE_STATISTICS *MemoryTypeStatistics,
+ IN EFI_PHYSICAL_ADDRESS *DefaultMaximumAddress,
+ IN BOOLEAN CreateHob
+ )
+{
+ (VOID)MemoryTypeInformationInitialized;
+ (VOID)MemoryTypeInformation;
+ (VOID)MemoryTypeStatistics;
+ (VOID)DefaultMaximumAddress;
+ (VOID)CreateHob;
+}
+
+/**
+ Provide a test stub for PEI HOB-list retrieval.
+
+ @param[in] PeiServices PEI services table pointer.
+ @param[out] HobList Receives the HOB list pointer.
+
+ @retval EFI_NOT_AVAILABLE_YET HOB-list retrieval is outside this test's scope.
+**/
+extern "C" EFI_STATUS
+EFIAPI
+PeiGetHobList (
+ IN CONST EFI_PEI_SERVICES **PeiServices,
+ IN OUT VOID **HobList
+ )
+{
+ (VOID)PeiServices;
+ (VOID)HobList;
+ return EFI_NOT_AVAILABLE_YET;
+}
+
+/**
+ Provide a test stub for PHIT construction.
+
+ @param[in] BootMode Current boot mode.
+ @param[in] MemoryBegin Start address of the memory region.
+ @param[in] MemoryLength Length of the memory region in bytes.
+**/
+extern "C" VOID
+PeiCoreBuildHobHandoffInfoTable (
+ IN EFI_BOOT_MODE BootMode,
+ IN EFI_PHYSICAL_ADDRESS MemoryBegin,
+ IN UINT64 MemoryLength
+ )
+{
+ (VOID)BootMode;
+ (VOID)MemoryBegin;
+ (VOID)MemoryLength;
+}
+
+/**
+ Initialize GoogleTest and execute the PEI memory-service test cases.
+
+ @param[in] argc Number of command-line arguments.
+ @param[in] argv Command-line argument array.
+
+ @return GoogleTest's result code: zero if all tests pass, nonzero otherwise.
+**/
+int
+main (
+ int argc,
+ char *argv[]
+ )
+{
+ testing::InitGoogleTest (&argc, argv);
+ return RUN_ALL_TESTS ();
+}
diff --git a/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTestHost.inf b/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTestHost.inf
new file mode 100644
index 000000000000..c88541aa32fc
--- /dev/null
+++ b/MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTestHost.inf
@@ -0,0 +1,39 @@
+## @file
+# Unit tests for PEI Core page allocation.
+#
+# Builds the host-based test application that exercises AllocatePages() against
+# synthetic HOB lists and verifies freed-range reuse and fallback behavior.
+#
+# Copyright (c) 2026, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+##
+
+[Defines]
+ INF_VERSION = 0x00010005
+ BASE_NAME = PeiMemoryServicesGoogleTest
+ FILE_GUID = 28D287A7-50AC-4A49-AB33-76FC0A392784
+ MODULE_TYPE = HOST_APPLICATION
+ VERSION_STRING = 1.0
+
+[Sources]
+ PeiMemoryServicesGoogleTest.cpp
+ Memory/MemoryServices.c
+
+[Packages]
+ MdePkg/MdePkg.dec
+ MdeModulePkg/MdeModulePkg.dec
+ UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec
+
+[LibraryClasses]
+ GoogleTestLib
+ BaseLib
+ BaseMemoryLib
+ DebugLib
+ HobLib
+ PcdLib
+
+[Guids]
+ gEfiMemoryTypeInformationGuid
+
+[Pcd]
+ gEfiMdeModulePkgTokenSpaceGuid.PcdPeiMemoryBinsEnable
diff --git a/MdeModulePkg/Test/MdeModulePkgHostTest.dsc b/MdeModulePkg/Test/MdeModulePkgHostTest.dsc
index aeecc1a2f7ec..da24657efd03 100644
--- a/MdeModulePkg/Test/MdeModulePkgHostTest.dsc
+++ b/MdeModulePkg/Test/MdeModulePkgHostTest.dsc
@@ -77,6 +77,11 @@
HobLib|MdePkg/Test/Mock/Library/GoogleTest/MockHobLib/MockHobLib.inf
}
+ MdeModulePkg/Core/Pei/PeiMemoryServicesGoogleTestHost.inf {
+
+ HobLib|MdePkg/Test/Mock/Library/GoogleTest/MockHobLib/MockHobLib.inf
+ }
+
MdeModulePkg/Library/GptLib/UnitTest/GptLibUnitTestHost.inf {
GptLib|MdeModulePkg/Library/GptLib/GptLib.inf