Asterinas OverlayFS rmdir Panic 修复经验总结
第一部分:核心经验
一、Bug 背景与复现
Bug:OverlayInode::rmdir 中对 target.upper().unwrap() 的调用,在删除一个只存在于 lower 层且包含 whiteout 条目的目录时,会因 target.upper() 返回 None 而触发内核 panic。
复现要点:
- 在 lower 层创建一个目录
D,其中包含.wh.foo文件; - upper 层为空,使得
D只存在于 lower 层; - 挂载 overlay 后对合并视图中的
D调用rmdir; - 内核在
overlayfs/fs.rs处 panic。
复现程序的编写要求:
- 使用 C 编写,依赖
sys/mount.h等头文件; - 放在
test/initramfs/src/regression/fs/overlayfs/目录下; - 使用
common/Makefile的wildcard *.c自动编译,无需修改 Makefile; - 程序应当是幂等的,每次运行前清理环境,否则第二次运行会因为 upper 层残留的 whiteout 文件而失败。
二、修复方案
错误做法:
1 | let target_upper = target.upper().unwrap() // 可能会 panic |
正确做法:
1 | if visitor.contains_whiteout() |
关键点:
- 原代码中的
unwrap()假设“只要合并视图有 whiteout,upper 层就一定存在对应 inode”,但这是错误的假设; - 修复后只有在 upper 存在时才清理 upper 内部的 whiteout 文件,upper 为
None时跳过; - 后面的
upper.create(&whiteout_name(name), ...)必须保持无条件执行,以保证 overlayfsrmdir的语义正确。
Clippy 的 collapsible_if 警告:
- 嵌套的
if应当合并为&&形式,符合 Rust 的 let chains 语法。
三、测试的编写
测试框架的关键约束(来自 common/test.h):
FN_SETUP(name) { ... } END_SETUP()用于设置环境;FN_TEST(name) { ... } END_TEST()用于定义测试;CHECK(expr)用于 setup,失败会 abort;TEST_SUCC(func)用于期望成功的调用;TEST_ERRNO(func, err)用于期望失败并返回特定 errno 的调用;TEST_RES(func, cond)要求 errno 为 0 且 cond 为真;- cleanup 阶段应使用裸调用并忽略错误,而不是
CHECK。
常见错误:
TEST_RES(stat(...), _ret < 0 && errno == ENOENT)是错的,因为stat失败时 errno 会被设置为 ENOENT,导致TEST_RES报错;- 应该用
TEST_ERRNO(stat(...), ENOENT)。
测试幂等性:
- 每次运行前必须清理 lower、upper、merged、work 目录;
- 特别注意清理内核在 upper 层创建的 whiteout 文件(如
.wh.D)。
四、Git 与 PR 工作流
分支管理:
- 从最新的
upstream/main创建修复分支,不要基于旧 commit; - 分支命名推荐
fix/issue-编号-简短描述; - 一个 PR 只做一件事(原子提交)。
Commit message 规范(Asterinas 风格):
- 首字母大写、祈使语气、不带 scope;
- 标题控制在 72 字符以内;
- 标识符用反引号包裹;
- 推荐格式:
1
2
3
4Fix `overlayfs` `rmdir` panic when upper inode is absent
Add a regression test for lower-only directories to prevent
future regressions.
修改历史:
- 未 push 时用
git commit --amend或git rebase -i HEAD~N合并 commit; - 已 push 后用
git push origin 分支名 --force-with-lease覆盖远程; --force-with-lease比--force更安全,避免误删他人提交。
PR 描述:
- 使用
Fixes #编号关联 issue; - 包含 Summary、Problem、Fix、Testing、Additional Context 等部分;
- 附上本地测试的原始输出(用 Markdown 代码块包裹)。
五、CI 与本地验证
本地可以模拟 CI:
make format:自动格式化代码;make check:运行完整的 lint(Clippy、rustfmt、typos 等);make test:用户态单元测试;make ktest:内核态单元测试;make run_kernel AUTO_TEST=regression:回归测试。
CI 状态解读:
Review required:需要至少 1 位有写入权限的维护者 Approve;All checks have passed:所有自动检查通过;Merging is blocked:通常因为缺少 Approve,不是代码问题。
CI 失败排查:
- Lint 失败通常是 Clippy 或 rustfmt 的问题,本地
make check可以复现; - 部分测试(如 cgroup
cpu.stat)可能因环境差异而失败,与代码改动无关; - 需要在 PR 中明确说明环境相关的失败。
六、与维护者的沟通
Issue 下的留言:
- 说明复现的版本(commit hash)、根因、修复思路、测试结果;
- 询问是否有人正在处理,避免重复劳动。
PR 下的互动:
- 及时回应 Copilot 和人类维护者的建议;
- 对已解决的评论点击
Resolve conversation; - 对 PR 描述保持更新,让它反映最新的状态。
最终结果:
- 维护者告知正在进行 OverlayFS 大重构(#3795),不打算单独合并旧实现的修复;
- 礼貌回复并
Close with comment关闭 PR; - 保留分支,等待重构后再判断是否需要重新提交。
七、开源协作的心态
- PR 被关闭不等于失败:项目方向变化、大重构、时机不对都会导致 PR 不被合并;
- 经验是实打实的:完整走一遍复现、定位、修复、测试、CI、review 流程,是宝贵的经历;
- 保持礼貌和专业:即使被拒绝,也要感谢维护者的反馈;
- 继续关注项目:维护者邀请继续关注其他 issue,是善意的信号。
第二部分:附录(零碎内容)
附录 A:Asterinas 环境相关
Docker 镜像:asterinas/dev:0.18.1-20260901
- 提供 Rust 工具链、QEMU、Nix 等开发环境;
- 启动命令:
1
2
3
4docker run -it --privileged --network=host \
-v /dev:/dev \
-v $(pwd)/asterinas:/root/asterinas \
asterinas/dev:0.18.1-20260901
Asterinas 版本:main e60f6de4e991d3ad66fc61d0340a363a2096bd85
main是分支名,后面是 commit 的完整 SHA-1;- 可以用
git rev-parse HEAD确认当前版本; - 可以用
git ls-remote https://github.com/asterinas/asterinas.git refs/heads/main查看最新 main 的 hash。
附录 B:OverlayFS 关键概念
whiteout:
- 表示“某个条目已被删除”的标记;
- 通常命名为
.wh.<name>; - 可以出现在 upper 或 lower 层;
- 只隐藏它对应的名字,不隐藏它所在的目录。
opaque 目录:
- 表示“此目录完全替代 lower 层的同名目录”;
- 通常通过
trusted.overlay.opaquexattr 标记。
copy-up:
- 当需要修改一个只存在于 lower 层的文件时,OverlayFS 会先把它复制到 upper 层;
- 包括元数据、数据、xattr 的复制;
- Linux 默认会完整复制(除非启用
metacopy=on)。
metacopy:
- Linux OverlayFS 的一个可选特性;
- 启用后 copy-up 只复制元数据,数据仍然从 lower 层读取;
- Asterinas 的
OverlayConfig中有metacopy: bool字段,但尚未启用(_ => ()会忽略该挂载参数)。
附录 C:测试框架宏语义
| 宏 | 语义 | 适用场景 |
|---|---|---|
CHECK(func) |
errno >= 0 | setup 阶段,失败即 abort |
TEST_SUCC(func) |
errno == 0 | 期望调用成功 |
TEST_ERRNO(func, err) |
errno == err | 期望失败并返回特定 errno |
TEST_RES(func, cond) |
errno == 0 且 cond 为真 | 调用成功且需要验证返回值 |
注意:TEST_RES 要求 errno == 0,不能用于期望失败的调用。
附录 D:Makefile 相关
common/Makefile 的关键行:
1 | C_SRCS := $(wildcard *.c) |
自动扫描当前目录下所有 .c 文件,无需手动添加目标。
CI 相关命令:
1 | make format # 自动格式化 |
附录 E:Git 操作命令速查
1 | # 同步上游 |
附录 F:Issue 与 PR 的 Markdown 语法
- GitHub 使用 GitHub Flavored Markdown(GFM);
- 代码块用三个反引号包裹,可指定语言;
- 行内代码用反引号;
- 列表用
-、*、1.; - 引用用
>; Fixes #1234会自动关联并关闭 issue。
附录 G:Linux ovl_link 参考实现
1 | static int ovl_link(struct dentry *old, struct inode *newdir, |
核心逻辑:先对源文件执行 ovl_copy_up,再在 upper 层建立硬链接。这个逻辑从 2011 年 OverlayFS 早期补丁延续至今,是 Linux 硬链接语义的标准实现。
附录 H:link bug 的修复思路
问题:OverlayInode::link 把 OverlayInode 直接转发给 upper 层的 RamInode::link,后者执行 downcast_ref::<RamInode>().unwrap() 时 panic。
修复方向:
1 | pub(crate) fn link(&self, old: &Arc<dyn Inode>, name: &str) -> Result<()> { |
与 metacopy 的关系:
- Asterinas 当前
metacopy字段未启用,build_upper_recursively_if_needed会完整复制数据; - 与 Linux 默认(
metacopy=off)行为一致; - 不需要修改
do_copy_up、read_at等其他函数。
附录 I:OverlayFS 其他相关 bug
在 link 之外,还有两个类似模式的 bug:
lchownpanic:在RamInode::create_impl中symlink_target.expect("a symlink target must be provided"),原因是 overlay copy-up 路径没有传 symlink target;linkpanic:在RamInode::link中old.downcast_ref::<RamInode>().unwrap()。
这些都是“OverlayFS 转发到底层时假设失败”的同类问题,未来可能需要统一处理。
附录 J:等待 review 期间可以做什么
- 本地研究下一个 bug,但不一定马上提 PR;
- 学习相关代码,理解模块之间的交互;
- 帮别人 review 其他 PR;
- 在 issue 下留言表示愿意处理,但等当前 PR 落地后再动手;
- 不要一次性开多个 PR,避免冲突和维护者负担。
附录 K:项目重构对 PR 的影响
- 当项目正在做大的重构时(如 #3795),针对旧实现的修复通常不会被合并;
- 维护者会建议等待重构完成后再处理;
- 这不是代码质量问题,而是时机问题;
- 保留本地分支,等重构后重新评估是否需要重新提交。
第三部分:结论
这次贡献虽然最终未被合并,但完整覆盖了开源协作的核心流程:
- 问题复现:从 bug 报告到本地复现;
- 根因分析:定位到
unwrap()的不安全调用; - 代码修复:遵循 Linux 语义,改为
if let Some(...),并处理 Clippy 建议; - 测试编写:使用项目提供的测试框架,保证幂等性;
- CI 验证:本地模拟 CI,修复 lint 问题;
- PR 流程:从分支创建、commit 规范、PR 描述到与维护者互动;
- 结果处理:面对 PR 未被合并的结果,礼貌回应并关闭。
