EDK II - Unbounded NVMe Namespace Allocation in NvmeDiscoverNamespaces
This is a variant of Finding 2.
- Advisory
- BYTERAY-2026-0269
- CWE
- CWE-789
- Vendor
- TianoCore
- Product
- EDK II
- Reported
- 2026-06-25
- Disclosure
- 2026-10-29
- Affected
- EDK II at commit 70741a15d9a714323a5598eac47727e60507e22f (master, 2026-06-21); affected code in MdeModulePkg/Bus/Pci/NvmExpressPei/NvmExpressPei.c.
- Credit
- Argus-Systems
Executive Summary
This is a variant of Finding 2. NvmeDiscoverNamespaces allocates AllocateZeroPool(Private->ControllerData->Nn * sizeof(PEI_NVME_NAMESPACE_INFO)) at line 176, where Nn (Number of Namespaces) is a UINT32 field read from the NVMe Identify Controller data returned by an external NVMe device. The only guard is an integer-overflow check at NvmExpressPeiHci.c:675: if (Private->ControllerData->Nn > MAX_UINT32 / sizeof(PEI_NVME_NAMESPACE_INFO)) return EFI_UNSUPPORTED. This prevents wraparound but imposes no magnitude cap. In PEI, AllocateZeroPool goes through PeiServicesAllocatePool (MdePkg/Library/PeiMemoryAllocationLib/MemoryAllocationLib.c:407), which fails for any size above 0xFFF8 - sizeof(EFI_HOB_MEMORY_POOL) (MdeModulePkg/Core/Pei/Memory/MemoryServices.c:864). On X64, where PEI_NVME_NAMESPACE_INFO is 40 bytes, any Nn above 1638 therefore makes the allocation return NULL. The result is not checked: NvmeDiscoverNamespaces still runs the enumeration loop (lines 184-190), and for each active namespace EnumerateNvmeDevNamespace writes a record through &Private->NamespaceInfo[ActiveNamespaceNum] (lines 107-112 and 136-141). With NamespaceInfo == NULL, these are writes to physical memory starting at address 0, one 40-byte record per active namespace, with contents partly taken from the device (Eui64, 1 << Lbads, Nsze - 1).
This advisory is one of a set reported to VulnCheck on 2026-06-25 for coordinated disclosure to the EDK II maintainers. After several follow-ups over the following months no response was received. 126 days after the initial report, ByteRay is publishing the findings in good faith so that affected integrators can assess and remediate them. All findings were identified against EDK II commit 70741a15d9a714323a5598eac47727e60507e22f (master, 2026-06-21).
Affected Products and Versions
- Vendor: TianoCore
- Product: EDK II (
MdeModulePkg) - Affected commit:
70741a15d9a714323a5598eac47727e60507e22f(master, 2026-06-21). The code is present on that commit and later revisions that have not changed it. - Location:
MdeModulePkg/Bus/Pci/NvmExpressPei/NvmExpressPei.c:176 - Function:
NvmeDiscoverNamespaces - Category:
CWE-789 (Memory Allocation with Excessive Size Value)
Technical Details
Controllability: EXTERNAL_UNTRUSTED — confidence: high
Data Flow
A malicious NVMe device responds to the NvmeIdentifyController admin command (sent at NvmExpressPeiHci.c:664) with a crafted Identify Controller data structure. The Nn field (Number of Namespaces, UINT32 at offset 516) is fully device-controlled. The overflow-only guard at line 675 checks if (Private->ControllerData->Nn > MAX_UINT32 / sizeof(PEI_NVME_NAMESPACE_INFO)) return EFI_UNSUPPORTED, which prevents 32-bit wraparound but allows Nn up to MAX_UINT32 / 40 (about 107M) on X64. NvmeInitPrivateData then calls NvmeDiscoverNamespaces (NvmExpressPei.c:350), which at line 176 executes Private->NamespaceInfo = AllocateZeroPool(Private->ControllerData->Nn * sizeof(PEI_NVME_NAMESPACE_INFO)). Any request above the PEI pool limit (MemoryServices.c:864, about 64 KB, i.e. Nn > 1638 on X64) fails and NamespaceInfo is NULL, with no check. The loop at line 184 iterates up to Nn times and calls EnumerateNvmeDevNamespace for each ID. For each namespace the device reports as active (Ncap != 0, line 101), lines 107-112 compute NamespaceInfo = &Private->NamespaceInfo[DeviceIndex] and write NamespaceId, NamespaceUuid = NamespaceData->Eui64 and Controller, and lines 136-141 write the Media fields, including BlockSize = 1 << Lbads and LastBlock = Nsze - 1 from the device's Identify Namespace data. These writes land at physical address DeviceIndex * 40 and continue upward from 0 for as many active namespaces as the device reports.
Affected Callers
NvmeInitPrivateData(NvmExpressPei.c:350)NvmeInitControllerDataFromPciDevice(NvmExpressPei.c:555, viaNvmeInitPrivateData)NvmeInitControllerFromHostControllerPpi(NvmExpressPei.c:644, viaNvmeInitPrivateData)
Sanitization / Security Controls
- Line 675: Overflow-only guard
Nn > MAX_UINT32 / sizeof(PEI_NVME_NAMESPACE_INFO): INEFFECTIVE, allows anyNnlarge enough to make the PEI pool allocation fail. - PEI pool size limit (
MemoryServices.c:864): makes the allocation fail rather than exhaust memory, which is what turns a largeNninto a NULLNamespaceInfo. - No NULL check on
Private->NamespaceInfoafter line 176, and none inEnumerateNvmeDevNamespacebefore the writes at lines 107-112 and 136-141.
Reachability and Preconditions
Attacker position and trust level for this issue are stated in the Controllability line and Data Flow above, and the concrete trigger is in Reproduction below.
Exploitability: Medium
Realism: Low
Reproduction
- Attacker has physical access to install an NVMe device (PCIe slot or M.2 port) on a controller that NvmExpressPei (PEI-phase NVMe driver) enumerates; attacker controls device firmware or can emulate an NVMe device.
- Program the device to return
Nn = 2000in the Identify Controller response (above 1638, so2000 * 40bytes exceeds the PEI pool limit, and below the line 675 limit ofMAX_UINT32 / 40), and to report namespaces as active (Ncap != 0,Ms == 0) in the Identify Namespace responses. - Boot the target. Firmware sends Identify Controller, receives the crafted
Nn, passes the overflow check, andAllocateZeroPoolat line 176 returns NULL. - The enumeration loop calls
EnumerateNvmeDevNamespacefor each namespace ID; each active namespace writes a 40-byte record at addressActiveNamespaceNum * 40, starting at 0, which faults if page 0 is not mapped or otherwise overwrites low physical memory.
Impact
Writes through a NULL-based pointer during PEI: one 40-byte record per active namespace is written sequentially starting at physical address 0, with fields partly chosen by the device (Eui64, block size, last LBA). Depending on how low memory is mapped and used at that point, this either faults (denial of service during boot) or corrupts low physical memory with partly device-controlled data. The write targets are fixed (address 0 upward), not chosen freely by the attacker. Requires a malicious or emulated NVMe device on a controller enumerated by NvmExpressPei, which is only included on some platforms.
Remediation and Mitigation
As of commit 70741a15d9a714323a5598eac47727e60507e22f this issue is unfixed upstream and no CVE has been assigned. Cap the attacker-influenced allocation size against a sane upper bound before allocating. The specific control that is absent or ineffective is identified in the Sanitization / Security Controls list under Technical Details.
Timeline
- 2026-06-25 - Reported to VulnCheck for coordinated disclosure to the TianoCore / EDK II security team.
- 2026-07 to 2026-10 - Several follow-ups sent; no response received.
- 2026-10-29 - Public disclosure in good faith, 126 days after the initial report.
Credits
Discovered and reported by Argus-Systems.