Skip to content

bug(fat): concurrent page-cache writeback can break cluster chains and panic #2158

Description

@fslongjin

问题概述

异步 page-cache writeback 并发扩展 FAT 文件时,FAT 簇链可能短于文件扩展逻辑预期。FATFile::ensure_len() 随后对不存在的目标簇直接调用 Option::unwrap(),导致内核 panic。

该问题在 PR #2141 的一次 Dunitest CI 中出现,但 PR 的最新变更仅涉及 serial8250 TTY epoll 通知,没有修改 FAT 或 page-cache。相同分支此前连续多次通过 Dunitest,因此目前判断这是一个被时序触发的、独立的 FAT 并发一致性问题,而不是 TTY 变更的直接回归。

现场证据

失败任务:

执行到 normal/proc_mount_exports 时发生 panic:

Kernel Panic Occurred. raw_pid: 19
File: src/filesystem/fat/entry.rs
Line: 295, Column: 18
Message: called `Option::unwrap()` on a `None` value

关键调用链:

FATFile::write
LockedFATInode::write_sync
AsyncPageCacheBackend::write_pages
PageCacheManager::submit_writeback_batch
PageCacheManager::try_start_reclaimer_writeback_ranges
WorkQueue worker

触发点位于稀疏/跨簇扩展路径:

let end_cluster = fs
    .get_cluster_by_relative(self.first_cluster, cluster_offset_start as usize)
    .unwrap();

此处说明 ensure_len() 完成簇分配后,实际可遍历的 FAT chain 仍未覆盖目标 offset。

初步根因分析

当前 FATFileSystem 已有 dirent_io_lock,用于串行化目录项扇区的 read-modify-write,但 FAT 表本身缺少对应的全局 mutation lock。

存在至少两类并发风险:

  1. allocate_cluster() 的“扫描空闲簇 → 标记 EOC → 连接前驱簇 → 更新 FSInfo”不是一个原子事务。不同 inode 的异步 writeback 可以并发执行该流程,可能选择同一个空闲簇或观察到中间状态。
  2. set_entry() 会读取包含多个 FAT entry 的整个扇区、修改其中一个 entry 后写回。不同线程修改同一 FAT 扇区中的不同 entry 时,read-modify-write 窗口可能互相覆盖,丢失刚建立的 chain link。

简单把两个 unwrap() 改成 EINVAL/EIO 只能避免 panic,不能修复已经断裂或交叉的簇链,属于表面 workaround。

Linux 6.6 对照

Linux 6.6 在 struct msdos_sb_info 中维护 per-filesystem fat_lock mutex:

  • fs/fat/fat.h: struct mutex fat_lock
  • fs/fat/fatent.c: fat_alloc_clusters() 在扫描、分配、连接和更新 free-cluster 状态期间持有 fat_lock
  • fat_free_clusters() 同样在遍历和释放 chain 期间持有该锁

DragonOS 应采用等价的 per-filesystem FAT mutation serialization,而不是仅修补 panic 点。

建议修复方向

  1. FATFileSystem 增加 per-filesystem FAT mutation mutex。
  2. 串行化 cluster allocation、link、deallocation 以及相关 FAT-sector RMW。
  3. set_entry() 拆分为公开的加锁入口和内部“已持锁” helper,避免 allocate_cluster()/deallocate_cluster() 嵌套获取同一 mutex。
  4. 审计锁序:inode lock、FAT mutation lock、FSInfo lock、dirent I/O lock 和 block-device I/O 之间不得形成反向依赖。
  5. 保留防御性错误处理:即使检测到损坏的 chain,也应返回明确错误并记录上下文,而不是 panic;但这不能替代一致性修复。
  6. 增加 dunitest,至少并发扩展多个 FAT 文件并触发异步 writeback,验证:
    • 不重复分配 cluster;
    • chain 长度覆盖最终文件大小;
    • 数据和目录项在 sync/remount 后保持完整;
    • 无 panic、死锁或永久 Writeback 页面。

复现状态

  • CI 中已出现一次确定的内核 panic。
  • 同一 PR 分支此前多次成功,说明问题可能依赖 writeback/reclaimer 时序。
  • 本地执行完整 Dunitest 时先遇到另一个无关测试失败,未运行到 proc_mount_exports,因此尚未获得第二次 FAT panic;不能把本地未复现视为问题不存在。

验收标准

  • 并发 FAT allocation/free 不会产生重复分配、丢失 link 或短 chain。
  • FAT entry 的扇区级 RMW 不会互相覆盖。
  • 稀疏扩展和 page-cache reclaimer writeback 不再触发 entry.rs 中的 unwrap panic。
  • 错误路径不泄漏 cluster、不遗留永久 Dirty/Writeback 页面。
  • 新增的并发 FAT dunitest 可稳定通过,并完成一次完整 Dunitest CI。

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-fatfsArea: Fat filesystemA-fsArea: 文件系统bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions