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。

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