Repository navigation
[Bug] nvme: kernel heap memory leak per inactive namespace in nvme_scan_device() exhausts system heap and can brick boot #11919
Description
Title: [Bug] nvme: kernel heap memory leak per inactive namespace in nvme_scan_device() exhausts system heap and can brick boot
RT-Thread Version
v5.2.0, v5.2.1, v5.2.2, v5.3.0 and current master (introduced by commit 945114f, 2024-11-24, "[DM/FEATURE] Support NVME (#9591)")
Affected area
Device drivers (components/drivers/nvme/nvme.c)
Hardware-BSP vendor
QEMU (qemu-virt64-aarch64 BSP, default .config already enables RT_USING_NVME + RT_NVME_PCI); any board with an NVMe controller
Architecture
ARM64 (also affects any arch using this driver)
Board and hardware details
QEMU virt machine (-M virt) with a stock nvme PCI device (QEMU 9.2.3, unmodified). No special hardware is needed: any NVMe controller whose Identify Controller nn (Number of Namespaces) field is larger than its count of active namespaces triggers the leak. Note QEMU itself always reports nn = 256 while exposing 1 namespace, which is legal per the NVMe spec, so the leak reproduces with a fully compliant device response.
Develop Toolchain
GCC (aarch64 toolchain), scons, QEMU 9.2.3
Describe the bug
nvme_scan_device() (components/drivers/nvme/nvme.c) iterates nsid = 1 .. nn using the device-controlled nn field from the Identify Controller response, and allocates a 4480-byte struct rt_nvme_device (rt_calloc) before issuing the per-namespace Identify command:
for (rt_uint32_t nsid = 1; nsid <= number_of_ns; ++nsid)
{
struct rt_nvme_device *ndev = rt_calloc(1, sizeof(*ndev)); /* 4480 B */
...
if ((err = nvme_identify(nvme, nsid, 0, id)))
{
goto _free_res; /* leak: ndev not freed */
}
if (!id->nsze)
{
continue; /* leak: ndev not freed */ <-- main path
}
...Every namespace that reports nsze == 0 (inactive/allocated-but-not-active namespaces — a perfectly legal response; e.g. QEMU returns an all-zero structure for every non-attached nsid), and every failed identify, leaks 4480 bytes (4536 bytes with the memheap header) of the kernel system heap. The leaked memory is never recovered; the reference is simply dropped.
Measured impact, full-chain on the official qemu-virt64-aarch64 BSP with the default config (free via the free msh command):
| device response | kernel heap available after boot |
|---|---|
| no NVMe device | 66,941,784 B |
nn = 1 (single active ns) |
66,931,592 B (disk nvme0n1 registered, healthy) |
stock QEMU (nn = 256, 1 active ns) |
65,774,912 B — 1,156,872 B (≈1.1 MiB) leaked = 255 × 4536 B |
So with the stock, unmodified QEMU NVMe device (a legal nn = 256 response), every boot loses ~1.8 % of a 64 MiB kernel heap. Repeated controller re-registration (hot-plug / surprise removal + rescan) accumulates the leak linearly.
With a larger device-controlled nn (malformed or malicious controller firmware, e.g. nn = 0xFFFFFF), the loop keeps allocating until the kernel heap is fully exhausted, which happens deterministically before main() can start the shell — the system never reaches the msh prompt (boot bricked):
=== OOM POINT === nsid=14758 avail=600 maxused=67108264 (64 MiB heap)
#0 nvme_scan_device (nvme=0xffff000000263b78, number_of_ns=16384) at components/drivers/nvme/nvme.c:1079
#1 rt_nvme_controller_register (nvme=0xffff000000263b78) at components/drivers/nvme/nvme.c:1216
#2 pci_nvme_probe (pdev=0xffff000000263838) at components/drivers/nvme/nvme-pci.c:95
#3 pci_probe (dev=0xffff00000000038) at components/drivers/pci/pci.c:1050
(serial console output stops permanently after hello rt-thread; no msh /> prompt is ever printed)
To reproduce
- Build the official
qemu-virt64-aarch64BSP (default.config,RT_USING_NVMEis already on). - Create a raw disk image, boot with a stock QEMU nvme device (QEMU reports
nn=256for 1 active namespace — no device model modification needed):dd if=/dev/zero of=/tmp/nvme.img bs=1M count=16 qemu-system-aarch64 -M virt,gic-version=2 -cpu max -m 128M \ -kernel rtthread.bin -serial <console> \ -drive file=/tmp/nvme.img,if=none,id=nvme0 \ -device nvme,drive=nvme0 - Run
freein msh and compare with a boot without the nvme device: ~1.1 MiB of the kernel heap is gone after a single boot;list_blkshows the disk otherwise working normally. - GDB confirmation (gdb-multiarch): break at the
if (!id->nsze) continue;branch (nvme.c:1091) — it is hit once per inactive nsid (2..256 with stock QEMU), andsystem_heap->available_sizedrops by exactly 4536 bytes on each hit.
Expected behaviour
ndev must be allocated only after the namespace is known to be active (nsze != 0 and identify succeeded), or freed on the nsze == 0 / identify-error paths; nn should also not be trusted as a loop bound without an upper limit (e.g. cap by mnan or a sane maximum, and/or use the Active Namespace ID list, CNS 02h).
Actual behaviour
4480 bytes leaked per inactive namespace per controller registration; kernel heap exhaustion and an unbootable system with a large device-reported nn.
Other additional context
- CWE-401 (Missing Release of Memory after Effective Lifetime).
- CVSS 3.1: AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 4.9 (Medium).
- Attack prerequisites (explicit): the attacker must attach a malformed/malicious NVMe controller (AV:P) for the heap-exhaustion/brick outcome; the per-boot 1.1 MiB leak however requires no attacker at all — any compliant drive reporting
nnlarger than its active namespace count triggers it (QEMU's stock nvme device is one example, tested here). - Same device-controlled-metadata parser family as previously reported mtd-cfi ([Bug] mtd-cfi erase-region parser has an off-by-one out-of-bounds write when sect_count reaches CFI_FLASH_SECT_MAX #11272) and usbhost ([Bug] legacy usbdevice core trusts host-controlled wLength for Microsoft OS Compatible ID descriptor responses, causing heap out-of-bounds read and information disclosure #11287/[Bug] legacy USB host trusts device-controlled bNumInterfaces and can trigger out-of-bounds access in fixed interface array #11288) issues, which were accepted.
- No existing CVE/issue covers this (searched NVD, this repo's issues/PRs/branches; nvme-related items are [DM/FEATURE] Support NVME #9591 feature, [DM/NVME] Fixup the QUEUE alloc errorno check #10672 errno fix, [dm][nvme] replace the CPU physical address to DMA address #11650 DMA-address fix).
- Verified present in master (blob 260e1fb04) as of 2026-10-23; introduced in 945114f (first release containing it: v5.2.0).
Activity
github-actions commentedon Oct 22, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.usualtreee commentedon Oct 23, 2026
/claim
usualtreee commentedon Oct 23, 2026
确认复现(带 NVMe 时 available 少 1.17MB / 1,169,600 字节),已修复验证归零,见 PR #11920
github-actions commentedon Oct 23, 2026
@usualtreee has claimed this issue.
Yunlk commentedon Oct 23, 2026
在 QEMU 上复现了,并验证了最小补丁能把缺口收回。三步测量用的是同一个固件、同一套启动参数,只切换 NVMe 设备的有无。
测量方法(与建议步骤的一处偏差)
建议里是用 msh 的
free取值。我这边没有可脚本化的串口输入,所以改成开机时由应用层打印一次内存信息(INIT_APP_EXPORT里延时 300ms 后调rt_memory_info()),效果等价,且可重复:c rt_memory_info(&total, &used, &max_used); rt_kprintf("[MEM] total=%u used=%u available=%u\n", total, used, total - used);同样用这个探针输出
nvme0n1是否存在,用来确认设备真的被枚举。三组数字
推导:
也就是说这个缺口里约 1.10 MB 是泄漏、约 785 KB 是正常占用。1.10 MB 与 issue 里报告的"约 1.15 MB"吻合(255×4536 = 1,156,680,差异来自分配器对齐与首个对象的边界)。
顺带说明:总缺口 1.87 MB 不能直接当成泄漏量,如果只看"有/无设备"的差值会高估约 785 KB。
最小补丁
components/drivers/nvme/nvme.c,把分配挪到
nsze判断之后,并补上注册失败路径的释放:`diff
for (rt_uint32_t nsid = 1; nsid <= number_of_ns; ++nsid)
{
`
另外
rt_hw_blk_disk_register()失败时也是goto _free_res,那条路径同样会漏ndev,补丁里一并补了rt_free(ndev)。这条改法相比"在
continue前加rt_free"的优点是:identify 失败路径自动不再需要释放(那时还没分配),只需处理注册失败一处。没做的部分
if (!id->nsze) continue处确认每个 nsid 命中一次、每次 -4536 字节)。上面的三组数字已经能从总量上等价证明这一点:回收量 / 4536 ≈ 254,与nn = 256、1 个活动命名空间完全对得上。qemu.py(内部为-M virt,acpi=on,its=on,gic-version=3),而不是建议里的-M virt,gic-version=2 -cpu max -m 128M。原因是手工那条命令在本机完全没有串口输出(GICv2 与 GICv3 两种都试过),而qemu.py的-serial mon:stdio组合可以正常拿到 console。这一点如果你们验证时用的是 gic-version=2,结果应当一致(本 issue 只涉及nvme.c的堆分配,与中断控制器无关)。环境:
bsp/qemu-virt64-aarch64默认 .config(RT_USING_NVME+RT_NVME_PCI),QEMU 11.0.0,xpack aarch64-none-elf-gcc 15.2.1,master1c801efa。需要的话我可以把补丁提成 PR,或者把那个开机内存探针改成正式 utest(不过它依赖 NVMe 设备存在,进 CI 可能不合适)。