Skip to content

[nvme] fix: memory leak per inactive namespace in nvme_scan_device() - #11920

Open
usualtreee wants to merge 1 commit into
RT-Thread:masterfrom
usualtreee:fix/nvme-ns-leak
Open

usualtreee wants to merge 1 commit into
RT-Thread:masterfrom
usualtreee:fix/nvme-ns-leak

Conversation

@usualtreee

@usualtreee usualtreee commented Oct 23, 2026 •

Copy link
Copy Markdown

拉取/合并请求描述:(PR description)

为什么提交这份PR (why to submit this PR)

修复 issue #11919:nvme_scan_device()(components/drivers/nvme/nvme.c)在发出每个命名空间的 Identify 命令之前就分配了一个 4480 字节的 struct rt_nvme_device,但设备上报的 nn(Identify Controller 的命名空间数量字段,QEMU 报 nn=256 而实际只暴露 1 个命名空间)会让循环遍历所有 nsid,每个未激活命名空间都泄漏这份内存:

  • 主路径:if (!id->nsze) continue;(QEMU 下 255 次)
  • 错误路径:rt_hw_blk_disk_register() 失败时 goto _free_res

在 bsp/qemu-virt64-aarch64 上实测,挂载 QEMU NVMe 设备后可用堆内存比不挂载少约 1.17 MB(1,169,600 字节)。

你的解决方案是什么 (what is your solution)

  1. 把 rt_calloc 移到 if (!id->nsze) continue; 之后——只在命名空间确认为激活后才分配;
  2. 在 rt_hw_blk_disk_register() 失败路径补 rt_free(ndev),消除错误路径的泄漏。

修改文件:components/drivers/nvme/nvme.c(+9/-7)

请提供验证的bsp和config (provide the config and bsp)

  • BSP: bsp/qemu-virt64-aarch64(默认 .config 已开 RT_USING_NVME + RT_NVME_PCI)
  • 工具链: Arm GNU Toolchain 13.3.Rel1(aarch64-none-elf-gcc)
  • QEMU: qemu-system-aarch64 10.2.1,-M virt,acpi=on,its=on,gic-version=2 -cpu max -m 128M -smp 4
  • NVMe 镜像: 16 MB 裸镜像,-device nvme,id=nvme-ctrl-0,serial=deadbeef -drive if=none,file=nvme.img,format=raw,id=nvme0 -device nvme-ns,drive=nvme0

验证方法:同一份固件分别带/不带 NVMe 设备启动,msh 里执行 free 对比 available size:

场景 available size max used size
不带 NVMe 66,965,536 156,504
带 NVMe(修复前) 65,795,936 1,326,104
带 NVMe(修复后) 66,948,536 173,504
  • 修复前缺口 = 1,169,600 字节(≈1.1 MB),即 255 个未激活命名空间 × 每命名空间 ~4.5 KB;

  • 修复后 available 回到基线附近,剩余 ~17 KB 为 1 个激活命名空间 + 控制器的正常占用,泄漏归零。

  • action: (本地 qemu 实测;CI 编译由仓库 workflow 执行)

当前拉取/合并请求的状态 Intent for your PR

必须选择一项 Choose one (Mandatory):

  • 本拉取/合并请求是一个草稿版本 This PR is for a code-review and is intended to get feedback
  • 本拉取/合并请求是一个成熟版本 This PR is mature, and ready to be integrated into the repo

代码质量 Code Quality:

我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:

  • 已经仔细查看过代码改动的对比 Already check the difference between PR and old code
  • 代码风格正确,包括缩进空格,命名及其他风格 Style guide is adhered to, including spacing, naming and other styles
  • 没有垃圾代码,代码尽量精简,不包含#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up
  • 所有变更均有原因及合理的,并且不会影响到其他软件组件代码或BSP All modifications are justified and not affect other components or BSP
  • 对难懂代码均提供对应的注释 I've commented appropriately where code is tricky
  • 代码是高质量的 Code in this PR is of high quality
  • 已经使用clang-format 源码格式化工具确保格式符合RT-Thread代码规范 This PR has been formatted with clang-format and complies with RT-Thread code specification
  • 如果是新增bsp, 已经添加ci检查到.github/ALL_BSP_COMPILE.json 详细请参考链接BSP自查

nvme_scan_device() allocates a 4480-byte struct rt_nvme_device before
issuing the per-namespace Identify command.  The device-controlled nn
field from Identify Controller (QEMU reports nn=256 while exposing a
single namespace) makes the loop run over every nsid, and for every
inactive namespace the allocation is leaked:

- on the !id->nsze path (the main path, 255 times with QEMU), and
- on the rt_hw_blk_disk_register() error path.

Reproduced on bsp/qemu-virt64-aarch64: with a QEMU NVMe device attached,
               total        used        free      shared  buff/cache   available
