问题概述
异步 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。
存在至少两类并发风险:
allocate_cluster() 的“扫描空闲簇 → 标记 EOC → 连接前驱簇 → 更新 FSInfo”不是一个原子事务。不同 inode 的异步 writeback 可以并发执行该流程,可能选择同一个空闲簇或观察到中间状态。
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 点。
建议修复方向
- 为
FATFileSystem 增加 per-filesystem FAT mutation mutex。
- 串行化 cluster allocation、link、deallocation 以及相关 FAT-sector RMW。
- 将
set_entry() 拆分为公开的加锁入口和内部“已持锁” helper,避免 allocate_cluster()/deallocate_cluster() 嵌套获取同一 mutex。
- 审计锁序:inode lock、FAT mutation lock、FSInfo lock、dirent I/O lock 和 block-device I/O 之间不得形成反向依赖。
- 保留防御性错误处理:即使检测到损坏的 chain,也应返回明确错误并记录上下文,而不是 panic;但这不能替代一致性修复。
- 增加 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。
问题概述
异步 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:关键调用链:
触发点位于稀疏/跨簇扩展路径:
此处说明
ensure_len()完成簇分配后,实际可遍历的 FAT chain 仍未覆盖目标 offset。初步根因分析
当前
FATFileSystem已有dirent_io_lock,用于串行化目录项扇区的 read-modify-write,但 FAT 表本身缺少对应的全局 mutation lock。存在至少两类并发风险:
allocate_cluster()的“扫描空闲簇 → 标记 EOC → 连接前驱簇 → 更新 FSInfo”不是一个原子事务。不同 inode 的异步 writeback 可以并发执行该流程,可能选择同一个空闲簇或观察到中间状态。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-filesystemfat_lockmutex:fs/fat/fat.h:struct mutex fat_lockfs/fat/fatent.c:fat_alloc_clusters()在扫描、分配、连接和更新 free-cluster 状态期间持有fat_lockfat_free_clusters()同样在遍历和释放 chain 期间持有该锁DragonOS 应采用等价的 per-filesystem FAT mutation serialization,而不是仅修补 panic 点。
建议修复方向
FATFileSystem增加 per-filesystem FAT mutation mutex。set_entry()拆分为公开的加锁入口和内部“已持锁” helper,避免allocate_cluster()/deallocate_cluster()嵌套获取同一 mutex。复现状态
proc_mount_exports,因此尚未获得第二次 FAT panic;不能把本地未复现视为问题不存在。验收标准
entry.rs中的 unwrap panic。