第一部分:核心经验

一、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
2
3
4
5
if visitor.contains_whiteout()
&& let Some(target_upper) = target.upper()
{
// 清理 whiteout ...
}

关键点:

  • 原代码中的 unwrap() 假设“只要合并视图有 whiteout,upper 层就一定存在对应 inode”,但这是错误的假设;
  • 修复后只有在 upper 存在时才清理 upper 内部的 whiteout 文件,upper 为 None 时跳过;
  • 后面的 upper.create(&whiteout_name(name), ...) 必须保持无条件执行,以保证 overlayfs rmdir 的语义正确。

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
    4
    Fix `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
    4
    docker 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.opaque xattr 标记。

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
2
3
4
5
6
make format                              # 自动格式化
make check # 完整 lint 检查
make test # 用户态单元测试
make ktest # 内核态单元测试
make run_kernel AUTO_TEST=regression # 自动运行回归测试
make run_kernel ENABLE_REGRESSION_TEST=true # 只构建不自动运行

附录 E:Git 操作命令速查

1
2
3
4
5
6
7
8
9
10
11
12
# 同步上游
git fetch upstream
git checkout -b fix/xxx upstream/main

# 修改历史
git commit --amend # 修改最近一次 commit
git rebase -i HEAD~N # 交互式合并最近 N 次 commit
git push origin 分支名 --force-with-lease # 覆盖远程

# 查看状态
git log --oneline upstream/main..分支名 # 查看分支多出的提交
git log --oneline 分支名..upstream/main # 查看分支缺少的提交

附录 F:Issue 与 PR 的 Markdown 语法

  • GitHub 使用 GitHub Flavored Markdown(GFM);
  • 代码块用三个反引号包裹,可指定语言;
  • 行内代码用反引号;
  • 列表用 -、*、1.;
  • 引用用 >;
  • Fixes #1234 会自动关联并关闭 issue。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int ovl_link(struct dentry *old, struct inode *newdir,
struct dentry *new)
{
int err;
struct dentry *olddentry;
struct dentry *newdentry;
struct dentry *upperdir;

err = ovl_copy_up(old);
if (err)
goto out;

err = ovl_copy_up(new->d_parent);
if (err)
goto out;

upperdir = ovl_dentry_upper(new->d_parent);
mutex_lock_nested(&upperdir->d_inode->i_mutex, I_MUTEX_PARENT);
newdentry = ovl_lookup_create(upperdir, new);
err = PTR_ERR(newdentry);
if (IS_ERR(newdentry))
goto out_unlock;

olddentry = ovl_dentry_upper(old);
err = vfs_link(olddentry, upperdir->d_inode, newdentry);
if (!err) {
// ...
} else {
// ...
}
out_unlock:
mutex_unlock(&upperdir->d_inode->i_mutex);
out:
return err;
}

核心逻辑:先对源文件执行 ovl_copy_up,再在 upper 层建立硬链接。这个逻辑从 2011 年 OverlayFS 早期补丁延续至今,是 Linux 硬链接语义的标准实现。

问题:OverlayInode::link 把 OverlayInode 直接转发给 upper 层的 RamInode::link,后者执行 downcast_ref::<RamInode>().unwrap() 时 panic。

修复方向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub(crate) fn link(&self, old: &Arc<dyn Inode>, name: &str) -> Result<()> {
if self.type_ != InodeType::Dir {
return_errno_with_message!(Errno::ENOTDIR, "self is not dir");
}

let old_overlay = old
.downcast_ref::<OverlayInode>()
.ok_or(Errno::EXDEV)?;
if old_overlay.type_() == InodeType::Dir {
return_errno_with_message!(Errno::EPERM, "old is a dir");
}

// 先把 old 完整 copy-up 到 upper 层
let old_upper = old_overlay.build_upper_recursively_if_needed()?;

// 再把父目录 build 到 upper 层
let upper = self.build_upper_recursively_if_needed()?;

upper.link(&old_upper, name)
}

与 metacopy 的关系:

  • Asterinas 当前 metacopy 字段未启用,build_upper_recursively_if_needed 会完整复制数据;
  • 与 Linux 默认(metacopy=off)行为一致;
  • 不需要修改 do_copy_up、read_at 等其他函数。

附录 I:OverlayFS 其他相关 bug

在 link 之外,还有两个类似模式的 bug:

  1. lchown panic:在 RamInode::create_impl 中 symlink_target.expect("a symlink target must be provided"),原因是 overlay copy-up 路径没有传 symlink target;
  2. link panic:在 RamInode::link 中 old.downcast_ref::<RamInode>().unwrap()。

这些都是“OverlayFS 转发到底层时假设失败”的同类问题,未来可能需要统一处理。

附录 J:等待 review 期间可以做什么

  • 本地研究下一个 bug,但不一定马上提 PR;
  • 学习相关代码,理解模块之间的交互;
  • 帮别人 review 其他 PR;
  • 在 issue 下留言表示愿意处理,但等当前 PR 落地后再动手;
  • 不要一次性开多个 PR,避免冲突和维护者负担。

附录 K:项目重构对 PR 的影响

  • 当项目正在做大的重构时(如 #3795),针对旧实现的修复通常不会被合并;
  • 维护者会建议等待重构完成后再处理;
  • 这不是代码质量问题,而是时机问题;
  • 保留本地分支,等重构后重新评估是否需要重新提交。

第三部分:结论

这次贡献虽然最终未被合并,但完整覆盖了开源协作的核心流程:

  1. 问题复现:从 bug 报告到本地复现;
  2. 根因分析:定位到 unwrap() 的不安全调用;
  3. 代码修复:遵循 Linux 语义,改为 if let Some(...),并处理 Clippy 建议;
  4. 测试编写:使用项目提供的测试框架,保证幂等性;
  5. CI 验证:本地模拟 CI,修复 lint 问题;
  6. PR 流程:从分支创建、commit 规范、PR 描述到与维护者互动;
  7. 结果处理:面对 PR 未被合并的结果,礼貌回应并关闭。