Mem:         8002344      689772     6463032        4172     1038688     7312572
Swap:        2097152           0     2097152 reports ~1.17 MB (1,169,600 bytes) less available heap than
without it.

Allocate the device only after the namespace is confirmed active
(id->nsze != 0), and free it when the block disk registration fails.

Refs: RT-Thread#11919
@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • Use workflow from 保持默认分支(通常为 master)
    Keep the default branch (usually master) in Use workflow from
  • 在 branch 输入框填写 PR 分支 fix/nvme-ns-leak
    Enter PR branch fix/nvme-ns-leak in the branch field
  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将作为独立提交推送至你的分支。
    The formatting changes will be pushed to your branch as a separate commit.

完成后,提交将自动更新至 fix/nvme-ns-leak 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the fix/nvme-ns-leak branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@github-actions

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: components

Reviewers: @Maihuanyi

Changed Files (Click to expand)
  • components/drivers/nvme/nvme.c

📊 Current Review Status (Last Updated: 2026-10-23 10:59 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@Yunlk

Yunlk commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

独立复跑完成,补丁有效。数据贴在下面,另外有一处数字需要你确认一下。

环境

  • bsp/qemu-virt64-aarch64 默认 .config(RT_USING_NVME + RT_NVME_PCI)
  • QEMU 11.0.0,xpack aarch64-none-elf-gcc 15.2.1,基线 tree 1c801efa
  • 本 PR 分支:fix/nvme-ns-leak @ 0581b812b
  • 取值方式:开机后由应用层调用 rt_memory_info() 打印一次(我这边没法脚本化驱动 msh 的 free,用等价方式代替),同时打印 nvme0n1 是否存在以确认设备真的枚举

三组数字(本机)

配置 used available
不带 NVMe 设备 5,055,752 62,053,112
带 NVMe,未修复(master) 7,011,800 60,097,064
带 NVMe,本 PR 5,859,144 61,249,720

推导:

  • 未修复总缺口 = 7,011,800 − 5,055,752 = 1,956,048 B ≈ 1.87 MB
  • 本 PR 回收 = 7,011,800 − 5,859,144 = 1,152,656 B,÷4536 ≈ 254 个对象 ✅
  • 剩余增量(控制器 + 1 个活动命名空间的正常占用)= 62,053,112 − 61,249,720 = 803,392 B ≈ 785 KB

交叉验证:我自己按同样思路写了一份补丁(calloc 后移 + 注册失败释放),跑出来 used=5,859,200 / available=61,249,664,与本 PR 相差 56 字节(编译布局噪声),功能等价。所以我那份不提 PR 了。

需要你确认的一处

你给的三组是:不带设备 66,965,536、修复前 65,795,936、修复后 66,948,536。

按这组数字推:

  • 修复前缺口 = 66,965,536 − 65,795,936 = 1,169,600 B(与泄漏量吻合,约 255 个对象 ✅)
  • 修复后相对不带设备只多了 17,000 B(66,965,536 − 66,948,536)

但我这边修复后的正常占用是 803,392 B,差了将近 47 倍。一个挂着 4 个 IO queue(RT_USING_NVME_IO_QUEUE 4)的 NVMe 控制器 + 1 个活动命名空间,只占 17 KB 看起来偏小。

可能的原因:

  1. 「修复后」那次启动时 -device nvme-ns 没挂上(控制器在但命名空间不在),那 nvme0n1 就不会出现,占用自然小很多
  2. 三组不是同一份 .config(例如 IO queue 数量不同)
  3. 「不带设备」那组的基线里混进了别的东西

能否确认一下修复后那次 nvme0n1 确实存在、以及三组是否同一份 .config?如果是 1 或 2,那「修复前缺口 1,169,600」正好接近泄漏量(因为总缺口里几乎不含正常占用),反而更干净——但需要说明,否则读者会拿 1,169,600 去对比我这边的 1,956,048,以为两边矛盾。

diff 复核(按项目要求只看两处)

  1. rt_calloc 确实挪到了 nsze 判断之后 ✅ 不活动的 namespace 不会再分配对象
  2. 注册失败路径加了 rt_free(ndev) ✅

另外确认两点没有引入新问题:

  • identify 失败路径现在是 goto _free_res,此时 ndev 尚未分配,所以不需要释放,也不会用到未初始化的 ndev ✅
  • _free_res 之后没有对 ndev 的使用;正常路径 err 保持 RT_EOK ✅

一个小建议(不影响合入):rt_free(ndev) 之后如果将来有人在这条路径上加日志或继续操作 ndev,容易踩野指针;可以考虑 rt_free(ndev); ndev = RT_NULL;,与文件里其他错误路径的风格保持一致。纯属防御,你判断要不要加。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants