From bb7668d971f463c7aeb9740316e5f1dffe06fc34 Mon Sep 17 00:00:00 2001 From: sparkzky Date: Mon, 10 Aug 2026 16:57:02 +0800 Subject: [PATCH 1/3] feat(fs): implement inotify filesystem event notification Implements inotify (issue #2151): the fsnotify core notification layer, the inotify pseudo-device, 4 syscalls (init/init1/add_watch/rm_watch), and VFS write-path hooks for all standard events (create/delete/move/ modify/access/close/attrib/self events). Architecture: - fsnotify/ unified dispatch layer: global (inode_id, dev_id) index, TOTAL_WATCHES atomic fast-path (zero cost when no watches), lock-family separation (global index lock / events lock / wd lock never nested). - inotify.rs device: InotifyInode implements IndexNode + PollableInode, epoll-integrated via LockedEPItemLinkedList, exact inotify_event layout (name field aligned to sizeof(inotify_event)=16, matching Linux ABI). - VFS hooks placed in syscall-core layer (vcore/open/rename_utils/...), NOT per-filesystem: single anchor covers ext4/tmpfs/overlayfs/fuse. Hooks fire only after success and never alter syscall return values. Review fixes incorporated: - Directory watches receive child content events (issue B): IN_MODIFY/ ACCESS/OPEN/CLOSE delivered to parent dir watch with child name. MountFSInode::as_any_ref() returns the inner inode's Any, so use downcast_arc instead of downcast_ref for parent resolution. - Guard DELETE_SELF on hardlink unlink/rename-over: only emit when i_nlink reaches 0, matching Linux fsnotify_link_count() semantics. - Composite mark index key (inode_id, dev_id): prevents FUSE cross-mount event leakage when multiple mounts reuse same inode number. - Remove has_any_watch() gate on File::inotify_parent resolution so watches added after open receive content events. - Tolerate metadata failure after unlink/rename-over (FUSE GETATTR can return ENOENT); cache nlinks before the namespace operation. - SYS_INOTIFY_INIT only registered on x86_64 (generic syscall ABI uses inotify_init1); riscv64/loongarch64 lack the legacy init syscall. - Skip MOVED events on no-op rename; EOF reads no longer deliver IN_ACCESS; reject mask==0 and IN_MASK_ADD|IN_MASK_CREATE. Test: dunitest inotify_dir_watch (2 tests) + inotify_events (6 tests) covering content/namespace/self events, multi-instance, and poll. Design doc: docs/kernel/filesystem/inotify.md Signed-off-by: sparkzky --- docs/kernel/filesystem/inotify.md | 570 ++++++++++++ kernel/src/filesystem/fsnotify/group.rs | 36 + kernel/src/filesystem/fsnotify/mark.rs | 73 ++ kernel/src/filesystem/fsnotify/mod.rs | 293 ++++++ kernel/src/filesystem/inotify.rs | 870 ++++++++++++++++++ kernel/src/filesystem/mod.rs | 2 + kernel/src/filesystem/vfs/file.rs | 90 +- kernel/src/filesystem/vfs/mod.rs | 1 + kernel/src/filesystem/vfs/mount/mod.rs | 19 + kernel/src/filesystem/vfs/open.rs | 39 +- .../src/filesystem/vfs/syscall/link_utils.rs | 13 +- .../filesystem/vfs/syscall/rename_utils.rs | 108 ++- .../filesystem/vfs/syscall/symlink_utils.rs | 3 + kernel/src/filesystem/vfs/vcore.rs | 65 +- .../suites/normal/inotify_dir_watch.cc | 150 +++ .../dunitest/suites/normal/inotify_events.cc | 334 +++++++ user/apps/tests/dunitest/whitelist.txt | 2 + 17 files changed, 2652 insertions(+), 16 deletions(-) create mode 100644 docs/kernel/filesystem/inotify.md create mode 100644 kernel/src/filesystem/fsnotify/group.rs create mode 100644 kernel/src/filesystem/fsnotify/mark.rs create mode 100644 kernel/src/filesystem/fsnotify/mod.rs create mode 100644 kernel/src/filesystem/inotify.rs create mode 100644 user/apps/tests/dunitest/suites/normal/inotify_dir_watch.cc create mode 100644 user/apps/tests/dunitest/suites/normal/inotify_events.cc diff --git a/docs/kernel/filesystem/inotify.md b/docs/kernel/filesystem/inotify.md new file mode 100644 index 0000000000..997d198e42 --- /dev/null +++ b/docs/kernel/filesystem/inotify.md @@ -0,0 +1,570 @@ +# inotify 文件系统事件通知 — 设计与实施计划 + +> 对应 issue: [DragonOS-Community/DragonOS#2151](https://github.com/DragonOS-Community/DragonOS/issues/2151) +> 参考实现:Linux 6.6 `fs/notify/`、`fs/anon_inodes.c`、`include/uapi/linux/inotify.h` +> 状态:**设计评审中** + +本文档是实施前的架构设计。目标:在 DragonOS 内核实现完整的 inotify(`inotify_init1` / `inotify_add_watch` / `inotify_rm_watch` / `read`),覆盖标准事件集,支持 epoll。 + +--- + +## 0. 设计原则(不可妥协) + +1. **Linux 语义对齐**:行为参考 Linux 6.6。`inotify_event` 字节布局、事件 mask、错误码、read 语义必须与 glibc/strace 期望一致。 +2. **低冗余**:VFS 写路径 hook 只在每个操作的**唯一入口**插一处,不逐个文件系统实现去改。 +3. **不破坏现有功能**:所有 hook 必须在「操作成功之后」触发,且 hook 本身不能阻塞/失败导致原操作回退。`fsnotify()` 是「尽力而为」投递,绝不影响 syscall 的返回值。 +4. **不引入 workaround**:mark 生命周期用强引用 pinning 语义解决,而非「删了再假装没删」。 +5. **不过度设计**:fsnotify 层只保留 inotify 当前需要的最小抽象;不预先实现 connector/SRCU/superblock-mark/mount-mark(Linux 有,DragonOS 暂不需要)。 + +--- + +## 1. 总体架构 + +三层,自底向上: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ syscall 层 (inotify_init1/add_watch/rm_watch) │ +│ → 创建 InotifyInstance → 注册为伪文件 fd │ +└─────────────────────────────────────────────────────────────┘ + │ read(fd) / poll(fd) / epoll(fd) + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ inotify 设备 (filesystem/inotify.rs) │ +│ InotifyInode: impl IndexNode + PollableInode │ +│ - 事件队列 (VecDeque) │ +│ - WaitQueue + LockedEPItemLinkedList (epoll 集成) │ +│ InotifyBackend: impl FsNotifyBackend (事件格式化/入队/合并) │ +└─────────────────────────────────────────────────────────────┘ + ▲ handle_event(mark, mask, name, cookie, is_dir) + │ +┌─────────────────────────────────────────────────────────────┐ +│ fsnotify 统一通知层 (filesystem/fsnotify/) │ +│ fsnotify(): VFS hook 调用的统一入口 │ +│ → 用 inode_id 在全局 mark 索引中找匹配的 mark │ +│ → 对每个 mark 调 backend.handle_event() │ +│ FsNotifyGroup: 一个通知消费者(一个 inotify fd 对应一个) │ +│ FsNotifyMark: 一个 watch (group + inode + mask + wd) │ +└─────────────────────────────────────────────────────────────┘ + ▲ fsnotify() 调用点 + │ +┌─────────────────────────────────────────────────────────────┐ +│ VFS 写路径 hook 点(syscall-core + File 层,非各 FS 实现) │ +│ vcore.rs / rename_utils.rs / symlink_utils.rs / link_ │ +│ utils.rs / open.rs + File::do_read/do_write/Drop │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 为什么 fsnotify 与 inotify 分两层 + +这是 Linux 的天然接缝,**不是为了未来 fanotify 而做的投机抽象**: +- VFS hook 只知道「某 inode 上发生了某事件」,不知道事件如何被消费。 +- inotify 负责「把事件格式化成 `inotify_event`、按 wd 关联、合并、入队」。 + +即使永不做 fanotify,这个边界也让 VFS hook 保持轻薄、与消费侧解耦。`FsNotifyBackend` trait 仅 3 个方法(见 §3),不是过度抽象。 + +--- + +## 2. VFS hook 点(核心改动面) + +### 2.1 决策:hook 在 syscall-core 层,不在 MountFSInode,不在各 FS 实现 + +**理由(这是最关键的架构决策):** + +1. **已具备 child inode 上下文**。经核实,vcore/rename_utils 在执行变异**之前**已经 `find()`/`lookup()` 解析出目标 inode: + - `do_unlink_at` `vcore.rs:705` `let target_inode = parent_inode.find(filename)?;` + - `do_remove_dir` `vcore.rs:665` `let target_inode = parent_inode.find(filename)?;` + - `do_renameat2` `rename_utils.rs:87` `let old_inode = old_parent_inode.lookup(old_filename)?;` + - `do_sys_open` `open.rs:347-358` create 成功后持有 `inode` 与 `parent_inode`。 + + 这意味着 `IN_DELETE_SELF`/`IN_MOVE_SELF` 所需的「被监听 inode 自身」已经握在手里,无需再 lookup(避免 TOCTOU 竞态)。 + +2. **最低 regression 风险**。MountFSInode (`mount/mod.rs:3585+`) 使用复杂的 `DentryMutationContext`、双层锁(children_gate / dentry_namespace_lock)。在那里插桩要求 hook 在这些锁下安全;而 syscall-core 层的锁上下文更简单、更可控。 + +3. **唯一入口,低冗余**。所有用户态触发的 namespace 变更都经过 syscall → vcore/open/rename_utils,一处 hook 覆盖全部文件系统(ext4/tmpfs/overlayfs/fuse/ramfs/...)。无需逐 FS 修改。 + +4. **Linux 同构**。Linux 的 fsnotify hook 就在 `fs/namei.c`(`do_unlinkat`/`vfs_rename` 等)和 `fs/read_write.c`,即 syscall 层。 + +> 与 issue 原文「修改各文件系统实现」的建议不同——侦察发现 syscall-core 层是更优的单一锚点。这不是 workaround,是更彻底的去重。 + +### 2.2 hook 点清单 + +| 事件 | 位置(已核实) | 通知对象 | name 字段 | +|---|---|---|---| +| `IN_CREATE` | `open.rs` create 成功后 (`:359` `created=true`);`vcore.rs do_mkdir_at` (`:595` mkdir 返回后) | 父目录的 watch | 子项名 | +| `IN_OPEN` | `open.rs do_sys_open`:在 `File` 构造并 `alloc_fd` 成功、返回 fd 前(约 `open.rs` 末尾成功路径) | 被打开 inode 的 watch | 无 | +| `IN_DELETE` | `vcore.rs do_unlink_at` (`:713` unlink 成功后) | 父目录 watch | 子项名 | +| `IN_DELETE_SELF` + `IN_IGNORED` | `do_unlink_at` 同处(已有 `target_inode`) | `target_inode` 自身 watch;随后移除该 mark | 无 | +| `IN_MOVED_FROM` / `IN_MOVED_TO` | `rename_utils.rs do_renameat2` (`:124` move_to 成功后);共享 cookie | 源/目标父目录 watch | 子项名 | +| `IN_MOVE_SELF` | `do_renameat2` 同处(已有 `old_inode`) | `old_inode` 自身 watch | 无 | +| `IN_MODIFY` | `File::do_write` 成功后(`file.rs` do_write 返回 `Ok(n)` 处) | 被写 inode 的 watch | 无 | +| `IN_MODIFY`(truncate) | `do_ftruncate`/`resize` 成功路径;`fallocate` 的写类模式(`FALLOC_FL_PUNCH_HOLE`/`ZERO_RANGE`/正常分配,非 `KEEP_SIZE`)成功后 | 被 setattr inode 的 watch | 无 | +| `IN_ACCESS` | `File::do_read` 成功后 | 被读 inode 的 watch | 无 | +| `IN_CLOSE_WRITE` / `IN_CLOSE_NOWRITE` | `File::drop`(= 最后一次 close,见 §2.4) | 被关闭 inode 的 watch | 无 | +| `IN_ATTRIB` | `do_chmod`/`do_chown`/`do_utimensat`/`do_truncate`(setattr syscall-core)成功后;定位见 `kernel/src/filesystem/vfs/syscall/` 各 `do_*` | 被 setattr inode 的 watch | 无 | + +`do_symlinkat`(`symlink_utils.rs`)/`do_linkat`(`link_utils.rs`) 成功后 → 父目录 `IN_CREATE`(子项名)。 + +### 2.3 `fsnotify()` 调用约定 + +```rust +// filesystem/fsnotify/mod.rs +/// 统一事件投递入口。在 VFS 操作成功后调用。 +/// - to_parent: 对子项事件,传父目录 inode + 子项名; +/// - to_child: 对自身事件(DELETE_SELF/MOVE_SELF/MODIFY/CLOSE/OPEN),传目标 inode; +/// 二者可同时非空(如 unlink:父目录得 IN_DELETE,子项得 IN_DELETE_SELF)。 +pub fn fsnotify( + mask: FsEvent, // FS_CREATE / FS_MODIFY / ... + parent: Option<(&Arc, &str)>, // (父目录, 子项名) + child: Option<&Arc>, // 目标 inode 自身 + cookie: u32, // move 配对用,否则 0 +) +``` + +实现:持全局 `FSNOTIFY` 自旋锁(irqsave),分别用 parent/child 的 `inode_id()` 在全局 mark 索引中查匹配 mark,对每个 mark 调 `group.backend.handle_event(mark, mask, name, cookie, is_dir)`。 + +**铁律**:`fsnotify()` 内部**只能**获取 `FSNOTIFY` 全局锁与 group 队列锁,**绝不**回调任何 `IndexNode` 方法(避免在 MountFSInode/File 锁下重入 VFS)。所需的 `inode_id` 与 `is_dir` 在调用前由调用方从已持有的 inode metadata 取好传入(或 `fsnotify` 内部仅读 `metadata()`——但为安全起见,调用方预取 `is_dir` 传入更稳妥;inode_id 由 fsnotify 内部读 metadata,因为 inode 活着、metadata 只读不锁,安全)。 + +> 锁序(全代码库一致):`MountFSInode/File 锁` → `FSNOTIFY 全局锁` → `group 队列锁`。任何反向获取即 bug。 + +### 2.4 close 事件为什么放 `File::drop` + +`Arc` 的 `Drop` 只在**最后一个引用**释放时执行(Rust 语义保证),等价于 Linux 的 `__fput`。`File::drop`(`file.rs:2151`)已是 epoll 释放、flock 释放、`inode.close()` 的汇聚点。在此根据 `self.mode` 是否含 `FMODE_WRITER` 决定 `IN_CLOSE_WRITE` 还是 `IN_CLOSE_NOWRITE`。 + +注意:`Drop` 里 inode 的 Arc 仍活着(`self.inode`),可安全取 `inode_id`。 + +### 2.5 `FMODE_NONOTIFY` 的用途 + +`file.rs:513` 已定义 `FMODE_NONOTIFY = 0x4000000`(open_fmode 已支持从 flags 传入),但当前无人消费。用途: +- inotify fd 自身的 File 打开时设置 `FMODE_NONOTIFY`。 +- 所有 File 层 hook(do_read/do_write/Drop)开头检查:`if self.mode.contains(FMODE_NONOTIFY) { return; }`,避免对 inotify fd 的 read/poll 产生递归事件。 + +namespace hook(vcore 层)操作的是常规文件,不涉及 `FMODE_NONOTIFY`。 + +--- + +## 3. fsnotify 通知层 + +### 3.1 模块结构 + +``` +kernel/src/filesystem/fsnotify/ + mod.rs — 事件 mask、fsnotify()、全局 mark 索引、FsNotifyBackend trait + group.rs — FsNotifyGroup + mark.rs — FsNotifyMark + 生命周期 +``` + +### 3.2 核心数据结构(数据结构优先) + +```rust +// === 事件 mask(对应 Linux FS_* 内核事件,与用户态 IN_* 分离)=== +bitflags! { + pub struct FsEvent: u32 { + const ACCESS = 0x00000001; // IN_ACCESS + const MODIFY = 0x00000002; // IN_MODIFY + const ATTRIB = 0x00000004; // IN_ATTRIB + const CLOSE_WRITE= 0x00000008; // IN_CLOSE_WRITE + const CLOSE_NOWRITE=0x00000010; // IN_CLOSE_NOWRITE + const OPEN = 0x00000020; // IN_OPEN + const MOVED_FROM = 0x00000040; // IN_MOVED_FROM + const MOVED_TO = 0x00000080; // IN_MOVED_TO + const CREATE = 0x00000100; // IN_CREATE + const DELETE = 0x00000200; // IN_DELETE + const DELETE_SELF= 0x00000400; // IN_DELETE_SELF + const MOVE_SELF = 0x00000800; // IN_MOVE_SELF + // 内核内部 + const UNMOUNT = 0x00002000; // 文件系统卸载 + const Q_OVERFLOW = 0x00004000; // 队列溢出 + const IN_IGNORED = 0x00008000; // watch 被撤销(inode 删除/卸载) + const ISDIR = 0x40000000; // 事件对象是目录(由 dispatch 设置) + } +} + +/// 一个 watch:连接 group 与 inode。 +pub struct FsNotifyMark { + pub wd: i32, // watch descriptor,group 内唯一 + pub group: Weak, // 所属 group(避免环引用) + pub inode: Arc, // 强引用:watch 期间 pin 住 inode(防 evict) + pub mask: AtomicU32, // 订阅 mask(IN_MASK_ADD 并发改,必须原子读,对齐 Linux fsnotify_mark.mask) + pub oneshot: AtomicBool, // IN_ONESHOT:触发一次后自动撤销 + pub excl_unlink: bool, // IN_EXCL_UNLINK +} + +/// 一个通知消费者(一个 inotify fd 对应一个 group)。 +pub struct FsNotifyGroup { + pub backend: Box, // 后端自带内部锁;fsnotify 层只依赖 trait,不反向依赖 inotify 类型 + pub marks: Mutex>>, // group 拥有的所有 mark(强引用) + pub wait_queue: WaitQueue, // read 阻塞 / 唤醒 + pub epitems: LockedEPItemLinkedList, // epoll 集成 +} + +/// inotify 后端。**事件队列与 wd 表用两把独立锁**,使 read(消费)与 add_watch/rm_watch(wd 管理)不互相阻塞, +/// 对齐 Linux notification_lock(事件)与 group 内 mark 锁分离。 +pub struct InotifyBackend { + // —— 事件锁:handle_event(生产) 与 read(消费) 竞争 —— + pub events: SpinLock, // irqsave:fsnotify 可在持 VFS 锁时调用 + pub max_queued_events: usize, // 常量 16384(见 §6.1),入队前检查 + // —— wd 锁:add_watch/rm_watch/read(wd→mark) 竞争 —— + pub wd: Mutex, // wd_counter + wd_map +} +pub struct InotifyQueue { + pub list: VecDeque, + pub overflowed: bool, // 置位后后续插入一个 IN_Q_OVERFLOW(wd=-1) +} +pub struct WdTable { + pub counter: i32, // 单调分配 wd(饱和见 §6.2) + pub map: BTreeMap>, +} + +/// 队列里的一个事件(已格式化为 inotify 语义,含 wd)。 +pub struct InotifyEventInfo { + pub wd: i32, + pub mask: u32, // 已转为用户态 IN_* mask + pub cookie: u32, + pub name: Option>, // 子项名(目录 watch 的子事件才有) +} + +/// 后端接口(最小抽象)。fsnotify 层通过此 trait 调用,保持 VFS↔fsnotify↔inotify 单向依赖。 +pub trait FsNotifyBackend: Send + Sync { + fn handle_event(&self, group: &FsNotifyGroup, mark: &FsNotifyMark, + mask: FsEvent, name: Option<&str>, cookie: u32); + fn free_mark(&self, mark: &FsNotifyMark); // mark 销毁时从 wd 表移除 + fn free_group(&self); // fd close 收尾 + fn queue_nonempty(&self) -> bool; // poll 用 +} +``` + +### 3.3 全局 mark 索引 + +```rust +// 用 inode_id 反查「挂在该 inode 上的所有 mark」。 +// 存 Weak:group 拥有 mark(强),索引只做查找,不阻止回收。 +static FSNOTIFY_MARKS: SpinLock>>>> = ...; +// 亦可在 FsManager / 一个 FsNotifyRegistry 结构体里,避免全局 static 初始化顺序坑点。 +``` + +**为什么 key 用 `InodeId` 安全**(关键正确性论证): +- `InodeId` 由 `generate_inode_id()`(`vcore.rs:72`,原子计数器)分配,**仅在 inode 被完全释放后才可能复用**。 +- mark 持有 inode 的**强 `Arc`**,故 watch 期间 inode 不可能被 evict,其 `InodeId` 不会被复用。 +- mark 从 group 移除时,**同步**从全局索引删除对应 Weak。删除后即使原 inode 释放、`InodeId` 被新 inode 复用,索引里已无该 key,不会误匹配。 +- dispatch 时 `Weak::upgrade()` 失败的死引用:惰性清理(upgrade 失败即从 vec 移除),不影响正确性。 + +> FUSE 的 `inode_generation()`:FUSE 可能复用 inode 号。watch 期间 inode 被 Arc pin,generation 稳定;`InodeId` 在 DragonOS 是全局原子值(非 FS 内部号),不随 FUSE 内部号复用而复用。故无需把 generation 纳入 key。(实现时若发现 FUSE 路径有 inode 对象替换,再以 `Arc::addr` 兜底——留作实现期验证点。) + +### 3.4 dispatch 流程(fsnotify 主体) + +``` +// 全局 watch 计数:绝大多数时刻为 0(系统未使用 inotify)。fsnotify 的第一道闸门, +// 无 watch 时零锁开销——对齐 Linux i_fsnotify_mask/DCACHE_FSNOTIFY_PARENT_WATCHED 的快速跳过。 +static TOTAL_WATCHES: AtomicUsize = AtomicUsize::new(0); + +fsnotify(mask, parent, child, cookie): + // ① 快速路径:无任何 watch → 直接返回(read/write/close 热路径零成本) + if TOTAL_WATCHES.load(Relaxed) == 0 { return } + // ② 收集候选(调用方已预取 is_dir 传入更佳;此处仍可读 metadata,inode 活着且只读) + lock FSNOTIFY (irqsave) + for (inode, name_opt) in [(parent.inode, parent.name), (child, None)] 若非空: + id = inode.metadata().inode_id() + snapshot += FSNOTIFY_MARKS.get(id) 里 upgrade 成功的 Arc // 死 Weak 顺手剔除 + unlock FSNOTIFY // 临界区仅哈希查表 + // ③ 锁外投递(不在全局锁内做 backend 工作) + for each mark in snapshot: + if (mark.mask.load(Relaxed) & mask.bits()) == 0 { continue } // 原子读 mask 过滤 + mark.group.backend.handle_event(&group, &mark, mask, name_opt, cookie) // 内部取 events 锁 + if mark.oneshot { 撤销该 mark } +``` + +**锁族分离(无死锁/低竞争)**: +- 数据锁族 A(投递路径):`FSNOTIFY 全局锁`(查表,秒放)→ mark 所在 group 的 `events 锁`(入队)。二者**不嵌套**:全局锁在 handle_event 前释放。 +- 数据锁族 B(控制路径):add_watch/rm_watch 取 `wd 锁` + `marks 锁` + 全局索引锁;**不取 events 锁**。 +- read 路径只取 `events 锁`。 +- 故 events 锁 与 wd/marks/全局索引锁 几乎不相交 → read 不阻塞 add_watch,反之亦然。 +- 与外部 VFS/File 锁的顺序:`MountFSInode/File 锁`(调用方已持有)→ `FSNOTIFY` → `events`。永不反向。 + +--- + +## 4. inotify 设备层 + +### 4.1 模块结构 + +``` +kernel/src/filesystem/inotify.rs +``` + +### 4.2 伪文件实现(照搬 eventfd/signalfd 模式,不抽 anon_inode 公共框架) + +```rust +pub struct InotifyFs { /* FileSystem:返回 magic,无挂载 */ } // 类比 EventFdFs +#[cast_to([crate::filesystem::vfs::CastTo])] // 沿用现有 cast 宏 +pub struct InotifyInode { + group: Arc, + // read_at/poll 直接委托给 group +} +impl IndexNode for InotifyInode { /* read_at / metadata / fs / is_stream=true ... */ } +impl PollableInode for InotifyInode { /* poll/add_epitem/remove_epitem 委托 group */ } +``` + +**为什么不做 anon_inode 公共框架**:eventfd/signalfd 各自实现伪 FS,工作良好;抽取公共框架是独立重构,会动到 eventfd/signalfd(引入 regression 风险),且对 inotify 无收益。遵循「渐进演化 > 革命性重写」。inotify 照搬现有模式即可。**anon_inode 统一框架列为后续可选重构**(不在本 issue 范围)。 + +### 4.3 read 语义(必须精确) + +`struct inotify_event`(小端,无 padding): + +```c +struct inotify_event { + int wd; // 4 + uint32_t mask; // 4 + uint32_t cookie; // 4 + uint32_t len; // 4 = name 长度(含末尾 NUL,向上对齐到 8 的倍数) + char name[]; // 变长,NUL 填充到 len +}; +``` + +`read(fd, buf, count)`: +1. 若 `count < sizeof(inotify_event)`(16)→ `EINVAL`。 +2. 从队列头部逐个取事件,序列化写入 buf,**只写完整事件**(写不下的留在队列,下次读)。 +3. 一个事件之后,若剩余空间 ≥ 下一个事件大小,继续打包;否则停止。 +4. `name` 按 Linux:长度 = `strlen+1`,向上对齐到 8 字节倍数,不足补 NUL。 +5. 阻塞(`O_NONBLOCK` 未设且队列空)→ `wq_wait_event_interruptible!`,被信号打断 → `EINTR`/`ERESTARTSYS`。 +6. `O_NONBLOCK` 且队列空 → `EAGAIN`。 + +`is_stream() = true`(inotify fd 不可 seek;pread/pwrite/lseek → `ESPIPE`),`read_at` 忽略 offset,从队列头出队。 + +### 4.4 poll / epoll + +```rust +fn poll(&self, ..) -> Result { + if !group.backend.events.is_empty() { Ok(EPOLLIN.bits() | EPOLLRDNORM.bits()) } + else { Ok(0) } +} +``` + +事件入队后:`wait_queue.wakeup_all()` + `EventPoll::wakeup_epoll(&group.epitems, EPOLLIN|EPOLLRDNORM)`。完全类比 `eventfd.rs` / `signalfd.rs`。 + +--- + +## 5. 生命周期管理(最易出 bug 处) + +### 5.1 add_watch + +``` +sys_inotify_add_watch(fd, path, mask): + 1. 从 fd 取 File → 取 InotifyInode → group + 2. 解析 path → inode(遵循 IN_DONT_FOLLOW:不跟随末尾 symlink) + 3. 权限检查:`permission::check_inode_permission(&inode, &md, PermissionMask::MAY_READ)` 失败 → EACCES。对齐 Linux `inode_permission(MAY_READ)`,防止无读权限者通过监听泄露文件名/元数据(评审 Blocker 1 落实点,本就已在设计中,此处明确 API)。 + 4. IN_ONLYDIR 且非目录 → ENOTDIR + 5. 同 inode 上已有该 group 的 mark: + - IN_MASK_CREATE → EEXIST + - IN_MASK_ADD → 原子 mask.fetch_or(新 mask)(不替换、不换 wd) + - 否则 → mask.store(新 mask),返回原 wd + 6. 超 max_user_watches → ENOSPC + 7. 分配 wd(group 内单调,饱和见 §6.2),建 FsNotifyMark{inode: 强 Arc, mask: AtomicU32} + 8. 加入 group.marks;加入全局索引 inode_id → Weak + 9. `TOTAL_WATCHES.fetch_add(1)`(维护 §3.4 快速路径计数;原子,无锁) + 10. 返回 wd +``` + +> **TOTAL_WATCHES 维护**:add_watch +1;rm_watch / mark 因 DELETE_SELF/UNMOUNT 撤销 / group 销毁 -1。归零后 fsnotify 即走零开销快速路径。计数为近似值(Relaxed),仅用于短路,不影响正确性。 + +### 5.2 rm_watch + +``` +sys_inotify_rm_watch(fd, wd): + 从 wd 表取 mark → 从 group.marks 与全局索引移除 → TOTAL_WATCHES.fetch_sub(1) → drop 强引用 + wd 无效 → EINVAL +``` + +### 5.3 inode 被删除(unlink/rmdir)— 关键 + +- 父目录 watch 得 `IN_DELETE`/(rmdir 时子项含 `IN_ISDIR`)。 +- **若被删 inode 自身有 mark**:其 group 得 `IN_DELETE_SELF` + `IN_IGNORED`,然后该 mark 从 group 与全局索引移除(强引用释放,inode 方可真正 evict)。 +- 见 §2.2,hook 在 `do_unlink_at`/`do_remove_dir` 成功后,`target_inode` 已在手。 + +### 5.4 inode 被移动(rename)— 关键 + +- 源父目录 watch 得 `IN_MOVED_FROM`(带 cookie)。 +- 目标父目录 watch 得 `IN_MOVED_TO`(同 cookie,便于用户态配对)。 +- 若被移动 inode 自身有 mark:得 `IN_MOVE_SELF`。mark **不**删除(inode 仍存活,只是换了位置)。 +- cookie:本次 rename 内 `AtomicI32::fetch_add(1)` 取一个,FROM/TO 共用;0 表示无 move。 + +### 5.5 group 销毁(inotify fd close) + +`File::drop` → 触发 inode.close → InotifyInode 收尾: +- 遍历 group.marks,逐个从全局索引移除,drop。 +- 唤醒 wait_queue / 清理 epitems。 +- group 释放。所有 watch 自然失效(符合 fd 关闭后 watch 失效语义)。 + +### 5.6 文件系统卸载(unmount) + +- 遍历该 sb 上所有 mark 的 inode,发 `IN_UNMOUNT` + `IN_IGNORED`,移除 mark。 +- DragonOS 卸载路径需插入一次 mark 扫描(实现期定位卸载入口)。**这是语义铁律,不能省**(Linux `fsnotify_sb_delete`)。列为必须项;若卸载路径当前不发,先记 TODO 但不阻塞 inotify 主体——卸载场景在 ANOLISA 用例中不会触发(skillfs/memory 是长挂载)。 + +--- + +## 6. 约束、限制与边界 + +### 6.1 资源限制(常量,先不接 procfs sysctl) + +| 限制 | 默认值 | 超限错误 | +|---|---|---| +| `max_user_instances` | 128 | inotify_init1 → `EMFILE` | +| `max_user_watches` | 8192 | inotify_add_watch → `ENOSPC` | +| `max_queued_events` | 16384 | 超限后丢事件,插入一个 `IN_Q_OVERFLOW`(wd=-1) | +| 单事件队列字节上限 | 按现有 VecDeque 自然增长,配 max_queued_events 上限即可 | — | + +`/proc/sys/fs/inotify/*` sysctl 暴露列为后续增强(不影响内核正确性)。**强制执行**:`inotify_init1`/`add_watch` 在分配前检查对应上限并返回错误;`handle_event` 入队前检查 `max_queued_events`,超限即丢弃并置 `overflowed`,随后插入单个 `IN_Q_OVERFLOW`(wd=-1,仅一次,清 flag)。这是防止恶意/失控进程撑爆内核内存的硬约束(评审 Blocker 2 落实点)。 + +### 6.2 wd 分配 + +group 内单调递增 `i32`(正数)。Linux 用 idr 回收 wd;DragonOS 用单调计数器——**不引入 idr = 不引入不需要的复杂度**。溢出处理(评审 Minor 落实点):wd 仅取 `1..=i32::MAX-1`;counter 饱和,再 add_watch 时返回 `ENOSPC`(单 group 21 亿次 watch 才触发,可接受;绝不产生负 wd,因 `-1` 被 `IN_Q_OVERFLOW` 占用)。`cookie: u32` 用 `wrapping_add` 回绕,回绕合法(Linux 同)。 + +### 6.3 事件合并(coalescing) + +为匹配 Linux 行为并防止 write 风暴: +- 入队前,若队列**末尾**事件与本事件 `(wd, mask, cookie, name)` 完全相同且 mask 属于可合并类(`ACCESS`/`MODIFY`,无 name),则**丢弃**新事件(Linux `inotify_merge` / `event_compare`)。 +- 带不同 name 或不同 cookie 的事件不合并。 + +合并是优化也是正确性(避免 `IN_Q_OVERFLOW`)。但**不是**语义硬要求——即便不合并,行为仍合法,只是事件多。实现里做最简末尾去重。 + +### 6.4 用户态 mask 位(完整支持) + +事件位:`IN_ACCESS/MODIFY/ATTRIB/CLOSE_WRITE/CLOSE_NOWRITE/OPEN/MOVED_FROM/MOVED_TO/CREATE/DELETE/DELETE_SELF/MOVE_SELF`,`IN_ISDIR`(dispatch 设置,非订阅)。 + +控制位(add_watch 传入): +- `IN_DONT_FOLLOW` — 路径解析不跟随末尾 symlink。 +- `IN_EXCL_UNLINK` — 子项被 unlink 后不再为它产生事件(入队时按 mark 的此位过滤 unlinked 子项事件)。 +- `IN_MASK_ADD` — 增量并,不替换。 +- `IN_MASK_CREATE` — 已存在则 EEXIST。 +- `IN_ONESHOT` — 触发一次后自动撤销。 +- `IN_ONLYDIR` — 仅当目标是目录。 + +init1 控制位: +- `IN_CLOEXEC` → fd 设 close-on-exec。 +- `IN_NONBLOCK` → fd 的 File 设 `O_NONBLOCK`。 + +--- + +## 7. syscall 注册 + +4 个号已在 `kernel/src/arch/x86_64/syscall/nr.rs` 定义(253/254/255/294),未注册 handler。 + +在 `kernel/src/syscall/` 新增模块(或 inotify.rs 内),用 `declare_syscall!` 注册: + +| nr | handler | 签名约定(沿用 Syscall trait) | +|---|---|---| +| 253 `sys_inotify_init` | 无参,等价 `inotify_init1(0)` | `-> Result` 返回 fd | +| 294 `sys_inotify_init1` | `(flags: u32)` | 解析 `IN_CLOEXEC`/`IN_NONBLOCK` | +| 254 `sys_inotify_add_watch` | `(fd, path: *const u8, mask: u32)` | `vfs_check_and_clone_cstr` 取路径 | +| 255 `sys_inotify_rm_watch` | `(fd, wd: i32)` | — | + +fd 创建流程(沿用 eventfd): +``` +let inode = Arc::new(InotifyInode::new(group)); +let mut file = File::new(inode, O_RDONLY); +file.mode |= FMODE_NONOTIFY; // 防递归 +if flags & IN_NONBLOCK { file.mode |= O_NONBLOCK 对应位 } +let fd = pcb.fd_table().alloc_fd(Arc::new(file), cloexec)?; +``` + +> 实现期需确认:`File::new` 的确切参数、`alloc_fd` 的 cloexec 设置方式、`FMODE_NONOTIFY` 与 FileMode 的关系(是否需新增位)。以 eventfd 的 fd 创建代码为模板。 + +--- + +## 8. 改动文件清单 + +| 文件 | 动作 | 说明 | +|---|---|---| +| `kernel/src/filesystem/fsnotify/mod.rs` | 新增 | mask、fsnotify()、全局索引、Backend trait | +| `kernel/src/filesystem/fsnotify/group.rs` | 新增 | FsNotifyGroup | +| `kernel/src/filesystem/fsnotify/mark.rs` | 新增 | FsNotifyMark + 生命周期 | +| `kernel/src/filesystem/inotify.rs` | 新增 | InotifyFs/Inode/Backend、syscalls | +| `kernel/src/filesystem/mod.rs` | 改 | `pub mod fsnotify; pub mod inotify;` | +| `kernel/src/filesystem/vfs/vcore.rs` | 改 | do_unlink_at/do_remove_dir/do_mkdir_at 后插 fsnotify | +| `kernel/src/filesystem/vfs/syscall/rename_utils.rs` | 改 | do_renameat2 后插 MOVED_FROM/TO/MOVE_SELF | +| `kernel/src/filesystem/vfs/syscall/symlink_utils.rs` | 改 | do_symlinkat 后插 CREATE | +| `kernel/src/filesystem/vfs/syscall/link_utils.rs` | 改 | do_linkat 后插 CREATE | +| `kernel/src/filesystem/vfs/open.rs` | 改 | do_sys_open CREATE 后插 IN_CREATE;成功插 IN_OPEN | +| `kernel/src/filesystem/vfs/file.rs` | 改 | do_read→ACCESS、do_write→MODIFY、Drop→CLOSE_*;FMODE_NONOTIFY 短路 | +| chmod/chown/utimensat/truncate 入口 | 改 | IN_ATTRIB / (truncate)MODIFY | +| syscall 注册文件 | 改 | declare_syscall! ×4 | + +**不动**:MountFSInode、各 FS 的 IndexNode 实现(ext4/tmpfs/overlayfs/fuse/...)、eventfd/signalfd。 + +### 量级估计 +- 新增 fsnotify + inotify:~1200–1800 行(含注释)。 +- hook 插桩:每处 5–15 行,~10 处 → ~150 行。 +- 合计落在 issue 估计的 2000–3000 行区间偏低端(因选择 syscall-core 单点 hook,省去逐 FS 改动)。 + +--- + +## 9. 验证计划 + +### 9.1 单元/集成自测(user/apps/c_unitest 风格) +1. `inotify_init1(O_NONBLOCK|O_CLOEXEC)` 返回有效 fd。 +2. `add_watch("/tmp/test", IN_ALL_EVENTS)` 成功,返回 wd≥0。 +3. `touch /tmp/test/a` → read 得 `IN_CREATE`;`echo x > a` → `IN_MODIFY`/`IN_CLOSE_WRITE`;`rm a` → `IN_DELETE`。 +4. 监听目录、重命名子文件 → `IN_MOVED_FROM`+`IN_MOVED_TO` cookie 相等。 +5. `rm` 被监听文件本身 → `IN_DELETE_SELF`+`IN_IGNORED`。 +6. epoll inotify fd:有事件时 `EPOLLIN` 触发;无事件阻塞。 +7. `O_NONBLOCK` 空队列 read → `EAGAIN`。 +8. `count < 16` read → `EINVAL`。 +9. 多事件打包、name 对齐到 8 字节。 +10. `IN_MASK_ADD`/`IN_MASK_CREATE`/`IN_ONESHOT`/`IN_ONLYDIR` 行为。 +11. fd 关闭后所有 watch 失效。 + +### 9.2 回归保护(铁律:不破坏现有功能) +- `make kernel` 通过编译。 +- 既有文件系统测试(ext4/tmpfs)不受影响:hook 在「成功之后」且不改变返回值。 +- 特别检查:`FMODE_NONOTIFY` 短路不影响 eventfd/signalfd(它们不走新 hook)。 +- 大压力写循环不触发 panic / 死锁(锁序验证)。 + +### 9.3 用户态证据 +最终交付时附上:自测程序源码 + 运行输出(read 得到的 `inotify_event` hexdump)、epoll 触发日志、`make kernel` 编译成功日志。 + +--- + +## 10. 风险与缓解 + +| 风险 | 缓解 | +|---|---| +| hook 在持有 VFS 锁下调用导致死锁 | `fsnotify()` 只取自己的锁;调用方预取 is_dir;不在 hook 内回调 IndexNode 写方法 | +| mark 悬空引用 | mark 强引用 pin inode;删除时同步清索引;dispatch 惰性清理死 Weak | +| inode_id 复用误匹配 | 强引用保证 watch 期 id 不复用;见 §3.3 论证 | +| close 事件误触(多次 Drop) | Rust `Drop` 仅最后一次执行;天然等价 `__fput` | +| FUSE inode 对象替换 | 实现期验证;必要时用 `Arc::addr` 兜底 key | +| 事件风暴 / 队列溢出 | max_queued_events + 末尾去重 + IN_Q_OVERFLOW | +| 递归(inotify fd 自身被监听) | `FMODE_NONOTIFY` 短路 | +| unmount 不发 IN_UNMOUNT | 先标记 TODO,不阻塞主体;ANOLISA 用例不触发 | + +--- + +## 11. 非目标(明确不做) + +- fanotify。 +- superblock / mount 级 mark(仅 inode mark)。 +- anon_inode 公共框架重构(照搬 eventfd 模式)。 +- `/proc/sys/fs/inotify/*` sysctl 读写(仅内核常量)。 +- procfs/sysfs 的 inotify(仅覆盖经 syscall-core 的常规/挂载 FS)。 +- idr wd 回收(单调计数器)。 + +这些是**有意识的范围裁剪**,均不损害 inotify 核心语义与 ANOLISA 用例。每条都可独立后续迭代。 + + +--- + +## 12. 对抗性评审记录(maintainer 裁决) + +独立 reviewer subagent 对方案做了 7 维对抗性审查,maintainer 逐条裁决如下: + +| 评审意见 | 严重度 | 裁决 | 落实 | +|---|---|---|---| +| add_watch 缺读权限检查 | Blocker | **驳回(误读)** | §5.1 本就有;明确为 `check_inode_permission(MAY_READ)` | +| 队列/实例/watch 无上限致 OOM | Blocker | **驳回(误读)** | §6.1 本就有;补强「强制执行」段 | +| mask 无锁读 = 数据竞争 | Blocker | **采纳** | mask 改 `AtomicU32`(§3.2/§3.4) | +| 全局锁无快速路径 | Major | **采纳** | `TOTAL_WATCHES` 原子短路(§3.4) | +| hook 定位不精确 + fallocate 遗漏 | Major | **采纳** | 收紧行号;补 fallocate→IN_MODIFY(§2.2) | +| backend 锁粒度过粗 | Major | **采纳** | events 锁与 wd 锁拆分(§3.2/§3.4) | +| trait 写死具体类型 | Major | **采纳** | backend 改 `Box`(§3.2) | +| wd 溢出 | Minor | **采纳** | 饱和到 i32::MAX-1,溢出返 ENOSPC(§6.2) | + +6 条有效改进已纳入;2 条误读已驳回并顺手收紧表述。**结论:方案可进入实施。** \ No newline at end of file diff --git a/kernel/src/filesystem/fsnotify/group.rs b/kernel/src/filesystem/fsnotify/group.rs new file mode 100644 index 0000000000..754ef6e84a --- /dev/null +++ b/kernel/src/filesystem/fsnotify/group.rs @@ -0,0 +1,36 @@ +//! [`FsNotifyGroup`]:一个通知消费者(一个 inotify fd 对应一个 group)。 + +use alloc::boxed::Box; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use crate::filesystem::epoll::event_poll::EPollItemList; +use crate::libs::mutex::Mutex; +use crate::libs::wait_queue::WaitQueue; + +use super::mark::FsNotifyMark; +use super::FsNotifyBackend; + +/// 一个通知消费者。一个 inotify fd 对应一个 group。 +/// +/// - `backend`:具体后端(自带内部锁),fsnotify 层只依赖 [`FsNotifyBackend`] trait; +/// - `marks`:group 拥有的所有 mark(强引用,pin 住被监听 inode); +/// - `wait_queue` / `epitems`:read 阻塞唤醒与 epoll 集成。 +#[derive(Debug)] +pub struct FsNotifyGroup { + pub backend: Box, + pub marks: Mutex>>, + pub wait_queue: WaitQueue, + pub epitems: EPollItemList, +} + +impl FsNotifyGroup { + pub fn new(backend: Box) -> Arc { + Arc::new(Self { + backend, + marks: Mutex::new(Vec::new()), + wait_queue: WaitQueue::default(), + epitems: EPollItemList::default(), + }) + } +} diff --git a/kernel/src/filesystem/fsnotify/mark.rs b/kernel/src/filesystem/fsnotify/mark.rs new file mode 100644 index 0000000000..b278317275 --- /dev/null +++ b/kernel/src/filesystem/fsnotify/mark.rs @@ -0,0 +1,73 @@ +//! [`FsNotifyMark`]:一个 watch(group + inode + mask + wd)及其生命周期管理。 + +use alloc::sync::{Arc, Weak}; +use core::sync::atomic::{AtomicBool, AtomicU32}; + +use crate::filesystem::vfs::{IndexNode, InodeId}; + +use super::{adjust_total_watches, index_remove, FsNotifyGroup}; + +/// 一个 watch:连接 group 与 inode。 +/// +/// 生命周期:由 `group.marks` 持有强引用(pin 住被监听 inode),全局索引持 `Weak`。 +/// 撤销时机:`rm_watch`、`IN_DELETE_SELF`/`IN_UNMOUNT` 触发、group 销毁。 +#[derive(Debug)] +pub struct FsNotifyMark { + /// watch descriptor,group 内唯一。 + pub wd: i32, + /// 所属 group(弱引用,避免环引用)。 + pub group: Weak, + /// 强引用:watch 期间 pin 住 inode(防 evict,保证 InodeId 不复用)。 + pub inode: Arc, + /// 订阅 mask(`IN_MASK_ADD` 并发改,必须原子读)。 + pub mask: AtomicU32, + /// `IN_ONESHOT`:触发一次后自动撤销。 + pub oneshot: AtomicBool, + /// `IN_EXCL_UNLINK`:已 unlink 子项不再产生事件。 + pub excl_unlink: bool, +} + +impl FsNotifyMark { + /// 取被监听 inode 的标识:(inode_id, dev_id) 复合键。 + /// + /// FUSE 等多挂载场景可能复用相同 inode 号(如 FUSE_ROOT_ID=1), + /// 必须用 (inode_id, dev_id) 组合区分不同挂载上的 inode,否则会跨挂载 + /// 误匹配 mark,导致事件泄露或误判已有 watch。 + pub fn identity(&self) -> (InodeId, usize) { + self.inode + .metadata() + .map(|m| (m.inode_id, m.dev_id)) + .unwrap_or((InodeId::new(0), 0)) + } +} + +/// 撤销一个 mark:从 group.marks、全局索引移除,并维护全局计数。 +/// +/// 在 `rm_watch`、`DELETE_SELF`/`UNMOUNT` dispatch、group 销毁时调用。 +/// 注意:不取 events 锁,故与 read 路径互不阻塞(锁族分离)。 +pub fn destroy_mark(mark: &Arc) { + let Some(group) = mark.group.upgrade() else { + // group 已销毁,mark 仅可能残留在 snapshot 中;直接清索引即可。 + index_remove(mark); + return; + }; + + // 从 group.marks 移除(按指针相等)。 + let mut marks = group.marks.lock(); + let before = marks.len(); + marks.retain(|m| !Arc::ptr_eq(m, mark)); + let removed = before != marks.len(); + drop(marks); + + if removed { + // 投递 IN_IGNORED:watch 被撤销(rm_watch/oneshot/DELETE_SELF/UNMOUNT 均经此路径)。 + // shutdown(fd close) 不调用 destroy_mark,故不误发。 + group.backend.notify_ignored(&group, mark); + // 通知后端从其内部结构(wd 表)移除。 + group.backend.free_mark(mark); + // 从全局索引移除。 + index_remove(mark); + // 维护全局 watch 计数(唯一计数器,覆盖上限检查 + 快速路径)。 + adjust_total_watches(-1); + } +} diff --git a/kernel/src/filesystem/fsnotify/mod.rs b/kernel/src/filesystem/fsnotify/mod.rs new file mode 100644 index 0000000000..0890cd7ec1 --- /dev/null +++ b/kernel/src/filesystem/fsnotify/mod.rs @@ -0,0 +1,293 @@ +//! 文件系统事件通知统一层(fsnotify)。 +//! +//! 本模块是 VFS 写路径 hook 与具体后端(当前仅 inotify)之间的解耦层。 +//! VFS hook 只调用 [`fsnotify`],由它查全局 mark 索引并把事件分发给匹配的 watch。 +//! +//! 设计原则(见 `docs/kernel/filesystem/inotify.md` §0/§3): +//! - `fsnotify()` 尽力而为,绝不影响 syscall 返回值; +//! - `fsnotify()` 内部只取本层自旋锁与 group 队列锁,绝不回调 `IndexNode` 写方法; +//! - 锁序:`MountFSInode/File 锁` → `FSNOTIFY 全局锁` → `group 队列锁`,永不反向。 + +pub mod group; +pub mod mark; + +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; + +use hashbrown::HashMap; + +use crate::filesystem::vfs::{FileType, IndexNode, InodeId}; +use crate::libs::spinlock::SpinLock; +use system_error::SystemError; + +pub use group::FsNotifyGroup; +pub use mark::FsNotifyMark; + +// 事件 mask:对应 Linux 内核 `FS_*` 事件,其比特位与用户态 `IN_*` 完全一致, +// 故可直接作为用户态 mask 使用(仅 `ISDIR` 由 dispatch 按需设置)。 +// +// 参考:Linux `include/uapi/linux/inotify.h`、`include/linux/fsnotify_backend.h`。 +bitflags::bitflags! { + pub struct FsEvent: u32 { + const ACCESS = 0x00000001; // IN_ACCESS + const MODIFY = 0x00000002; // IN_MODIFY + const ATTRIB = 0x00000004; // IN_ATTRIB + const CLOSE_WRITE = 0x00000008; // IN_CLOSE_WRITE + const CLOSE_NOWRITE= 0x00000010; // IN_CLOSE_NOWRITE + const OPEN = 0x00000020; // IN_OPEN + const MOVED_FROM = 0x00000040; // IN_MOVED_FROM + const MOVED_TO = 0x00000080; // IN_MOVED_TO + const CREATE = 0x00000100; // IN_CREATE + const DELETE = 0x00000200; // IN_DELETE + const DELETE_SELF = 0x00000400; // IN_DELETE_SELF + const MOVE_SELF = 0x00000800; // IN_MOVE_SELF + const UNMOUNT = 0x00002000; // IN_UNMOUNT(文件系统卸载) + const Q_OVERFLOW = 0x00004000; // IN_Q_OVERFLOW(队列溢出) + const IN_IGNORED = 0x00008000; // watch 被撤销(inode 删除/卸载) + const ISDIR = 0x40000000; // 事件对象是目录(由 dispatch 设置) + } +} + +/// 后端接口(最小抽象)。 +/// +/// fsnotify 层通过此 trait 调用具体后端,保持 VFS → fsnotify → inotify 单向依赖。 +pub trait FsNotifyBackend: Send + Sync + core::fmt::Debug { + /// 处理一个事件:格式化、(可选)合并、入队,并唤醒等待者。 + fn handle_event( + &self, + group: &FsNotifyGroup, + mark: &FsNotifyMark, + mask: FsEvent, + name: Option<&str>, + cookie: u32, + ); + /// mark 销毁时从后端内部结构(如 wd 表)移除。 + fn free_mark(&self, mark: &FsNotifyMark); + /// mark 被撤销时向消费者投递一个 IN_IGNORED 事件(rm_watch/oneshot/DELETE_SELF/UNMOUNT)。 + /// fd close(shutdown) 路径不调用此方法。 + fn notify_ignored(&self, group: &FsNotifyGroup, mark: &FsNotifyMark); + /// poll 用:队列是否非空。 + fn queue_nonempty(&self) -> bool; +} + +/// 全局 watch 计数:绝大多数时刻为 0。`fsnotify` 的第一道闸门—— +/// 无 watch 时零锁开销(对齐 Linux `i_fsnotify_mask` 快速跳过)。 +static TOTAL_WATCHES: AtomicUsize = AtomicUsize::new(0); + +/// move 事件 cookie 分配器:每次 rename 取一个,FROM/TO 共享。 +/// 0 表示「无 move」,故从 1 开始,回绕时跳过 0。 +static NEXT_COOKIE: AtomicU32 = AtomicU32::new(1); + +/// 取一个新的非零 move cookie。 +pub fn next_cookie() -> u32 { + loop { + let c = NEXT_COOKIE.fetch_add(1, Ordering::Relaxed); + if c != 0 { + return c; + } + } +} +// 全局 mark 索引:用 `(InodeId, dev_id)` 复合键反查「挂在该 inode 上的所有 mark」。 +// +// 必须用复合键:FUSE 多挂载会复用相同 inode 号(如 FUSE_ROOT_ID=1),纯 InodeId 键 +// 会跨挂载误匹配,导致事件泄露 / 误判已有 watch。 +// 存 `Weak`:group 拥有 mark(强引用),索引只做查找,不阻止回收。 +// dispatch 时 `Weak::upgrade()` 失败的死引用会被惰性剔除。 +type MarkIndex = HashMap<(InodeId, usize), Vec>>; + +lazy_static::lazy_static! { + static ref FSNOTIFY_MARKS: SpinLock = SpinLock::new(HashMap::new()); +} + +/// 记录一次 watch 计数变更(add +1,撤销 -1)。仅用于短路,Relaxed 即可。 +pub(crate) fn adjust_total_watches(delta: i32) { + if delta >= 0 { + TOTAL_WATCHES.fetch_add(delta as usize, Ordering::Relaxed); + } else { + TOTAL_WATCHES.fetch_sub((-delta) as usize, Ordering::Relaxed); + } +} + +/// 系统中是否存在任意 inotify watch。供 VFS 热路径(open/read/write/close)做廉价短路: +/// 无 watch 时完全跳过 parent 解析与 fsnotify 调用(零开销)。 +pub fn has_any_watch() -> bool { + TOTAL_WATCHES.load(Ordering::Relaxed) != 0 +} + +/// 原子地预留一个 watch 槽位(用于上限检查)。 +/// 成功时 TOTAL_WATCHES 已 +1;超限时回退并返回 ENOSPC。 +/// 这是唯一的全局 watch 计数器,同时服务「快速路径短路」和「max_user_watches 上限检查」。 +pub(crate) fn try_reserve_watch(max: usize) -> Result<(), SystemError> { + let prev = TOTAL_WATCHES.fetch_add(1, Ordering::Relaxed); + if prev >= max { + TOTAL_WATCHES.fetch_sub(1, Ordering::Relaxed); + return Err(SystemError::ENOSPC); + } + Ok(()) +} + +pub(crate) fn index_add(mark: &Arc) { + let key = mark.identity(); + let mut idx = FSNOTIFY_MARKS.lock_irqsave(); + idx.entry(key).or_default().push(Arc::downgrade(mark)); +} +/// 把 mark 从全局索引移除(按指针相等匹配,rm_watch / 撤销时调用)。 +pub(crate) fn index_remove(mark: &FsNotifyMark) { + let key = mark.identity(); + let self_ptr = mark as *const FsNotifyMark; + let mut idx = FSNOTIFY_MARKS.lock_irqsave(); + if let Some(vec) = idx.get_mut(&key) { + let mut i = 0; + while i < vec.len() { + // 剔除:指针相等(同一个 mark),或 Weak 已死。 + let drop_it = match vec[i].upgrade() { + Some(arc) => core::ptr::eq(Arc::as_ptr(&arc), self_ptr), + None => true, + }; + if drop_it { + vec.swap_remove(i); + } else { + i += 1; + } + } + if vec.is_empty() { + idx.remove(&key); + } + } +} + +/// 统一事件投递入口。在 VFS 操作**成功之后**调用,尽力而为,不影响调用方返回值。 +/// +/// - `parent`:对子项事件(CREATE/DELETE/MOVED_*),传 `(父目录 inode, 子项名)`; +/// - `child`:对自身事件(DELETE_SELF/MOVE_SELF/MODIFY/CLOSE/OPEN/ATTRIB),传目标 inode; +/// - 二者可同时非空(如 unlink:父目录得 `IN_DELETE`,子项得 `IN_DELETE_SELF`)。 +/// +/// # 安全性 +/// 内部只读 `inode.metadata()`(inode 活着、metadata 只读不锁),不调用任何写方法, +/// 避免在调用方持有的 VFS/File 锁下重入。 +pub fn fsnotify( + mask: FsEvent, + parent: Option<(&Arc, &str)>, + child: Option<&Arc>, + cookie: u32, +) { + // ① 快速路径:系统无任何 watch → 直接返回(read/write/close 热路径零成本)。 + if TOTAL_WATCHES.load(Ordering::Relaxed) == 0 { + return; + } + + // ② 预取 inode 元数据(inode 活着、metadata 只读不锁,安全)。 + // 事件的「主体」是 child(被创建/删除/移动/修改的对象);IN_ISDIR 由主体是否为 + // 目录决定,对 parent/child 两类 watch 一视同仁。若无 child(仅父目录自身事件 + // 的退化情况),ISDIR 不置位。 + // 用 (inode_id, dev_id) 复合键:FUSE 多挂载复用相同 inode 号,必须加 dev_id 区分。 + let (child_key, event_is_dir, child_unlinked) = match child { + Some(c) => match c.metadata() { + Ok(md) => ( + Some((md.inode_id, md.dev_id)), + md.file_type == FileType::Dir, + md.nlinks == 0, + ), + Err(_) => (None, false, false), + }, + None => (None, false, false), + }; + let parent_key = parent.and_then(|(p, _)| p.metadata().ok().map(|md| (md.inode_id, md.dev_id))); + + if child_key.is_none() && parent_key.is_none() { + return; + } + + // ③ 收集候选 mark 快照:临界区仅做哈希查表(秒放),不做后端工作。 + // (mark 强引用, name, is_parent) + let mut snapshot: Vec<(Arc, Option<&str>, bool)> = Vec::new(); + { + let idx = FSNOTIFY_MARKS.lock_irqsave(); + if let Some(pk) = parent_key { + let name = parent.map(|(_, n)| n); + if let Some(vec) = idx.get(&pk) { + for w in vec.iter() { + if let Some(m) = w.upgrade() { + snapshot.push((m, name, true)); + } + } + } + } + if let Some(ck) = child_key { + if let Some(vec) = idx.get(&ck) { + for w in vec.iter() { + if let Some(m) = w.upgrade() { + snapshot.push((m, None, false)); + } + } + } + } + } + // 注:死 Weak 在 lock 内 upgrade 失败时被跳过;惰性清理留给 index_remove。 + + // 事件路由(Linux 模型): + // - 命名空间事件 CREATE/DELETE/MOVED_FROM/MOVED_TO:仅父目录 watch 收(带 name); + // - 自身事件 DELETE_SELF/MOVE_SELF:仅子项自身 watch 收; + // - 内容类事件 ACCESS/MODIFY/ATTRIB/CLOSE_*/OPEN:父目录 watch(带 name)与子项自身 watch + // 均收——使「监听目录」能收到子文件被读/写/开关/改属性的事件(inotify 头号用例)。 + // 一次 fsnotify 调用可同时通知父目录与子项(如 unlink:父得 IN_DELETE,子得 IN_DELETE_SELF)。 + let self_only = FsEvent::DELETE_SELF | FsEvent::MOVE_SELF; + let parent_only = FsEvent::CREATE | FsEvent::DELETE | FsEvent::MOVED_FROM | FsEvent::MOVED_TO; + // 内容类事件(IN_EXCL_UNLINK 抑制对象)。 + let content_type = FsEvent::MODIFY + | FsEvent::ACCESS + | FsEvent::ATTRIB + | FsEvent::CLOSE_WRITE + | FsEvent::CLOSE_NOWRITE + | FsEvent::OPEN; + + // ④ 锁外投递。 + for (mark, name, is_parent) in snapshot { + // 父 mark 收除 self_only 外的全部;自身 mark 收除 parent_only 外的全部。 + let routed = if is_parent { + mask & !self_only + } else { + mask & !parent_only + }; + if routed.is_empty() { + continue; + } + + // inode 死亡事件(DELETE_SELF/UNMOUNT):无论 watch 是否订阅都必须撤销 mark + // 并投递 IN_IGNORED(由 destroy_mark 无条件入队),否则 watch 泄漏强引用。 + let inode_death = + routed.contains(FsEvent::DELETE_SELF) || routed.contains(FsEvent::UNMOUNT); + + let subscribed = mark.mask.load(Ordering::Relaxed); + let mask_matches = (subscribed & routed.bits()) != 0; + + // 非 inode-death 事件:未订阅或被 EXCL_UNLINK 抑制时跳过。 + if !inode_death { + if !mask_matches { + continue; + } + // IN_EXCL_UNLINK:仅对子项的「内容类」事件、且子项已 unlink 时抑制。 + if is_parent && mark.excl_unlink && routed.intersects(content_type) && child_unlinked { + continue; + } + } + + // dispatch 设置 ISDIR(主体是目录时)。 + let mut delivered = routed; + if event_is_dir { + delivered |= FsEvent::ISDIR; + } + + if let Some(group) = mark.group.upgrade() { + group + .backend + .handle_event(&group, &mark, delivered, name, cookie); + } + + // 撤销:inode 死亡(无条件)或 oneshot(订阅匹配后触发一次即撤销)。 + if inode_death || mark.oneshot.load(Ordering::Relaxed) { + mark::destroy_mark(&mark); + } + } +} diff --git a/kernel/src/filesystem/inotify.rs b/kernel/src/filesystem/inotify.rs new file mode 100644 index 0000000000..c6cb100269 --- /dev/null +++ b/kernel/src/filesystem/inotify.rs @@ -0,0 +1,870 @@ +//! inotify 文件系统事件通知设备层。 +//! +//! 实现伪文件(`InotifyInode`:`IndexNode + PollableInode`)与 inotify 后端 +//!(`InotifyBackend`:`FsNotifyBackend`),以及 4 个 syscall handler。 +//! +//! 模式照搬 `eventfd.rs`:伪 FS + 伪 Inode + epoll 集成。 +//! +//! 详见 `docs/kernel/filesystem/inotify.md` §4。 + +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::string::{String, ToString}; +use alloc::sync::{Arc, Weak}; +use alloc::vec::Vec; +use core::any::Any; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + +use alloc::boxed::Box; +use system_error::SystemError; + +use crate::arch::interrupt::TrapFrame; +use crate::arch::syscall::nr::{SYS_INOTIFY_ADD_WATCH, SYS_INOTIFY_INIT1, SYS_INOTIFY_RM_WATCH}; +// SYS_INOTIFY_INIT 仅存在于 x86_64(Linux generic syscall ABI 用 inotify_init1 替代)。 +#[cfg(target_arch = "x86_64")] +use crate::arch::syscall::nr::SYS_INOTIFY_INIT; +use crate::arch::MMArch; +use crate::filesystem::epoll::event_poll::EventPoll; +use crate::filesystem::epoll::{EPollEventType, EPollItem}; +use crate::filesystem::fsnotify::{ + self, mark, FsEvent, FsNotifyBackend, FsNotifyGroup, FsNotifyMark, +}; +use crate::filesystem::vfs::fcntl::AtFlags; +use crate::filesystem::vfs::file::{File, FileFlags, FileMode, FilePrivateData}; +use crate::filesystem::vfs::permission::{check_inode_permission, PermissionMask}; +use crate::filesystem::vfs::utils::user_path_at; +use crate::filesystem::vfs::{ + FileSystem, FileType, FsInfo, IndexNode, InodeMode, Magic, Metadata, PollableInode, SuperBlock, + NAME_MAX, VFS_MAX_FOLLOW_SYMLINK_TIMES, +}; +use crate::libs::mutex::{Mutex, MutexGuard}; +use crate::libs::spinlock::SpinLock; +use crate::mm::MemoryManagementArch; +use crate::process::ProcessManager; +use crate::syscall::table::{FormattedSyscallParam, Syscall}; +use crate::syscall::user_access::vfs_check_and_clone_cstr; + +// ============================================================================ +// 用户态 mask 位(与 Linux `include/uapi/linux/inotify.h` 完全一致) +// ============================================================================ + +/// `inotify_event.mask` 中的事件位与控制位。事件位与 [`FsEvent`] 低 16 位一致。 +#[allow(dead_code)] +mod user_mask { + pub const IN_ACCESS: u32 = 0x00000001; + pub const IN_MODIFY: u32 = 0x00000002; + pub const IN_ATTRIB: u32 = 0x00000004; + pub const IN_CLOSE_WRITE: u32 = 0x00000008; + pub const IN_CLOSE_NOWRITE: u32 = 0x00000010; + pub const IN_OPEN: u32 = 0x00000020; + pub const IN_MOVED_FROM: u32 = 0x00000040; + pub const IN_MOVED_TO: u32 = 0x00000080; + pub const IN_CREATE: u32 = 0x00000100; + pub const IN_DELETE: u32 = 0x00000200; + pub const IN_DELETE_SELF: u32 = 0x00000400; + pub const IN_MOVE_SELF: u32 = 0x00000800; + pub const IN_UNMOUNT: u32 = 0x00002000; + pub const IN_Q_OVERFLOW: u32 = 0x00004000; + pub const IN_IGNORED: u32 = 0x00008000; + pub const IN_ONLYDIR: u32 = 0x01000000; + pub const IN_DONT_FOLLOW: u32 = 0x02000000; + pub const IN_EXCL_UNLINK: u32 = 0x04000000; + pub const IN_MASK_CREATE: u32 = 0x10000000; + pub const IN_MASK_ADD: u32 = 0x20000000; + pub const IN_ISDIR: u32 = 0x40000000; + pub const IN_ONESHOT: u32 = 0x80000000; + + /// add_watch 传入的合法控制位集合(用于 `from_bits` 校验)。 + pub const WATCH_CONTROL: u32 = + IN_ONLYDIR | IN_DONT_FOLLOW | IN_EXCL_UNLINK | IN_MASK_CREATE | IN_MASK_ADD | IN_ONESHOT; + + /// 仅 add-time 控制位(更新/订阅时应从 mask 剥离)。 + pub const ADD_TIME_ONLY: u32 = IN_ONLYDIR | IN_DONT_FOLLOW | IN_MASK_CREATE | IN_MASK_ADD; +} + +bitflags::bitflags! { + /// `inotify_init1` 的 flags(与 O_CLOEXEC/O_NONBLOCK 取值一致)。 + pub struct InotifyInitFlags: u32 { + const IN_CLOEXEC = FileFlags::O_CLOEXEC.bits(); + const IN_NONBLOCK = FileFlags::O_NONBLOCK.bits(); + } +} + +// ============================================================================ +// 资源限制(常量,先不接 procfs sysctl;见设计文档 §6.1) +// ============================================================================ + +const MAX_USER_INSTANCES: usize = 128; +const MAX_USER_WATCHES: usize = 8192; +const MAX_QUEUED_EVENTS: usize = 16384; + +/// 全局 inotify 实例计数(近似 per-user;DragonOS 当前单用户语义)。 +static TOTAL_INSTANCES: AtomicUsize = AtomicUsize::new(0); + +// ============================================================================ +// 后端数据结构 +// ============================================================================ + +/// 队列里的一个事件(已格式化为 inotify 语义,含 wd)。 +#[derive(Debug)] +struct InotifyEventInfo { + wd: i32, + /// 已转为用户态 `IN_*` mask(含 ISDIR)。 + mask: u32, + cookie: u32, + /// 子项名(目录 watch 的子事件才有)。 + name: Option>, +} + +/// 事件队列。受 `events` 锁保护。 +#[derive(Debug)] +struct InotifyQueue { + list: VecDeque, + /// 置位后已插入一个 `IN_Q_OVERFLOW(wd=-1)`;消费该事件时清零。 + overflowed: bool, +} + +/// watch descriptor 表。受 `wd` 锁保护。 +#[derive(Debug)] +struct WdTable { + /// 单调分配 wd(1..=i32::MAX-1,饱和见 §6.2;-1 被 Q_OVERFLOW 占用)。 + counter: i32, + map: BTreeMap>, +} + +/// inotify 后端共享状态:同时被 `InotifyBackend`(在 group 内)与 `InotifyInode`(read 入口)持有。 +/// +/// 事件锁与 wd 锁分离,使 read(消费)与 add_watch/rm_watch(wd 管理)互不阻塞。 +#[derive(Debug)] +pub struct InotifyState { + /// 事件锁:handle_event(生产) 与 read(消费) 竞争。irqsave:fsnotify 可在持 VFS 锁时调用。 + events: SpinLock, + max_queued_events: usize, + /// wd 锁:add_watch/rm_watch 竞争。 + wd: Mutex, +} + +impl InotifyState { + fn new() -> Self { + Self { + events: SpinLock::new(InotifyQueue { + list: VecDeque::new(), + overflowed: false, + }), + max_queued_events: MAX_QUEUED_EVENTS, + wd: Mutex::new(WdTable { + counter: 0, + map: BTreeMap::new(), + }), + } + } +} + +/// inotify 后端(实现 [`FsNotifyBackend`])。 +#[derive(Debug)] +struct InotifyBackend { + state: Arc, +} + +impl InotifyBackend { + /// 事件是否可合并(仅 ACCESS/MODIFY 且无 name,见 §6.3)。 + fn mergeable(mask: FsEvent) -> bool { + let core = mask - FsEvent::ISDIR; + core == FsEvent::ACCESS || core == FsEvent::MODIFY + } + + /// 入队一个事件(调用方已持 events 锁)。 + fn enqueue_locked(q: &mut InotifyQueue, max: usize, ev: InotifyEventInfo) { + if q.list.len() >= max { + // 队列满:丢弃当前事件,置 overflow 标志并插入单个 Q_OVERFLOW(仅一次)。 + if !q.overflowed { + q.overflowed = true; + q.list.push_back(InotifyEventInfo { + wd: -1, + mask: user_mask::IN_Q_OVERFLOW, + cookie: 0, + name: None, + }); + } + return; + } + q.list.push_back(ev); + } +} + +impl FsNotifyBackend for InotifyBackend { + fn handle_event( + &self, + group: &FsNotifyGroup, + mark: &FsNotifyMark, + mask: FsEvent, + name: Option<&str>, + cookie: u32, + ) { + // 用户订阅 mask(ISDIR 始终保留,由 dispatch 设置)。 + let subscribed = mark.mask.load(Ordering::Relaxed); + let user_mask = mask.bits() & (subscribed | FsEvent::ISDIR.bits()); + if user_mask == 0 { + return; + } + + // name 截断到 NAME_MAX。 + let name = name.map(|n| { + if n.len() > NAME_MAX { + &n[..NAME_MAX] + } else { + n + } + }); + + let ev = InotifyEventInfo { + wd: mark.wd, + mask: user_mask, + cookie, + name: name.map(Box::::from), + }; + + let wake = { + let mut q = self.state.events.lock_irqsave(); + // 合并:末尾事件 (wd, mask, cookie, name) 完全相同且可合并类 → 丢弃。 + if Self::mergeable(mask) && ev.name.is_none() { + if let Some(tail) = q.list.back() { + if tail.wd == ev.wd + && tail.mask == ev.mask + && tail.cookie == ev.cookie + && tail.name.is_none() + { + return; + } + } + } + Self::enqueue_locked(&mut q, self.state.max_queued_events, ev); + true + }; + + // 唤醒 read 等待者与 epoll。 + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + let _ = wake; + } + + fn free_mark(&self, mark: &FsNotifyMark) { + let mut t = self.state.wd.lock(); + t.map.remove(&mark.wd); + } + + fn notify_ignored(&self, group: &FsNotifyGroup, mark: &FsNotifyMark) { + // 投递 IN_IGNORED(watch 被撤销:rm_watch/oneshot/DELETE_SELF/UNMOUNT)。 + let wake = { + let mut q = self.state.events.lock_irqsave(); + Self::enqueue_locked( + &mut q, + self.state.max_queued_events, + InotifyEventInfo { + wd: mark.wd, + mask: user_mask::IN_IGNORED, + cookie: 0, + name: None, + }, + ); + true + }; + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + let _ = wake; + } + + fn queue_nonempty(&self) -> bool { + !self.state.events.lock_irqsave().list.is_empty() + } +} + +// ============================================================================ +// 伪文件系统 +// ============================================================================ + +lazy_static::lazy_static! { + static ref INOTIFY_FS: Arc = Arc::new(InotifyFs); +} + +/// inotify 伪文件系统(类比 `EventFdFs`,无真正挂载)。 +#[derive(Debug)] +pub struct InotifyFs; + +impl InotifyFs { + pub fn instance() -> Arc { + INOTIFY_FS.clone() + } +} + +impl FileSystem for InotifyFs { + fn page_cache_writeback_domain( + &self, + ) -> Option<&Arc> { + None + } + fn root_inode(&self) -> Arc { + // 不会被真正调用:inotify 不挂载。 + Arc::new(InotifyInode::new(false)) + } + + fn info(&self) -> FsInfo { + FsInfo { + blk_dev_id: 0, + max_name_len: 255, + } + } + + fn as_any_ref(&self) -> &dyn Any { + self + } + + fn name(&self) -> &str { + "inotify" + } + + fn super_block(&self) -> SuperBlock { + SuperBlock::new(Magic::INOTIFY_MAGIC, MMArch::PAGE_SIZE as u64, 255) + } +} + +// ============================================================================ +// 伪 inode +// ============================================================================ + +/// inotify fd 对应的伪 inode。read/poll/epoll 委托给 group 与后端状态。 +#[derive(Debug)] +pub struct InotifyInode { + group: Arc, + state: Arc, + /// `O_NONBLOCK`:read 空队列时立即返回 EAGAIN。 + /// 用 AtomicBool 以支持 fcntl(F_SETFL) 动态修改(由 File::set_flags 同步)。 + nonblock: AtomicBool, +} + +impl InotifyInode { + /// 创建一个新的 inotify 实例(inotify_init1 用)。 + pub fn new(nonblock: bool) -> Self { + let state = Arc::new(InotifyState::new()); + let backend = Box::new(InotifyBackend { + state: state.clone(), + }); + let group = FsNotifyGroup::new(backend); + Self { + group, + state, + nonblock: AtomicBool::new(nonblock), + } + } + + /// 同步 O_NONBLOCK 状态(由 File::set_flags 在 fcntl F_SETFL 时调用)。 + pub fn set_nonblocking(&self, nb: bool) { + self.nonblock.store(nb, Ordering::Relaxed); + } + + /// name 域长度(含末尾 NUL,向上对齐到 sizeof(inotify_event)=16 字节)。 + /// Linux `roundup(name_len+1, sizeof(struct inotify_event))`。 + /// ABI 硬约束:对齐到 8 会导致多事件缓冲区错位。 + fn name_field_len(name_len: usize) -> usize { + (name_len + 1 + 15) & !15 + } + + /// 计算一个事件序列化后的字节长度(固定头 16 + name 域)。 + fn record_len(name: Option<&str>) -> usize { + const HEADER: usize = 16; + match name { + None => HEADER, + Some(n) => HEADER + Self::name_field_len(n.len()), + } + } + + /// 序列化单个事件到 `out`(长度已由 `record_len` 保证足够)。返回写入字节数。 + fn serialize(ev: &InotifyEventInfo, out: &mut [u8]) -> usize { + let name = ev.name.as_deref(); + let name_field = match name { + None => 0usize, + Some(n) => Self::name_field_len(n.len()), + }; + out[0..4].copy_from_slice(&ev.wd.to_ne_bytes()); + out[4..8].copy_from_slice(&ev.mask.to_ne_bytes()); + out[8..12].copy_from_slice(&ev.cookie.to_ne_bytes()); + out[12..16].copy_from_slice(&(name_field as u32).to_ne_bytes()); + if let Some(n) = name { + let nb = n.as_bytes(); + out[16..16 + nb.len()].copy_from_slice(nb); + // NUL + 对齐填充。 + for b in &mut out[16 + nb.len()..16 + name_field] { + *b = 0; + } + } + 16 + name_field + } + + /// `inotify_add_watch`:在 `inode` 上建立(或更新)一个 watch。 + pub fn add_watch(&self, inode: Arc, mask: u32) -> Result { + // mask 校验:仅允许已知位。 + if (mask + & !(user_mask::WATCH_CONTROL + | 0x00000fff // IN_ALL_EVENTS 低 12 位 + | user_mask::IN_UNMOUNT + | user_mask::IN_Q_OVERFLOW + | user_mask::IN_IGNORED)) + != 0 + { + return Err(SystemError::EINVAL); + } + + // mask 必须含至少一个事件位(低 12 位 IN_ALL_EVENTS)。 + if (mask & 0x0000_0fff) == 0 { + return Err(SystemError::EINVAL); + } + // IN_MASK_ADD 与 IN_MASK_CREATE 互斥。 + if (mask & user_mask::IN_MASK_ADD) != 0 && (mask & user_mask::IN_MASK_CREATE) != 0 { + return Err(SystemError::EINVAL); + } + + let md = inode.metadata()?; + let target_identity = (md.inode_id, md.dev_id); + + // IN_ONLYDIR:目标必须是目录。 + if (mask & user_mask::IN_ONLYDIR) != 0 && md.file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); + } + + // 读权限检查(防止通过监听泄露文件名/元数据)。 + check_inode_permission(&inode, &md, PermissionMask::MAY_READ)?; + + // 订阅位:剥离 add-time 控制位,保留 ONESHOT/EXCL_UNLINK。 + let event_mask = mask & !user_mask::ADD_TIME_ONLY; + + // 查找同 inode 上已有 mark(同 group);若无,则在**同一把 marks 锁**内完成 + // 新建与插入,避免并发 add_watch 同一 inode 产生重复 mark(TOCTOU:查重与插入 + // 不在同一锁内时,两个线程可各自通过「无 existing」检查并各建一个 mark,导致 + // 重复事件且 rm_watch 无法彻底移除)。 + // 锁序 marks → wd → FSNOTIFY 为此处引入的嵌套;全代码库无反向获取 + // (destroy_mark/dispatch 均先释放 marks 再取 wd/FSNOTIFY),故无死锁。 + let mut marks = self.group.marks.lock(); + if let Some(existing) = marks.iter().find(|m| m.identity() == target_identity) { + if (mask & user_mask::IN_MASK_CREATE) != 0 { + return Err(SystemError::EEXIST); + } + if (mask & user_mask::IN_MASK_ADD) != 0 { + existing.mask.fetch_or(event_mask, Ordering::Relaxed); + // OR 语义:任一来源设置 oneshot 即生效(新 mask 或已有状态)。 + if (mask & user_mask::IN_ONESHOT) != 0 { + existing.oneshot.store(true, Ordering::Relaxed); + } + } else { + existing.mask.store(event_mask, Ordering::Relaxed); + existing + .oneshot + .store((mask & user_mask::IN_ONESHOT) != 0, Ordering::Relaxed); + } + return Ok(existing.wd); + } + + // watch 上限(唯一全局计数器:同时服务快速路径与上限检查)。 + fsnotify::try_reserve_watch(MAX_USER_WATCHES)?; + + // 分配 wd(饱和到 i32::MAX-1;-1 被 Q_OVERFLOW 占用)。 + let wd = { + let mut t = self.state.wd.lock(); + if t.counter >= i32::MAX - 1 { + fsnotify::adjust_total_watches(-1); + return Err(SystemError::ENOSPC); + } + t.counter += 1; + t.counter + }; + + let mark = Arc::new(FsNotifyMark { + wd, + group: Arc::downgrade(&self.group), + inode: inode.clone(), + mask: AtomicU32::new(event_mask), + oneshot: AtomicBool::new((mask & user_mask::IN_ONESHOT) != 0), + excl_unlink: (mask & user_mask::IN_EXCL_UNLINK) != 0, + }); + + // 持 marks 锁完成全部插入(wd 表 / group.marks / 全局索引)。 + self.state.wd.lock().map.insert(wd, Arc::downgrade(&mark)); + marks.push(mark.clone()); + drop(marks); + fsnotify::index_add(&mark); + // 注:watch 计数已由 try_reserve_watch 原子 +1(含上限检查),此处不可再次 +1, + // 否则每次 add 会 +2 而 destroy 仅 -1,导致 TOTAL_WATCHES 永不归零(fast-path 短路 + // 失效,read/write/close 热路径永久付出 fsnotify 锁开销)且上限提前触顶。 + + Ok(wd) + } + + /// `inotify_rm_watch`:按 wd 移除一个 watch。 + pub fn rm_watch(&self, wd: i32) -> Result<(), SystemError> { + let mark = { + let mut t = self.state.wd.lock(); + match t.map.remove(&wd) { + Some(w) => w.upgrade().ok_or(SystemError::EINVAL), + None => Err(SystemError::EINVAL), + } + }?; + // destroy_mark 完成 group.marks / 全局索引 / free_mark / 计数 收尾。 + mark::destroy_mark(&mark); + Ok(()) + } + + /// fd 关闭收尾:撤销该实例所有 watch,回退计数。 + fn shutdown(&self) { + let marks = { + let mut g = self.group.marks.lock(); + core::mem::take(&mut *g) + }; + let n = marks.len(); + for m in &marks { + fsnotify::index_remove(m); + self.state.wd.lock().map.remove(&m.wd); + } + if n > 0 { + fsnotify::adjust_total_watches(-(n as i32)); + } + TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); + // 唤醒任何阻塞在 read 的线程(队列不再增长)。 + self.group.wait_queue.wakeup_all(None); + } +} + +impl PollableInode for InotifyInode { + fn poll(&self, _private_data: &FilePrivateData) -> Result { + if self.group.backend.queue_nonempty() { + Ok((EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM).bits() as usize) + } else { + Ok(0) + } + } + + fn add_epitem( + &self, + epitem: Arc, + _private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + self.group.epitems.add(epitem); + Ok(()) + } + + fn remove_epitem( + &self, + epitem: &Arc, + _private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + self.group.epitems.remove(epitem) + } +} + +impl IndexNode for InotifyInode { + /// inotify fd 不可 seek:pread/pwrite/lseek → ESPIPE。 + fn is_stream(&self) -> bool { + true + } + + fn open( + &self, + _data: MutexGuard, + _flags: &FileFlags, + ) -> Result<(), SystemError> { + Ok(()) + } + + fn close(&self, _data: MutexGuard) -> Result<(), SystemError> { + // fd 关闭:撤销该实例所有 watch(不发 IN_IGNORED——fd 已无消费者), + // 回退实例/全局 watch 计数,唤醒任何阻塞的 reader。 + self.shutdown(); + Ok(()) + } + + /// read 语义见设计文档 §4.3。 + fn read_at( + &self, + _offset: usize, + len: usize, + buf: &mut [u8], + data: MutexGuard, + ) -> Result { + drop(data); + // 1. buffer 小于事件头 → EINVAL。 + if len < 16 { + return Err(SystemError::EINVAL); + } + + loop { + let mut written; + { + let mut q = self.state.events.lock_irqsave(); + if q.list.is_empty() { + // 空队列 + drop(q); + if self.nonblock.load(Ordering::Relaxed) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if ProcessManager::current_pcb().has_pending_signal_fast() { + return Err(SystemError::ERESTARTSYS); + } + wq_wait_event_interruptible!( + self.group.wait_queue, + self.group.backend.queue_nonempty(), + {} + )?; + continue; + } + + // 逐个从头部打包完整事件,放不下的留在队列下次读。 + written = 0; + while let Some(front) = q.list.front() { + let rl = Self::record_len(front.name.as_deref()); + if written + rl > len { + break; + } + let ev = q.list.pop_front().unwrap(); + if ev.mask == user_mask::IN_Q_OVERFLOW { + q.overflowed = false; + } + written += Self::serialize(&ev, &mut buf[written..written + rl]); + } + } + + if written == 0 { + // 首个事件即放不下(name 过大)。 + return Err(SystemError::EINVAL); + } + return Ok(written); + } + } + + fn write_at( + &self, + _offset: usize, + _len: usize, + _buf: &[u8], + _data: MutexGuard, + ) -> Result { + Err(SystemError::EBADF) + } + + fn metadata(&self) -> Result { + Ok(Metadata { + mode: InodeMode::from_bits_truncate(0o400), + file_type: FileType::File, + ..Default::default() + }) + } + + fn resize(&self, _len: usize) -> Result<(), SystemError> { + Ok(()) + } + + fn fs(&self) -> Arc { + InotifyFs::instance() + } + + fn as_any_ref(&self) -> &dyn Any { + self + } + + fn list(&self) -> Result, SystemError> { + Err(SystemError::EINVAL) + } + + fn as_pollable_inode(&self) -> Result<&dyn PollableInode, SystemError> { + Ok(self) + } + + fn absolute_path(&self) -> Result { + Ok(String::from("inotify")) + } +} + +// ============================================================================ +// init 实现 +// ============================================================================ + +/// `inotify_init1` 的内核实现。 +pub fn do_inotify_init1(flags: u32) -> Result { + let flags = InotifyInitFlags::from_bits(flags).ok_or(SystemError::EINVAL)?; + + // 实例上限。 + if TOTAL_INSTANCES.fetch_add(1, Ordering::Relaxed) >= MAX_USER_INSTANCES { + TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); + return Err(SystemError::EMFILE); + } + + let nonblock = flags.contains(InotifyInitFlags::IN_NONBLOCK); + let inode = Arc::new(InotifyInode::new(nonblock)); + let file_flags = FileFlags::O_RDONLY + | (if nonblock { + FileFlags::O_NONBLOCK + } else { + FileFlags::empty() + }); + let file = File::new(inode, file_flags).inspect_err(|_| { + // File::new 失败:file 未创建,不会 drop→shutdown,需手动回退实例计数。 + TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); + })?; + // 防递归:inotify fd 自身的 read/write 不应产生事件。 + file.set_mode_flags(FileMode::FMODE_NONOTIFY); + + let cloexec = flags.contains(InotifyInitFlags::IN_CLOEXEC); + let binding = ProcessManager::current_pcb().fd_table(); + let mut fd_table_guard = binding.write(); + // alloc_fd 失败时 file 被 drop → File::drop → close → shutdown → 回退实例计数, + // 故此处不再手动回退。 + fd_table_guard + .alloc_fd(file, None, cloexec) + .map(|fd| fd as usize) +} + +/// `inotify_init`(无参,等价 `inotify_init1(0)`)。 +pub fn do_inotify_init() -> Result { + do_inotify_init1(0) +} + +// ============================================================================ +// syscall handlers +// ============================================================================ + +pub struct SysInotifyInitHandle; +impl Syscall for SysInotifyInitHandle { + fn num_args(&self) -> usize { + 0 + } + fn handle(&self, _args: &[usize], _frame: &mut TrapFrame) -> Result { + do_inotify_init() + } + fn entry_format(&self, _args: &[usize]) -> Vec { + Vec::new() + } +} +#[cfg(target_arch = "x86_64")] +syscall_table_macros::declare_syscall!(SYS_INOTIFY_INIT, SysInotifyInitHandle); + +pub struct SysInotifyInit1Handle; +impl SysInotifyInit1Handle { + fn flags(args: &[usize]) -> u32 { + args[0] as u32 + } +} +impl Syscall for SysInotifyInit1Handle { + fn num_args(&self) -> usize { + 1 + } + fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { + do_inotify_init1(Self::flags(args)) + } + fn entry_format(&self, args: &[usize]) -> Vec { + vec![FormattedSyscallParam::new( + "flags", + format!("{:#x}", Self::flags(args)), + )] + } +} +syscall_table_macros::declare_syscall!(SYS_INOTIFY_INIT1, SysInotifyInit1Handle); + +pub struct SysInotifyAddWatchHandle; +impl SysInotifyAddWatchHandle { + fn fd(args: &[usize]) -> i32 { + args[0] as i32 + } + fn pathname(args: &[usize]) -> *const u8 { + args[1] as *const u8 + } + fn mask(args: &[usize]) -> u32 { + args[2] as u32 + } +} +impl Syscall for SysInotifyAddWatchHandle { + fn num_args(&self) -> usize { + 3 + } + fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { + let fd = Self::fd(args); + let path_ptr = Self::pathname(args); + let mask = Self::mask(args); + + let path = vfs_check_and_clone_cstr(path_ptr, Some(crate::filesystem::vfs::MAX_PATHLEN))? + .into_string() + .map_err(|_| SystemError::EINVAL)?; + + // 取 inotify fd 对应的 InotifyInode(file Arc 保活,ino 借用其 inode)。 + let file: Arc = { + let binding = ProcessManager::current_pcb().fd_table(); + let fd_table_guard = binding.read(); + fd_table_guard + .get_file_by_fd(fd) + .ok_or(SystemError::EBADF)? + }; + let inode = file.inode(); + let inotify_inode = inode + .as_any_ref() + .downcast_ref::() + .ok_or(SystemError::EINVAL)?; + // 解析路径(IN_DONT_FOLLOW:不跟随末尾 symlink)。 + let pcb = ProcessManager::current_pcb(); + let (inode_begin, remain_path) = user_path_at(&pcb, AtFlags::AT_FDCWD.bits(), &path)?; + let target = if (mask & user_mask::IN_DONT_FOLLOW) != 0 { + inode_begin.lookup_follow_symlink2(&remain_path, VFS_MAX_FOLLOW_SYMLINK_TIMES, false)? + } else { + inode_begin.lookup_follow_symlink(&remain_path, VFS_MAX_FOLLOW_SYMLINK_TIMES)? + }; + + inotify_inode.add_watch(target, mask).map(|wd| wd as usize) + } + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("fd", Self::fd(args).to_string()), + FormattedSyscallParam::new("pathname", format!("{:#x}", Self::pathname(args) as usize)), + FormattedSyscallParam::new("mask", format!("{:#x}", Self::mask(args))), + ] + } +} +syscall_table_macros::declare_syscall!(SYS_INOTIFY_ADD_WATCH, SysInotifyAddWatchHandle); + +pub struct SysInotifyRmWatchHandle; +impl SysInotifyRmWatchHandle { + fn fd(args: &[usize]) -> i32 { + args[0] as i32 + } + fn wd(args: &[usize]) -> i32 { + args[1] as i32 + } +} +impl Syscall for SysInotifyRmWatchHandle { + fn num_args(&self) -> usize { + 2 + } + fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { + let fd = Self::fd(args); + let wd = Self::wd(args); + let file: Arc = { + let binding = ProcessManager::current_pcb().fd_table(); + let fd_table_guard = binding.read(); + fd_table_guard + .get_file_by_fd(fd) + .ok_or(SystemError::EBADF)? + }; + let inode = file.inode(); + let inotify_inode = inode + .as_any_ref() + .downcast_ref::() + .ok_or(SystemError::EINVAL)?; + inotify_inode.rm_watch(wd).map(|_| 0) + } + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("fd", Self::fd(args).to_string()), + FormattedSyscallParam::new("wd", Self::wd(args).to_string()), + ] + } +} +syscall_table_macros::declare_syscall!(SYS_INOTIFY_RM_WATCH, SysInotifyRmWatchHandle); diff --git a/kernel/src/filesystem/mod.rs b/kernel/src/filesystem/mod.rs index 5ce9d7e02a..75c4136358 100644 --- a/kernel/src/filesystem/mod.rs +++ b/kernel/src/filesystem/mod.rs @@ -7,7 +7,9 @@ pub mod eventfd; pub mod ext4; pub mod fat; pub mod fs; +pub mod fsnotify; pub mod fuse; +pub mod inotify; pub mod kernfs; pub mod mbr; pub mod mqueue; diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 7f15de2a4a..79a106da2c 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -3,7 +3,13 @@ use core::{ sync::atomic::{AtomicUsize, Ordering}, }; -use alloc::{string::String, sync::Arc, vec::Vec}; +use crate::filesystem::fsnotify::{self, FsEvent}; +use alloc::{ + boxed::Box, + string::String, + sync::{Arc, Weak}, + vec::Vec, +}; use log::error; use system_error::SystemError; @@ -676,6 +682,10 @@ pub struct File { /// via `eventpoll_release(file)`. DragonOS keeps the same lifetime edge /// here so fd numbers can be safely reused after close. epitems: Arc, + /// fsnotify 内容事件用:open 时解析的父目录(Weak)+子项名。仅当系统存在 watch 时填充, + /// 供 do_write/do_read/Drop/IN_OPEN 向父目录 watch 投递子项内容事件(issue B 修复)。 + /// 局限:open 后 rename 会使快照 stale(dentry.parent 原地变更);EXCL_UNLINK 覆盖 unlink。 + inotify_parent: Option<(Weak, Box)>, /// One semantic inode pin per open file description. Duplicated file /// descriptors and VMAs share this `File`, while `O_PATH` still owns it. /// Declared last so the explicit `Drop` body runs `close()` before release. @@ -1067,6 +1077,19 @@ impl File { } } + // fsnotify:写成功后投递 IN_MODIFY(FMODE_NONOTIFY 短路,防 inotify fd 递归)。 + if written_len > 0 + && !self.mode.read().contains(FileMode::FMODE_NONOTIFY) + && fsnotify::has_any_watch() + { + // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 + match self.inotify_parent_for_fsnotify() { + Some((parent, name)) => { + fsnotify::fsnotify(FsEvent::MODIFY, Some((&parent, name)), Some(&self.inode), 0) + } + None => fsnotify::fsnotify(FsEvent::MODIFY, None, Some(&self.inode), 0), + } + } Ok(written_len) } /// @brief 创建一个新的文件对象 @@ -1256,6 +1279,19 @@ impl File { .downcast_arc::() .map(|mnt_inode| mnt_inode.mount_fs() as Arc); + // fsnotify:解析父目录+子项名快照存入 File,供内容事件 hook 向父目录 watch + // 投递子项 IN_MODIFY/ACCESS/OPEN/CLOSE(issue B)。 + // 始终解析(不门控 has_any_watch):若 open 时无 watch 则设为 None,之后加 watch + // 也不生效——watch 永远收不到已 open 文件的内容事件。解析本身轻量(一次 downcast + // + do_parent/dname),非 MountFSInode 直接 None。 + // 设备/套接字等非 MountFSInode 解析为 None。 + // 注意:MountFSInode::as_any_ref() 返回内部 inode 的 Any,因此 + // downcast_ref::() 永远失败。必须用 downcast_arc。 + let inotify_parent = inode + .clone() + .downcast_arc::() + .and_then(|mnt| mnt.fsnotify_parent_and_name()) + .map(|(parent, name)| (Arc::downgrade(&parent), name.into_boxed_str())); let f = File { open_file_id: alloc_open_file_id(), inode, @@ -1276,6 +1312,7 @@ impl File { wb_error_seq: Mutex::new(wb_error_seq), sb_error_seq: Mutex::new(sb_error_seq), epitems: Arc::new(EPollItemList::default()), + inotify_parent, _inode_retention: inode_retention, _mount_guard: mount_guard, }; @@ -1283,6 +1320,14 @@ impl File { return Ok(f); } + /// 解析 fsnotify 内容事件的 parent 参数:upgrade 构造期存储的 Weak 父目录。 + /// 返回 (owned Arc 父目录, 借用的子项名);调用方须绑定返回值以延长大 Arc 生命周期。 + /// 仅对 MountFSInode 上的普通文件返回 Some。 + fn inotify_parent_for_fsnotify(&self) -> Option<(Arc, &str)> { + let (parent, name) = self.inotify_parent.as_ref()?; + Some((parent.upgrade()?, name.as_ref())) + } + /// Create a file object for sockets created by socket syscalls. /// /// These should not be subject to open(2) pathname semantics. @@ -1561,6 +1606,20 @@ impl File { if len > 0 || self.file_type == FileType::File { self.touch_atime_after_access(); } + // fsnotify:仅在实际读到数据(len > 0)时投递 IN_ACCESS(FMODE_NONOTIFY 短路)。 + // EOF 读(len==0)不投递——与 atime 语义独立。 + if len > 0 + && !self.mode.read().contains(FileMode::FMODE_NONOTIFY) + && fsnotify::has_any_watch() + { + // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 + match self.inotify_parent_for_fsnotify() { + Some((parent, name)) => { + fsnotify::fsnotify(FsEvent::ACCESS, Some((&parent, name)), Some(&self.inode), 0) + } + None => fsnotify::fsnotify(FsEvent::ACCESS, None, Some(&self.inode), 0), + } + } Ok(len) } @@ -2162,6 +2221,7 @@ impl File { wb_error_seq: Mutex::new(*self.wb_error_seq.lock()), sb_error_seq: Mutex::new(*self.sb_error_seq.lock()), epitems: Arc::new(EPollItemList::default()), + inotify_parent: self.inotify_parent.clone(), _inode_retention: inode_retention, _mount_guard: mount_guard, }; @@ -2262,6 +2322,18 @@ impl File { self.private_data.lock().update_flags(new_flags)?; // 更新文件的打开模式 *self.flags.write() = new_flags; + + // 将 O_NONBLOCK 变更同步到 inotify fd(其 read_at 查内部 AtomicBool 而非 FileFlags, + // 与 socket 的 set_nonblocking 同理)。 + if new_flags.contains(FileFlags::O_NONBLOCK) != old_flags.contains(FileFlags::O_NONBLOCK) { + if let Some(ino) = self + .inode + .as_any_ref() + .downcast_ref::() + { + ino.set_nonblocking(new_flags.contains(FileFlags::O_NONBLOCK)); + } + } return Ok(()); } @@ -2446,6 +2518,22 @@ impl Drop for File { EventPoll::release_file_epitem(&epitem); let _ = self.remove_epitem(&epitem); } + // fsnotify:最后一次 close → IN_CLOSE_WRITE / IN_CLOSE_NOWRITE(FMODE_NONOTIFY 短路)。 + // 此时 self.inode 仍存活(Drop 在 inode.close() 之前),可安全取 inode_id。 + if !self.mode.read().contains(FileMode::FMODE_NONOTIFY) && fsnotify::has_any_watch() { + let m = if self.mode.read().contains(FileMode::FMODE_WRITER) { + FsEvent::CLOSE_WRITE + } else { + FsEvent::CLOSE_NOWRITE + }; + // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 + match self.inotify_parent_for_fsnotify() { + Some((parent, name)) => { + fsnotify::fsnotify(m, Some((&parent, name)), Some(&self.inode), 0) + } + None => fsnotify::fsnotify(m, None, Some(&self.inode), 0), + } + } if self.flags().contains(FileFlags::FASYNC) { if let Ok(pollable) = self.inode.as_pollable_inode() { diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 46c976369d..386c949b69 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -2029,6 +2029,7 @@ bitflags! { const MOUNT_MAGIC = 61267; const PIPEFS_MAGIC = 0x50495045; const EVENTFD_MAGIC = 0x45564446; // "EVDF" in ASCII + const INOTIFY_MAGIC = 0x494E4F54; // "INOT" in ASCII const PIDFD_MAGIC = 0x50494446; // "PIDF" in ASCII // Linux UAPI: SOCKFS_MAGIC. const SOCKFS_MAGIC = 0x534f434b; diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index a8fce5a715..294815eefa 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -3546,6 +3546,25 @@ impl MountFSInode { } } + /// 供 fsnotify 内容事件 hook 解析「该 inode 的父目录 + 自身 dentry 名」。 + /// + /// 在 File 构造期(open)调用一次,结果快照存入 File,供 do_write/do_read/Drop/ + /// IN_OPEN 向父目录 watch 投递子项内容事件(IN_MODIFY/ACCESS/OPEN/CLOSE/ATTRIB)。 + /// - 根或断连 dentry(`do_parent` 返回自身)→ None,避免把内容事件误投给自身 watch; + /// - 非 MountFSInode(设备/套接字等)由调用方 downcast 判定,本方法不涉及。 + pub fn fsnotify_parent_and_name(&self) -> Option<(Arc, alloc::string::String)> { + let parent = self.do_parent().ok()?; + // do_parent 对根/无父 dentry 返回自身:必须排除,否则内容事件以 parent=自身 投递, + // fsnotify 用自身 inode_id 反查会把 MODIFY 当作「父目录的子项事件」误投给自身 watch。 + if let Some(self_arc) = self.self_ref.upgrade() { + if Arc::ptr_eq(&parent, &self_arc) { + return None; + } + } + let name = self.dname().ok()?; + Some((parent as Arc, name.0.as_str().to_string())) + } + fn do_absolute_path(&self) -> Result { self.do_absolute_path_impl(false) } diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index acb8272a87..ef25a07b90 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -1,10 +1,12 @@ +use crate::filesystem::fsnotify::{self, FsEvent}; +use crate::libs::casting::DowncastArc; use alloc::sync::Arc; use system_error::SystemError; use super::{ fcntl::AtFlags, file::{File, FileFlags, PreopenedFile}, - mount::MountFlags, + mount::{MountFSInode, MountFlags}, permission::PermissionMask, syscall::{OpenHow, OpenHowResolve}, utils::{ @@ -133,6 +135,8 @@ pub fn do_fchmod(inode: Arc, mode: InodeMode) -> Result, uid: usize, gid: usize) -> Result Result return Err(err), }; + // fsnotify:创建成功 → 父目录得 IN_CREATE(子项是普通文件,IN_ISDIR 不置位)。 + fsnotify::fsnotify( + FsEvent::CREATE, + Some((&parent_inode, &filename)), + Some(&inode), + 0, + ); created = true; let created_path = ResolvedPath::new(inode)?; drop(parent_resolved); @@ -470,6 +483,7 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result File::new_with_mount_guard(inode, how.o_flags, mount_guard, operation_guard)?, }; let cloexec = how.o_flags.contains(FileFlags::O_CLOEXEC); + let opened_inode = file.inode(); // 把文件对象存入pcb let r = ProcessManager::current_pcb() @@ -477,6 +491,27 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result() + .and_then(|mnt| mnt.fsnotify_parent_and_name()); + match parent { + Some((p, n)) => fsnotify::fsnotify( + FsEvent::OPEN, + Some((&p, n.as_str())), + Some(&opened_inode), + 0, + ), + None => fsnotify::fsnotify(FsEvent::OPEN, None, Some(&opened_inode), 0), + } + } else { + fsnotify::fsnotify(FsEvent::OPEN, None, Some(&opened_inode), 0); + } + } return r; } @@ -661,6 +696,8 @@ pub fn do_utimensat( | SetMetadataMask::TIMES_BY_WRITE, )?; } + // fsnotify:时间戳变更 → IN_ATTRIB。 + fsnotify::fsnotify(FsEvent::ATTRIB, None, Some(&inode), 0); return Ok(0); } diff --git a/kernel/src/filesystem/vfs/syscall/link_utils.rs b/kernel/src/filesystem/vfs/syscall/link_utils.rs index c5f79882e6..efdea3eedc 100644 --- a/kernel/src/filesystem/vfs/syscall/link_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/link_utils.rs @@ -1,3 +1,4 @@ +use crate::filesystem::fsnotify::{self, FsEvent}; use crate::filesystem::vfs::mount::MountFS; use crate::filesystem::vfs::permission::check_inode_permission; use crate::filesystem::vfs::permission::PermissionMask; @@ -133,7 +134,17 @@ pub fn do_linkat( return Err(SystemError::EPERM); } - return new_parent.link(new_name, &old_inode).map(|_| 0); + let r = new_parent.link(new_name, &old_inode); + if r.is_ok() { + // fsnotify:父目录得 IN_CREATE(硬链接目标非目录,IN_ISDIR 不置位)。 + fsnotify::fsnotify( + FsEvent::CREATE, + Some((&new_parent, new_name)), + Some(&old_inode), + 0, + ); + } + r.map(|_| 0) } /// 检查是否允许创建硬链接(对应Linux的may_linkat) diff --git a/kernel/src/filesystem/vfs/syscall/rename_utils.rs b/kernel/src/filesystem/vfs/syscall/rename_utils.rs index 4265cc62a9..fb1e0f7098 100644 --- a/kernel/src/filesystem/vfs/syscall/rename_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/rename_utils.rs @@ -1,3 +1,4 @@ +use crate::filesystem::fsnotify::{self, FsEvent}; use crate::filesystem::vfs::permission::PermissionMask; use crate::filesystem::vfs::syscall::RenameFlags; use crate::filesystem::vfs::utils::is_ancestor; @@ -8,6 +9,7 @@ use crate::filesystem::vfs::VFS_MAX_FOLLOW_SYMLINK_TIMES; use crate::filesystem::vfs::{MAX_PATHLEN, NAME_MAX}; use crate::process::ProcessManager; use crate::syscall::user_access::vfs_check_and_clone_cstr; +use alloc::sync::Arc; /// # 修改文件名 /// /// @@ -93,10 +95,15 @@ pub fn do_renameat2( } } - if flags.contains(RenameFlags::EXCHANGE) { - let new_inode = new_parent_inode.lookup(new_filename)?; + // RENAME_EXCHANGE 目标必须存在;预先 lookup 供事件投递复用(move_to 后原位置查不到)。 + let exchange_new_inode = if flags.contains(RenameFlags::EXCHANGE) { + Some(new_parent_inode.lookup(new_filename)?) + } else { + None + }; + if let Some(new_inode) = &exchange_new_inode { if new_inode.metadata()?.file_type == crate::filesystem::vfs::FileType::Dir - && is_ancestor(&new_inode, &old_parent_inode) + && is_ancestor(new_inode, &old_parent_inode) { return Err(SystemError::EINVAL); } @@ -121,6 +128,101 @@ pub fn do_renameat2( PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC, )?; + // 非 EXCHANGE:预先取出可能被覆盖的目标 inode(move_to 会静默销毁它), + // 否则其上的 watch 会沦为持续产生事件的「幽灵 watch」。 + let displaced = if !flags.contains(RenameFlags::EXCHANGE) { + new_parent_inode.find(new_filename).ok() + } else { + None + }; + + // 缓存 displaced 的 nlinks(move_to 前,displaced 还活着)。 + // move_to 会静默销毁 displaced 的目录项,之后的 metadata 可能失败或返回过时值。 + // nlinks <= 1 表示这是最后一个链接,覆盖后 inode 被销毁。 + let displaced_nlinks = displaced + .as_ref() + .and_then(|d| d.metadata().ok()) + .map(|m| m.nlinks); + old_parent_inode.move_to(old_filename, &new_parent_inode, new_filename, flags)?; + + if flags.contains(RenameFlags::EXCHANGE) { + // EXCHANGE:两个 inode 互换位置 → 两组配对事件、两个 cookie、双方各 IN_MOVE_SELF。 + // - old_inode: old_dir/old_name → new_dir/new_name(cookie1) + // - new_inode: new_dir/new_name → old_dir/old_name(cookie2) + let new_inode = exchange_new_inode + .as_ref() + .expect("RENAME_EXCHANGE requires target to exist (checked above)"); + let cookie1 = fsnotify::next_cookie(); + fsnotify::fsnotify( + FsEvent::MOVED_FROM | FsEvent::MOVE_SELF, + Some((&old_parent_inode, old_filename)), + Some(&old_inode), + cookie1, + ); + fsnotify::fsnotify( + FsEvent::MOVED_TO, + Some((&new_parent_inode, new_filename)), + Some(&old_inode), + cookie1, + ); + let cookie2 = fsnotify::next_cookie(); + fsnotify::fsnotify( + FsEvent::MOVED_FROM | FsEvent::MOVE_SELF, + Some((&new_parent_inode, new_filename)), + Some(new_inode), + cookie2, + ); + fsnotify::fsnotify( + FsEvent::MOVED_TO, + Some((&old_parent_inode, old_filename)), + Some(new_inode), + cookie2, + ); + } else { + // 普通 rename(可能覆盖目标):单 cookie 配对 MOVED_FROM/MOVED_TO + MOVE_SELF。 + // No-op rename:同父目录 + 同文件名 → 无实际变更,跳过事件投递。 + if Arc::ptr_eq(&old_parent_inode, &new_parent_inode) && old_filename == new_filename { + return Ok(0); + } + let cookie = fsnotify::next_cookie(); + fsnotify::fsnotify( + FsEvent::MOVED_FROM | FsEvent::MOVE_SELF, + Some((&old_parent_inode, old_filename)), + Some(&old_inode), + cookie, + ); + fsnotify::fsnotify( + FsEvent::MOVED_TO, + Some((&new_parent_inode, new_filename)), + Some(&old_inode), + cookie, + ); + // 覆盖了已存在目标:补投 IN_DELETE(目标父目录)。 + // 仅当被覆盖的是不同 inode 且 nlinks 归零(最后一个硬链接)时才发 + // IN_DELETE_SELF(随之撤销 mark 并投递 IN_IGNORED)。 + // - 若 old_inode 与 displaced 是同一 inode(rename 覆盖自身别名),inode 未销毁; + // - 若 displaced 有多个硬链接(nlinks > 1),覆盖一个链接后 inode 仍存活。 + if let Some(displaced) = &displaced { + // 容错:metadata 读失败(如 FUSE)不影响 rename 的成功返回值; + // 失败时按「不同 inode + nlinks 未知」处理(保守发 DELETE_SELF)。 + let same_inode = displaced + .metadata() + .ok() + .zip(old_inode.metadata().ok()) + .map(|(d, o)| d.inode_id == o.inode_id) + .unwrap_or(false); + let mut mask = FsEvent::DELETE; + if !same_inode && displaced_nlinks.is_none_or(|n| n <= 1) { + mask |= FsEvent::DELETE_SELF; + } + fsnotify::fsnotify( + mask, + Some((&new_parent_inode, new_filename)), + Some(displaced), + 0, + ); + } + } return Ok(0); } diff --git a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs index 07984ab572..b27fb72b1f 100644 --- a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs @@ -1,5 +1,6 @@ use system_error::SystemError; +use crate::filesystem::fsnotify::{self, FsEvent}; use crate::{ filesystem::vfs::{ fcntl::AtFlags, @@ -66,6 +67,8 @@ pub fn do_symlinkat(from: &str, newdfd: Option, to: &str) -> Result Result { // 删除文件夹 parent_inode.rmdir(filename)?; + // fsnotify:父目录得 IN_DELETE(子项是目录→IN_ISDIR),子项得 IN_DELETE_SELF+IN_IGNORED。 + fsnotify::fsnotify( + FsEvent::DELETE | FsEvent::DELETE_SELF, + Some((&parent_inode, filename)), + Some(&target_inode), + 0, + ); return Ok(0); } @@ -709,8 +726,25 @@ pub fn do_unlink_at(dirfd: i32, path: &str) -> Result { return Err(SystemError::EISDIR); } + // 缓存 unlink 前的 nlinks——unlink 后 FUSE daemon 的 GETATTR 可能失败或返回过时值。 + // 用 unlink 前的缓存值判断是否为最后一个链接:nlinks_before <= 1 则 inode 被销毁。 + let nlinks_before = target_inode.metadata().map(|m| m.nlinks).unwrap_or(1); + // 在父目录上执行 unlink 操作 parent_inode.unlink(filename)?; + // Linux 语义:unlink 只删除目录项本身;只有当该 inode 的硬链接数(nlinks)归零时 + // inode 才真正被销毁,此时才发 IN_DELETE_SELF(随后 mark 被撤销并投递 IN_IGNORED)。 + // 存在硬链接时 inode 仍然存活,不应误撤销其上的 watch。 + let mut mask = FsEvent::DELETE; + if nlinks_before <= 1 { + mask |= FsEvent::DELETE_SELF; + } + fsnotify::fsnotify( + mask, + Some((&parent_inode, filename)), + Some(&target_inode), + 0, + ); return Ok(0); } @@ -780,7 +814,12 @@ where } let (md, mask) = prepare_write_side_effect_metadata(md, len); - do_resize(&inode, &md, mask) + let r = do_resize(&inode, &md, mask); + if r.is_ok() { + // fsnotify:截断改变数据与元数据 → IN_MODIFY + IN_ATTRIB。 + fsnotify::fsnotify(FsEvent::MODIFY | FsEvent::ATTRIB, None, Some(&inode), 0); + } + r } pub(crate) fn prepare_write_side_effect_metadata( @@ -918,6 +957,12 @@ pub fn vfs_fallocate_file( if len == 0 || offset > isize::MAX as usize || len > isize::MAX as usize { return Err(SystemError::EINVAL); } + // VFS 层 s_maxbytes 上限守卫(对齐 Linux do_fallocate):offset+len 不得溢出或超过 isize::MAX。 + // 具体文件系统实现各自再校验,但 VFS 层不应留缺口。 + let new_size = offset.checked_add(len).ok_or(SystemError::EFBIG)?; + if new_size > isize::MAX as usize { + return Err(SystemError::EFBIG); + } let mode_bits = mode as u32; if mode < 0 || (mode_bits & !FALLOC_FL_SUPPORTED_MASK) != 0 { @@ -980,16 +1025,16 @@ pub fn vfs_fallocate_file( _ => return Err(SystemError::ENODEV), } - let new_size = offset.checked_add(len).ok_or(SystemError::EFBIG)?; - if new_size > isize::MAX as usize { - return Err(SystemError::EFBIG); - } - - inode.fallocate_file( + let r = inode.fallocate_file( mode, offset, len, current_file_lock_owner_id(), file.private_data.lock(), - ) + ); + // fsnotify:写类 fallocate(非 KEEP_SIZE-only)改变数据 → IN_MODIFY。 + if r.is_ok() && (mode as u32) != FALLOC_FL_KEEP_SIZE { + fsnotify::fsnotify(FsEvent::MODIFY, None, Some(&inode), 0); + } + r } diff --git a/user/apps/tests/dunitest/suites/normal/inotify_dir_watch.cc b/user/apps/tests/dunitest/suites/normal/inotify_dir_watch.cc new file mode 100644 index 0000000000..97cf9229c3 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/inotify_dir_watch.cc @@ -0,0 +1,150 @@ +// inotify_dir_watch.cc - inotify directory-watch child content events test (dunitest/gtest) +// +// Regression coverage for the inotify directory-watch fix (issue B): +// watching a directory must deliver child *content* events +// (IN_MODIFY / IN_ACCESS / IN_OPEN / IN_CLOSE_WRITE / IN_CLOSE_NOWRITE) carrying the +// child name -- the dominant inotify use case (e.g. `inotifywait -m /dir`). +// Before the fix, directory watches only received namespace events (create/delete/move) +// and silently dropped all child content events. +// +// Runtime environment: DragonOS QEMU, /tmp is a writable tmpfs. +// Use GTEST_SKIP() if inotify syscalls are unavailable. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +struct Ev { + uint32_t mask; + std::string name; +}; + +// Drain all queued inotify events within a short time budget (nonblocking fd). +// Returns the parsed events (mask + child name). +std::vector drain_events(int ifd) { + std::vector out; + char buf[4096] __attribute__((aligned(8))); + for (int spins = 0; spins < 2000; spins++) { + ssize_t n = read(ifd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + usleep(2000); + continue; + } + break; + } + if (n == 0) { + usleep(2000); + continue; + } + for (char *p = buf; p + sizeof(struct inotify_event) <= buf + n;) { + struct inotify_event *e = reinterpret_cast(p); + out.push_back(Ev{e->mask, e->len ? std::string(e->name) : std::string()}); + p += sizeof(struct inotify_event) + e->len; + } + } + return out; +} + +bool saw(const std::vector &evs, uint32_t bit, const std::string &name) { + for (const auto &e : evs) { + if ((e.mask & bit) && e.name == name) return true; + } + return false; +} + +// Touch a regular file inside `dir` named `base`: create+write+close(write), +// then open(read)+read+close(read). +void exercise_child(const std::string &dir, const std::string &base) { + std::string path = dir + "/" + base; + int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0) << "open create: " << strerror(errno); + ASSERT_EQ(write(fd, "hello\n", 6), 6); + ASSERT_EQ(close(fd), 0); + int rfd = open(path.c_str(), O_RDONLY); + ASSERT_GE(rfd, 0) << "open read: " << strerror(errno); + char rb[16]; + ASSERT_GE(read(rfd, rb, sizeof(rb)), 0); + ASSERT_EQ(close(rfd), 0); +} + +} // namespace + +// Core regression: a directory watch receives child IN_MODIFY/IN_CREATE/IN_CLOSE_WRITE. +TEST(InotifyDirWatch, ChildContentEventsReachDirWatch) { + const std::string dir = "/tmp/dunitest_inotify_dir"; + const std::string child = "f"; + mkdir(dir.c_str(), 0777); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0) << "inotify_init1: " << strerror(errno); + + int wd = inotify_add_watch( + ifd, dir.c_str(), + IN_CREATE | IN_MODIFY | IN_ACCESS | IN_OPEN | IN_CLOSE_WRITE | IN_CLOSE_NOWRITE); + ASSERT_GE(wd, 0) << "inotify_add_watch: " << strerror(errno); + + exercise_child(dir, child); + auto evs = drain_events(ifd); + + // Diagnostic: print what we actually saw. + for (const auto &e : evs) { + printf(" saw mask=0x%x name=\"%s\"\n", e.mask, e.name.c_str()); + } + + EXPECT_TRUE(saw(evs, IN_CREATE, child)) + << "dir watch missed child IN_CREATE"; + EXPECT_TRUE(saw(evs, IN_MODIFY, child)) + << "dir watch missed child IN_MODIFY (issue B regression)"; + EXPECT_TRUE(saw(evs, IN_CLOSE_WRITE, child)) + << "dir watch missed child IN_CLOSE_WRITE"; + + inotify_rm_watch(ifd, wd); + close(ifd); + unlink((dir + "/" + child).c_str()); + rmdir(dir.c_str()); +} + +// Sanity (unchanged behavior): watching a file *itself* still receives IN_MODIFY. +TEST(InotifySelfWatch, ModifyReachesFileWatch) { + const std::string path = "/tmp/dunitest_inotify_self"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + int wd = inotify_add_watch(ifd, path.c_str(), IN_MODIFY); + ASSERT_GE(wd, 0); + + ASSERT_EQ(write(fd, "x", 1), 1); + auto evs = drain_events(ifd); + + bool got = false; + for (const auto &e : evs) { + if (e.mask & IN_MODIFY) got = true; + } + EXPECT_TRUE(got) << "self watch missed IN_MODIFY"; + + inotify_rm_watch(ifd, wd); + close(ifd); + close(fd); + unlink(path.c_str()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/suites/normal/inotify_events.cc b/user/apps/tests/dunitest/suites/normal/inotify_events.cc new file mode 100644 index 0000000000..02bf95d2b1 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/inotify_events.cc @@ -0,0 +1,334 @@ +// inotify_events.cc - comprehensive inotify event coverage (dunitest/gtest) +// +// Covers the event types NOT exercised by inotify_dir_watch.cc: +// - Namespace events: IN_DELETE, IN_MOVED_FROM, IN_MOVED_TO, IN_ISDIR +// - Self events: IN_DELETE_SELF (+ IN_IGNORED), IN_MOVE_SELF +// - IN_ATTRIB (chmod metadata change) +// - Multi-instance: two independent inotify fds watching the same inode +// - poll() readiness: inotify fd reports POLLIN after an event +// +// Runtime environment: DragonOS QEMU, /tmp is a writable tmpfs. +// Use GTEST_SKIP() if inotify syscalls are unavailable. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +struct Ev { + uint32_t mask; + uint32_t cookie; + std::string name; +}; + +// Drain all queued inotify events within a short time budget (nonblocking fd). +std::vector drain_events(int ifd) { + std::vector out; + char buf[4096] __attribute__((aligned(8))); + for (int spins = 0; spins < 300; spins++) { + ssize_t n = read(ifd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + usleep(2000); + continue; + } + break; + } + if (n == 0) { + usleep(2000); + continue; + } + for (char *p = buf; p + sizeof(struct inotify_event) <= buf + n;) { + struct inotify_event *e = reinterpret_cast(p); + out.push_back(Ev{e->mask, e->cookie, e->len ? std::string(e->name) : std::string()}); + p += sizeof(struct inotify_event) + e->len; + } + } + return out; +} + +bool saw(const std::vector &evs, uint32_t bit, const std::string &name) { + for (const auto &e : evs) { + if ((e.mask & bit) && e.name == name) return true; + } + return false; +} + +// A self-event has an empty name (no child). +bool saw_self(const std::vector &evs, uint32_t bit) { + for (const auto &e : evs) { + if ((e.mask & bit) && e.name.empty()) return true; + } + return false; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Namespace events on a directory watch: create, delete, move (rename). +// Also verifies IN_ISDIR is set when the created child is a directory. +// --------------------------------------------------------------------------- +TEST(InotifyNamespaceEvents, CreateDeleteMoveOnDirWatch) { + const std::string dir = "/tmp/dunitest_inotify_ns"; + mkdir(dir.c_str(), 0777); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0) << "inotify_init1: " << strerror(errno); + + int wd = inotify_add_watch( + ifd, dir.c_str(), + IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); + // Note: IN_ISDIR is NOT a valid add_watch mask bit — it is a flag the + // kernel sets on returned events to indicate the subject is a directory. + // We verify it appears automatically on events for directory children. + + // 1. Create a subdirectory -> IN_CREATE with IN_ISDIR. + const std::string subdir = "subdir"; + std::string subpath = dir + "/" + subdir; + ASSERT_EQ(mkdir(subpath.c_str(), 0777), 0) << "mkdir: " << strerror(errno); + + // 2. Create a regular file -> IN_CREATE without IN_ISDIR. + const std::string file1 = "file1"; + std::string fpath = dir + "/" + file1; + int fd = open(fpath.c_str(), O_CREAT | O_WRONLY, 0644); + ASSERT_GE(fd, 0) << "open create: " << strerror(errno); + close(fd); + + // 3. Rename file1 -> file2 within the dir -> IN_MOVED_FROM(file1) + IN_MOVED_TO(file2). + const std::string file2 = "file2"; + std::string fpath2 = dir + "/" + file2; + ASSERT_EQ(rename(fpath.c_str(), fpath2.c_str()), 0) << "rename: " << strerror(errno); + + // 4. Delete file2 -> IN_DELETE(file2). + ASSERT_EQ(unlink(fpath2.c_str()), 0) << "unlink: " << strerror(errno); + + auto evs = drain_events(ifd); + + for (const auto &e : evs) { + printf(" saw mask=0x%x cookie=%u name=\"%s\"\n", e.mask, e.cookie, e.name.c_str()); + } + + // Create subdir: IN_CREATE | IN_ISDIR + EXPECT_TRUE(saw(evs, IN_CREATE, subdir)) << "missed IN_CREATE for subdir"; + bool saw_dir = false; + for (const auto &e : evs) { + if ((e.mask & IN_CREATE) && (e.mask & IN_ISDIR) && e.name == subdir) saw_dir = true; + } + EXPECT_TRUE(saw_dir) << "IN_CREATE for subdir should carry IN_ISDIR"; + + // Create regular file: IN_CREATE without IN_ISDIR + EXPECT_TRUE(saw(evs, IN_CREATE, file1)) << "missed IN_CREATE for file1"; + + // Move: IN_MOVED_FROM(file1) and IN_MOVED_TO(file2) with matching cookie + EXPECT_TRUE(saw(evs, IN_MOVED_FROM, file1)) << "missed IN_MOVED_FROM for file1"; + EXPECT_TRUE(saw(evs, IN_MOVED_TO, file2)) << "missed IN_MOVED_TO for file2"; + uint32_t cookie_from = 0, cookie_to = 0; + for (const auto &e : evs) { + if ((e.mask & IN_MOVED_FROM) && e.name == file1) cookie_from = e.cookie; + if ((e.mask & IN_MOVED_TO) && e.name == file2) cookie_to = e.cookie; + } + EXPECT_NE(cookie_from, 0) << "IN_MOVED_FROM cookie should be non-zero for intra-dir rename"; + EXPECT_EQ(cookie_from, cookie_to) << "IN_MOVED_FROM/TO cookies must match for same rename"; + + // Delete: IN_DELETE(file2) + EXPECT_TRUE(saw(evs, IN_DELETE, file2)) << "missed IN_DELETE for file2"; + + inotify_rm_watch(ifd, wd); + close(ifd); + rmdir(subpath.c_str()); + rmdir(dir.c_str()); +} + +// --------------------------------------------------------------------------- +// IN_ATTRIB: changing file metadata (chmod) delivers IN_ATTRIB. +// --------------------------------------------------------------------------- +TEST(InotifyAttribEvent, ChmodDeliversAttrib) { + const std::string path = "/tmp/dunitest_inotify_attrib"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + + int wd = inotify_add_watch(ifd, path.c_str(), IN_ATTRIB); + ASSERT_GE(wd, 0) << "inotify_add_watch: " << strerror(errno); + + ASSERT_EQ(chmod(path.c_str(), 0600), 0) << "chmod: " << strerror(errno); + + auto evs = drain_events(ifd); + + for (const auto &e : evs) { + printf(" saw mask=0x%x name=\"%s\"\n", e.mask, e.name.c_str()); + } + + EXPECT_TRUE(saw_self(evs, IN_ATTRIB)) << "self watch missed IN_ATTRIB after chmod"; + + inotify_rm_watch(ifd, wd); + close(ifd); + unlink(path.c_str()); +} + +// --------------------------------------------------------------------------- +// IN_DELETE_SELF + IN_IGNORED: unlinking a watched file delivers +// IN_DELETE_SELF, followed by IN_IGNORED (watch auto-revoked). +// --------------------------------------------------------------------------- +TEST(InotifySelfEvents, UnlinkWatchedFileDeliversDeleteSelfAndIgnored) { + const std::string path = "/tmp/dunitest_inotify_delself"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + + int wd = inotify_add_watch(ifd, path.c_str(), IN_DELETE_SELF); + ASSERT_GE(wd, 0) << "inotify_add_watch: " << strerror(errno); + + ASSERT_EQ(unlink(path.c_str()), 0) << "unlink: " << strerror(errno); + + auto evs = drain_events(ifd); + + for (const auto &e : evs) { + printf(" saw mask=0x%x name=\"%s\"\n", e.mask, e.name.c_str()); + } + + EXPECT_TRUE(saw_self(evs, IN_DELETE_SELF)) + << "watched file missed IN_DELETE_SELF after unlink"; + EXPECT_TRUE(saw_self(evs, IN_IGNORED)) + << "watched file missed IN_IGNORED after unlink"; + + // IN_IGNORED is always emitted — do NOT call inotify_rm_watch after auto-revoke. + close(ifd); +} + +// --------------------------------------------------------------------------- +// IN_MOVE_SELF: renaming a watched file delivers IN_MOVE_SELF. +// --------------------------------------------------------------------------- +TEST(InotifySelfEvents, RenameWatchedFileDeliversMoveSelf) { + const std::string path = "/tmp/dunitest_inotify_moveself"; + const std::string path2 = "/tmp/dunitest_inotify_moveself_renamed"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + + int wd = inotify_add_watch(ifd, path.c_str(), IN_MOVE_SELF); + ASSERT_GE(wd, 0) << "inotify_add_watch: " << strerror(errno); + + ASSERT_EQ(rename(path.c_str(), path2.c_str()), 0) << "rename: " << strerror(errno); + + auto evs = drain_events(ifd); + + for (const auto &e : evs) { + printf(" saw mask=0x%x name=\"%s\"\n", e.mask, e.name.c_str()); + } + + EXPECT_TRUE(saw_self(evs, IN_MOVE_SELF)) + << "watched file missed IN_MOVE_SELF after rename"; + + inotify_rm_watch(ifd, wd); + close(ifd); + unlink(path2.c_str()); +} + +// --------------------------------------------------------------------------- +// Multiple independent inotify instances watching the same directory both +// receive events — ensures event fan-out works across groups. +// --------------------------------------------------------------------------- +TEST(InotifyMultiInstance, TwoInstancesBothReceiveEvents) { + const std::string dir = "/tmp/dunitest_inotify_multi"; + mkdir(dir.c_str(), 0777); + + int ifd1 = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd1, 0); + int ifd2 = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd2, 0); + + int wd1 = inotify_add_watch(ifd1, dir.c_str(), IN_CREATE); + ASSERT_GE(wd1, 0); + int wd2 = inotify_add_watch(ifd2, dir.c_str(), IN_CREATE); + ASSERT_GE(wd2, 0); + + const std::string child = "shared"; + std::string cpath = dir + "/" + child; + int fd = open(cpath.c_str(), O_CREAT | O_WRONLY, 0644); + ASSERT_GE(fd, 0); + close(fd); + + auto evs1 = drain_events(ifd1); + auto evs2 = drain_events(ifd2); + + printf(" instance1: %zu events\n", evs1.size()); + printf(" instance2: %zu events\n", evs2.size()); + + EXPECT_TRUE(saw(evs1, IN_CREATE, child)) << "instance1 missed IN_CREATE"; + EXPECT_TRUE(saw(evs2, IN_CREATE, child)) << "instance2 missed IN_CREATE"; + + inotify_rm_watch(ifd1, wd1); + inotify_rm_watch(ifd2, wd2); + close(ifd1); + close(ifd2); + unlink(cpath.c_str()); + rmdir(dir.c_str()); +} + +// --------------------------------------------------------------------------- +// poll() readiness: an inotify fd becomes readable (POLLIN) after an event +// is queued — validates epoll/eventpoll integration. +// --------------------------------------------------------------------------- +TEST(InotifyPollReady, FdBecomesReadableAfterEvent) { + const std::string path = "/tmp/dunitest_inotify_poll"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + + int wd = inotify_add_watch(ifd, path.c_str(), IN_MODIFY | IN_ATTRIB); + ASSERT_GE(wd, 0); + + // Before any event: poll should timeout (no readiness). + struct pollfd pfd = {.fd = ifd, .events = POLLIN, .revents = 0}; + int pr = poll(&pfd, 1, 100); + EXPECT_EQ(pr, 0) << "poll should timeout before any event"; + EXPECT_EQ(pfd.revents, 0); + + // Trigger an event. + ASSERT_EQ(chmod(path.c_str(), 0600), 0); + + // After the event: poll should report POLLIN within a short window. + pfd.revents = 0; + pr = poll(&pfd, 1, 1000); + EXPECT_GT(pr, 0) << "poll should be ready after event"; + EXPECT_TRUE(pfd.revents & POLLIN) << "POLLIN should be set after event"; + + // Consume and verify the event. + auto evs = drain_events(ifd); + EXPECT_TRUE(saw_self(evs, IN_ATTRIB)) << "should see IN_ATTRIB after poll-ready"; + + inotify_rm_watch(ifd, wd); + close(ifd); + unlink(path.c_str()); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index 40cc38c2e5..a074d589b4 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -97,3 +97,5 @@ normal/af_packet_e2e normal/af_packet_mcast normal/rtnetlink_link_semantics normal/tty_termios +normal/inotify_dir_watch +normal/inotify_events From b8ef2adf4d9a73348d70f23a6929979c9f4f46ec Mon Sep 17 00:00:00 2001 From: longjin Date: Thu, 20 Aug 2026 03:15:45 +0000 Subject: [PATCH 2/3] fix(fsnotify): harden inotify lifecycle and semantics Linearize mark publication, dispatch, and removal around stable object identities so watch updates, one-shot delivery, inode deletion, and final unmount cannot race. Make queue overflow, read consumption, allocation rollback, user quotas, and dentry snapshots safe under concurrency and memory pressure. Align VFS event routing with Linux semantics for open, close, links, rename, attributes, exclusion of unlinked paths, and deletion ordering. Add dunitest coverage for namespace events, inode lifecycle, O_PATH, hard links, rename aliases, multi-instance delivery, and poll readiness. Validated with make kernel and 12 passing inotify tests on both host Linux and a DragonOS QEMU guest. Signed-off-by: longjin --- kernel/src/filesystem/fsnotify/group.rs | 8 +- kernel/src/filesystem/fsnotify/mark.rs | 43 +- kernel/src/filesystem/fsnotify/mod.rs | 426 ++++++++++++--- kernel/src/filesystem/fuse/inode.rs | 19 + kernel/src/filesystem/inotify.rs | 516 +++++++++++++----- kernel/src/filesystem/vfs/file.rs | 89 ++- kernel/src/filesystem/vfs/mount/mod.rs | 276 +++++++++- kernel/src/filesystem/vfs/open.rs | 422 +++++++------- .../src/filesystem/vfs/syscall/link_utils.rs | 1 + .../filesystem/vfs/syscall/rename_utils.rs | 68 ++- kernel/src/filesystem/vfs/vcore.rs | 31 +- .../dunitest/suites/normal/inotify_events.cc | 126 +++++ 12 files changed, 1452 insertions(+), 573 deletions(-) diff --git a/kernel/src/filesystem/fsnotify/group.rs b/kernel/src/filesystem/fsnotify/group.rs index 754ef6e84a..4314733751 100644 --- a/kernel/src/filesystem/fsnotify/group.rs +++ b/kernel/src/filesystem/fsnotify/group.rs @@ -2,14 +2,14 @@ use alloc::boxed::Box; use alloc::sync::Arc; -use alloc::vec::Vec; +use hashbrown::HashMap; use crate::filesystem::epoll::event_poll::EPollItemList; use crate::libs::mutex::Mutex; use crate::libs::wait_queue::WaitQueue; use super::mark::FsNotifyMark; -use super::FsNotifyBackend; +use super::{FsNotifyBackend, FsNotifyObjectId}; /// 一个通知消费者。一个 inotify fd 对应一个 group。 /// @@ -19,7 +19,7 @@ use super::FsNotifyBackend; #[derive(Debug)] pub struct FsNotifyGroup { pub backend: Box, - pub marks: Mutex>>, + pub marks: Mutex>>, pub wait_queue: WaitQueue, pub epitems: EPollItemList, } @@ -28,7 +28,7 @@ impl FsNotifyGroup { pub fn new(backend: Box) -> Arc { Arc::new(Self { backend, - marks: Mutex::new(Vec::new()), + marks: Mutex::new(HashMap::new()), wait_queue: WaitQueue::default(), epitems: EPollItemList::default(), }) diff --git a/kernel/src/filesystem/fsnotify/mark.rs b/kernel/src/filesystem/fsnotify/mark.rs index b278317275..5f1994e513 100644 --- a/kernel/src/filesystem/fsnotify/mark.rs +++ b/kernel/src/filesystem/fsnotify/mark.rs @@ -1,11 +1,14 @@ //! [`FsNotifyMark`]:一个 watch(group + inode + mask + wd)及其生命周期管理。 use alloc::sync::{Arc, Weak}; -use core::sync::atomic::{AtomicBool, AtomicU32}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use crate::filesystem::vfs::{IndexNode, InodeId}; +use crate::filesystem::vfs::IndexNode; -use super::{adjust_total_watches, index_remove, FsNotifyGroup}; +use super::{ + adjust_total_watches, index_remove, FsNotifyDeleteState, FsNotifyGroup, FsNotifyObjectId, +}; +use crate::libs::mutex::Mutex; /// 一个 watch:连接 group 与 inode。 /// @@ -18,13 +21,21 @@ pub struct FsNotifyMark { /// 所属 group(弱引用,避免环引用)。 pub group: Weak, /// 强引用:watch 期间 pin 住 inode(防 evict,保证 InodeId 不复用)。 - pub inode: Arc, + pub _inode: Arc, + /// Keeps per-object delete state alive without retaining a dentry. + pub(crate) _delete_lifecycle: Option>>, + /// Captured once at watch creation; removal never performs metadata I/O. + pub object_id: FsNotifyObjectId, + /// Serializes dispatch with update/removal. This closes the one-shot and + /// rm_watch race without a packed atomic state machine. + pub dispatch_lock: Mutex<()>, + pub active: AtomicBool, /// 订阅 mask(`IN_MASK_ADD` 并发改,必须原子读)。 pub mask: AtomicU32, /// `IN_ONESHOT`:触发一次后自动撤销。 pub oneshot: AtomicBool, /// `IN_EXCL_UNLINK`:已 unlink 子项不再产生事件。 - pub excl_unlink: bool, + pub excl_unlink: AtomicBool, } impl FsNotifyMark { @@ -33,11 +44,8 @@ impl FsNotifyMark { /// FUSE 等多挂载场景可能复用相同 inode 号(如 FUSE_ROOT_ID=1), /// 必须用 (inode_id, dev_id) 组合区分不同挂载上的 inode,否则会跨挂载 /// 误匹配 mark,导致事件泄露或误判已有 watch。 - pub fn identity(&self) -> (InodeId, usize) { - self.inode - .metadata() - .map(|m| (m.inode_id, m.dev_id)) - .unwrap_or((InodeId::new(0), 0)) + pub fn identity(&self) -> FsNotifyObjectId { + self.object_id } } @@ -52,11 +60,20 @@ pub fn destroy_mark(mark: &Arc) { return; }; + // Stop dispatch before removing any lookup path. A snapshot that already + // owns the Arc will observe inactive after acquiring this lock. + let dispatch = mark.dispatch_lock.lock(); + mark.active.store(false, Ordering::Release); + drop(dispatch); + // 从 group.marks 移除(按指针相等)。 let mut marks = group.marks.lock(); - let before = marks.len(); - marks.retain(|m| !Arc::ptr_eq(m, mark)); - let removed = before != marks.len(); + let removed = marks + .get(&mark.object_id) + .is_some_and(|candidate| Arc::ptr_eq(candidate, mark)); + if removed { + marks.remove(&mark.object_id); + } drop(marks); if removed { diff --git a/kernel/src/filesystem/fsnotify/mod.rs b/kernel/src/filesystem/fsnotify/mod.rs index 0890cd7ec1..ba0cffa21d 100644 --- a/kernel/src/filesystem/fsnotify/mod.rs +++ b/kernel/src/filesystem/fsnotify/mod.rs @@ -17,13 +17,114 @@ use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use hashbrown::HashMap; -use crate::filesystem::vfs::{FileType, IndexNode, InodeId}; -use crate::libs::spinlock::SpinLock; +use crate::filesystem::vfs::{mount::MountFSInode, FileType, IndexNode, InodeId}; +use crate::libs::casting::DowncastArc; +use crate::libs::mutex::Mutex; use system_error::SystemError; pub use group::FsNotifyGroup; pub use mark::FsNotifyMark; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EnqueueResult { + Queued, + Merged, + DroppedQueueFull, + AllocationFailed, + Filtered, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct FsNotifyObjectId { + pub superblock: usize, + pub inode: InodeId, + pub generation: u64, +} + +#[derive(Clone, Copy, Debug)] +pub struct FsNotifyTarget { + pub id: FsNotifyObjectId, + pub is_dir: bool, + pub disconnected: bool, +} + +#[derive(Debug)] +pub(crate) struct FsNotifyDeleteState { + pending: bool, + committed: bool, + nlinks: usize, +} + +impl FsNotifyDeleteState { + pub(crate) fn new(nlinks: usize) -> Self { + Self { + pending: false, + committed: false, + nlinks, + } + } + + pub(crate) fn committed(&self) -> bool { + self.committed + } +} + +pub fn target_for_inode(inode: &Arc) -> Result { + if let Some(mounted) = inode.clone().downcast_arc::() { + let (superblock, ino, generation, file_type, disconnected) = mounted.fsnotify_target(); + return Ok(FsNotifyTarget { + id: FsNotifyObjectId { + superblock, + inode: ino, + generation, + }, + is_dir: file_type == FileType::Dir, + disconnected, + }); + } + let md = inode.metadata()?; + Ok(FsNotifyTarget { + id: FsNotifyObjectId { + superblock: md.dev_id, + inode: md.inode_id, + generation: inode.inode_generation(), + }, + is_dir: md.file_type == FileType::Dir, + disconnected: md.nlinks == 0, + }) +} + +pub fn canonical_inode(inode: Arc) -> Arc { + inode + .clone() + .downcast_arc::() + .map(|mounted| mounted.underlying_inode()) + .unwrap_or(inode) +} + +/// Notify both a pathname's current parent entry and the inode itself. This is +/// used by content/attribute operations whose Linux events are visible to both +/// a directory watch and a direct inode watch. +pub fn fsnotify_inode(mask: FsEvent, inode: &Arc) { + if !has_any_watch() { + return; + } + if let Some(mounted) = inode.clone().downcast_arc::() { + let (child, parent) = mounted.fsnotify_snapshot(); + if let Some((parent, name)) = parent.as_ref() { + return fsnotify_targets( + mask, + Some((parent, name.0.as_str())), + Some(&child), + 0, + false, + ); + } + return fsnotify_targets(mask, None, Some(&child), 0, false); + } + fsnotify_with_data(mask, None, Some(inode), 0, false); +} + // 事件 mask:对应 Linux 内核 `FS_*` 事件,其比特位与用户态 `IN_*` 完全一致, // 故可直接作为用户态 mask 使用(仅 `ISDIR` 由 dispatch 按需设置)。 // @@ -61,7 +162,7 @@ pub trait FsNotifyBackend: Send + Sync + core::fmt::Debug { mask: FsEvent, name: Option<&str>, cookie: u32, - ); + ) -> EnqueueResult; /// mark 销毁时从后端内部结构(如 wd 表)移除。 fn free_mark(&self, mark: &FsNotifyMark); /// mark 被撤销时向消费者投递一个 IN_IGNORED 事件(rm_watch/oneshot/DELETE_SELF/UNMOUNT)。 @@ -94,10 +195,103 @@ pub fn next_cookie() -> u32 { // 会跨挂载误匹配,导致事件泄露 / 误判已有 watch。 // 存 `Weak`:group 拥有 mark(强引用),索引只做查找,不阻止回收。 // dispatch 时 `Weak::upgrade()` 失败的死引用会被惰性剔除。 -type MarkIndex = HashMap<(InodeId, usize), Vec>>; +type MarkList = Arc>>; +type MarkIndex = HashMap; lazy_static::lazy_static! { - static ref FSNOTIFY_MARKS: SpinLock = SpinLock::new(HashMap::new()); + static ref FSNOTIFY_MARKS: Mutex = Mutex::new(HashMap::new()); +} + +pub(crate) fn mark_delete_pending(state: &mut FsNotifyDeleteState) { + state.pending = true; + state.nlinks = 0; +} + +pub(crate) fn note_link_added(state: &mut FsNotifyDeleteState) { + state.nlinks = state.nlinks.saturating_add(1); + state.pending = false; + state.committed = false; +} + +pub(crate) fn note_link_removed(state: &mut FsNotifyDeleteState) -> bool { + state.nlinks = state.nlinks.saturating_sub(1); + state.pending = state.nlinks == 0; + state.pending +} + +/// Called at the irreversible dentry/inode detach boundary. The first alias +/// that observes a pending zero-link object commits DELETE_SELF exactly once. +pub(crate) fn notify_dentry_detach(id: FsNotifyObjectId, state: &mut FsNotifyDeleteState) { + if !state.pending { + return; + } + state.pending = false; + state.committed = true; + notify_object_delete(id); +} + +pub(crate) fn notify_object_delete(id: FsNotifyObjectId) { + let marks = FSNOTIFY_MARKS.lock().get(&id).cloned(); + for mark in marks + .iter() + .flat_map(|entries| entries.iter()) + .filter_map(|entry| entry.upgrade()) + { + let guard = mark.dispatch_lock.lock(); + if !mark.active.load(Ordering::Acquire) { + continue; + } + if let Some(group) = mark.group.upgrade() { + group + .backend + .handle_event(&group, &mark, FsEvent::DELETE_SELF, None, 0); + } + mark.active.store(false, Ordering::Release); + drop(guard); + mark::destroy_mark(&mark); + } +} + +pub(crate) fn notify_unmount(superblock: usize) { + loop { + let mark = { + let mut idx = FSNOTIFY_MARKS.lock(); + let next = idx.iter().find_map(|(id, entries)| { + (id.superblock == superblock).then(|| { + ( + *id, + entries + .iter() + .filter_map(|entry| entry.upgrade()) + .find(|mark| mark.active.load(Ordering::Acquire)), + ) + }) + }); + match next { + Some((_id, Some(mark))) => Some(mark), + Some((id, None)) => { + idx.remove(&id); + continue; + } + None => None, + } + }; + let Some(mark) = mark else { break }; + let guard = mark.dispatch_lock.lock(); + if !mark.active.load(Ordering::Acquire) { + drop(guard); + mark::destroy_mark(&mark); + continue; + } + if let Some(group) = mark.group.upgrade() { + group + .backend + .handle_event(&group, &mark, FsEvent::UNMOUNT, None, 0); + } + mark.active.store(false, Ordering::Release); + drop(guard); + mark::destroy_mark(&mark); + } } /// 记录一次 watch 计数变更(add +1,撤销 -1)。仅用于短路,Relaxed 即可。 @@ -115,45 +309,82 @@ pub fn has_any_watch() -> bool { TOTAL_WATCHES.load(Ordering::Relaxed) != 0 } -/// 原子地预留一个 watch 槽位(用于上限检查)。 -/// 成功时 TOTAL_WATCHES 已 +1;超限时回退并返回 ENOSPC。 -/// 这是唯一的全局 watch 计数器,同时服务「快速路径短路」和「max_user_watches 上限检查」。 -pub(crate) fn try_reserve_watch(max: usize) -> Result<(), SystemError> { - let prev = TOTAL_WATCHES.fetch_add(1, Ordering::Relaxed); - if prev >= max { - TOTAL_WATCHES.fetch_sub(1, Ordering::Relaxed); - return Err(SystemError::ENOSPC); - } - Ok(()) -} - -pub(crate) fn index_add(mark: &Arc) { +pub(crate) fn index_add(mark: &Arc) -> Result<(), SystemError> { let key = mark.identity(); - let mut idx = FSNOTIFY_MARKS.lock_irqsave(); - idx.entry(key).or_default().push(Arc::downgrade(mark)); + loop { + let old = FSNOTIFY_MARKS.lock().get(&key).cloned(); + let live = old + .iter() + .flat_map(|entries| entries.iter()) + .filter(|entry| entry.strong_count() != 0) + .count(); + let mut next = Vec::new(); + next.try_reserve_exact(live.saturating_add(1)) + .map_err(|_| SystemError::ENOMEM)?; + next.extend( + old.iter() + .flat_map(|entries| entries.iter()) + .filter(|entry| entry.strong_count() != 0) + .cloned(), + ); + next.push(Arc::downgrade(mark)); + let next = Arc::try_new(next).map_err(|_| SystemError::ENOMEM)?; + + let mut idx = FSNOTIFY_MARKS.lock(); + let unchanged = match (idx.get(&key), old.as_ref()) { + (Some(current), Some(old)) => Arc::ptr_eq(current, old), + (None, None) => true, + _ => false, + }; + if !unchanged { + continue; + } + idx.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + idx.insert(key, next); + return Ok(()); + } } /// 把 mark 从全局索引移除(按指针相等匹配,rm_watch / 撤销时调用)。 pub(crate) fn index_remove(mark: &FsNotifyMark) { let key = mark.identity(); let self_ptr = mark as *const FsNotifyMark; - let mut idx = FSNOTIFY_MARKS.lock_irqsave(); - if let Some(vec) = idx.get_mut(&key) { - let mut i = 0; - while i < vec.len() { - // 剔除:指针相等(同一个 mark),或 Weak 已死。 - let drop_it = match vec[i].upgrade() { - Some(arc) => core::ptr::eq(Arc::as_ptr(&arc), self_ptr), - None => true, - }; - if drop_it { - vec.swap_remove(i); - } else { - i += 1; + loop { + let Some(old) = FSNOTIFY_MARKS.lock().get(&key).cloned() else { + return; + }; + let mut compact = Vec::new(); + if compact.try_reserve_exact(old.len()).is_err() { + return; + } + compact.extend(old.iter().filter_map(|entry| { + entry.upgrade().and_then(|arc| { + (!core::ptr::eq(Arc::as_ptr(&arc), self_ptr)).then(|| Arc::downgrade(&arc)) + }) + })); + let replacement = if compact.is_empty() { + None + } else { + match Arc::try_new(compact) { + Ok(entries) => Some(entries), + Err(_) => return, } + }; + let mut idx = FSNOTIFY_MARKS.lock(); + if !idx + .get(&key) + .is_some_and(|current| Arc::ptr_eq(current, &old)) + { + continue; } - if vec.is_empty() { - idx.remove(&key); + match replacement { + Some(entries) => { + idx.insert(key, entries); + } + None => { + idx.remove(&key); + } } + return; } } @@ -171,6 +402,16 @@ pub fn fsnotify( parent: Option<(&Arc, &str)>, child: Option<&Arc>, cookie: u32, +) { + fsnotify_with_data(mask, parent, child, cookie, true) +} + +fn fsnotify_with_data( + mask: FsEvent, + parent: Option<(&Arc, &str)>, + child: Option<&Arc>, + cookie: u32, + path_event: bool, ) { // ① 快速路径:系统无任何 watch → 直接返回(read/write/close 热路径零成本)。 if TOTAL_WATCHES.load(Ordering::Relaxed) == 0 { @@ -182,18 +423,33 @@ pub fn fsnotify( // 目录决定,对 parent/child 两类 watch 一视同仁。若无 child(仅父目录自身事件 // 的退化情况),ISDIR 不置位。 // 用 (inode_id, dev_id) 复合键:FUSE 多挂载复用相同 inode 号,必须加 dev_id 区分。 - let (child_key, event_is_dir, child_unlinked) = match child { - Some(c) => match c.metadata() { - Ok(md) => ( - Some((md.inode_id, md.dev_id)), - md.file_type == FileType::Dir, - md.nlinks == 0, - ), - Err(_) => (None, false, false), - }, - None => (None, false, false), - }; - let parent_key = parent.and_then(|(p, _)| p.metadata().ok().map(|md| (md.inode_id, md.dev_id))); + let child_target = child.and_then(|inode| target_for_inode(inode).ok()); + let parent_target = parent.and_then(|(inode, _)| target_for_inode(inode).ok()); + fsnotify_targets( + mask, + parent_target.as_ref().zip(parent.map(|(_, name)| name)), + child_target.as_ref(), + cookie, + path_event, + ); +} + +/// Metadata-I/O-free dispatch entry for callers that already hold a coherent +/// dentry snapshot. +pub(crate) fn fsnotify_targets( + mask: FsEvent, + parent: Option<(&FsNotifyTarget, &str)>, + child: Option<&FsNotifyTarget>, + cookie: u32, + path_event: bool, +) { + if TOTAL_WATCHES.load(Ordering::Relaxed) == 0 { + return; + } + let (child_key, event_is_dir, child_unlinked) = child + .map(|target| (Some(target.id), target.is_dir, target.disconnected)) + .unwrap_or((None, false, false)); + let parent_key = parent.map(|(target, _)| target.id); if child_key.is_none() && parent_key.is_none() { return; @@ -201,29 +457,13 @@ pub fn fsnotify( // ③ 收集候选 mark 快照:临界区仅做哈希查表(秒放),不做后端工作。 // (mark 强引用, name, is_parent) - let mut snapshot: Vec<(Arc, Option<&str>, bool)> = Vec::new(); - { - let idx = FSNOTIFY_MARKS.lock_irqsave(); - if let Some(pk) = parent_key { - let name = parent.map(|(_, n)| n); - if let Some(vec) = idx.get(&pk) { - for w in vec.iter() { - if let Some(m) = w.upgrade() { - snapshot.push((m, name, true)); - } - } - } - } - if let Some(ck) = child_key { - if let Some(vec) = idx.get(&ck) { - for w in vec.iter() { - if let Some(m) = w.upgrade() { - snapshot.push((m, None, false)); - } - } - } - } - } + let (parent_marks, child_marks) = { + let idx = FSNOTIFY_MARKS.lock(); + ( + parent_key.and_then(|key| idx.get(&key).cloned()), + child_key.and_then(|key| idx.get(&key).cloned()), + ) + }; // 注:死 Weak 在 lock 内 upgrade 失败时被跳过;惰性清理留给 index_remove。 // 事件路由(Linux 模型): @@ -237,13 +477,27 @@ pub fn fsnotify( // 内容类事件(IN_EXCL_UNLINK 抑制对象)。 let content_type = FsEvent::MODIFY | FsEvent::ACCESS - | FsEvent::ATTRIB | FsEvent::CLOSE_WRITE | FsEvent::CLOSE_NOWRITE | FsEvent::OPEN; // ④ 锁外投递。 - for (mark, name, is_parent) in snapshot { + let parent_name = parent.map(|(_, name)| name); + let candidates = parent_marks + .iter() + .flat_map(|entries| entries.iter()) + .filter_map(|entry| entry.upgrade().map(|mark| (mark, parent_name, true))) + .chain( + child_marks + .iter() + .flat_map(|entries| entries.iter()) + .filter_map(|entry| entry.upgrade().map(|mark| (mark, None, false))), + ); + for (mark, name, is_parent) in candidates { + let dispatch_guard = mark.dispatch_lock.lock(); + if !mark.active.load(Ordering::Acquire) { + continue; + } // 父 mark 收除 self_only 外的全部;自身 mark 收除 parent_only 外的全部。 let routed = if is_parent { mask & !self_only @@ -268,25 +522,41 @@ pub fn fsnotify( continue; } // IN_EXCL_UNLINK:仅对子项的「内容类」事件、且子项已 unlink 时抑制。 - if is_parent && mark.excl_unlink && routed.intersects(content_type) && child_unlinked { + if mark.excl_unlink.load(Ordering::Relaxed) + && path_event + && routed.intersects(content_type) + && child_unlinked + { continue; } } // dispatch 设置 ISDIR(主体是目录时)。 let mut delivered = routed; - if event_is_dir { + if event_is_dir && !routed.intersects(self_only) { delivered |= FsEvent::ISDIR; } - if let Some(group) = mark.group.upgrade() { + let group = mark.group.upgrade(); + let enqueue_result = if let Some(group) = group.as_ref() { group .backend - .handle_event(&group, &mark, delivered, name, cookie); - } + .handle_event(&group, &mark, delivered, name, cookie) + } else { + EnqueueResult::Filtered + }; // 撤销:inode 死亡(无条件)或 oneshot(订阅匹配后触发一次即撤销)。 - if inode_death || mark.oneshot.load(Ordering::Relaxed) { + let consumes_oneshot = matches!( + enqueue_result, + EnqueueResult::Queued | EnqueueResult::Merged | EnqueueResult::DroppedQueueFull + ); + let destroy = inode_death || (mark.oneshot.load(Ordering::Relaxed) && consumes_oneshot); + if destroy { + mark.active.store(false, Ordering::Release); + } + drop(dispatch_guard); + if destroy { mark::destroy_mark(&mark); } } diff --git a/kernel/src/filesystem/fuse/inode.rs b/kernel/src/filesystem/fuse/inode.rs index be30d35b74..58f3af78cd 100644 --- a/kernel/src/filesystem/fuse/inode.rs +++ b/kernel/src/filesystem/fuse/inode.rs @@ -1297,6 +1297,25 @@ impl FuseNode { .store(0, Ordering::Release); } + /// Publish a successful namespace link-count mutation locally. FUSE + /// unlink/rmdir replies carry no attributes, so waiting for GETATTR would + /// leave stale nlink state and race an older reply over the mutation. + pub(crate) fn note_link_removed(&self, directory: bool) -> Option { + let mut metadata = self.cached_metadata.lock(); + let nlinks = metadata.as_mut().map(|md| { + md.nlinks = if directory { + 0 + } else { + md.nlinks.saturating_sub(1) + }; + md.nlinks + }); + self.bump_attr_version(); + self.cached_metadata_deadline_ticks + .store(0, Ordering::Release); + nlinks + } + /// 累计该 inode 在 userspace daemon 侧持有的 LOOKUP 引用。 /// /// 对齐 Linux:每个成功的 LOOKUP/READDIRPLUS entry 都必须被记账,并在 inode diff --git a/kernel/src/filesystem/inotify.rs b/kernel/src/filesystem/inotify.rs index c6cb100269..dbeb97301a 100644 --- a/kernel/src/filesystem/inotify.rs +++ b/kernel/src/filesystem/inotify.rs @@ -7,14 +7,15 @@ //! //! 详见 `docs/kernel/filesystem/inotify.md` §4。 -use alloc::collections::{BTreeMap, VecDeque}; +use alloc::collections::VecDeque; use alloc::string::{String, ToString}; use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; use core::any::Any; -use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use alloc::boxed::Box; +use hashbrown::HashMap; use system_error::SystemError; use crate::arch::interrupt::TrapFrame; @@ -26,7 +27,8 @@ use crate::arch::MMArch; use crate::filesystem::epoll::event_poll::EventPoll; use crate::filesystem::epoll::{EPollEventType, EPollItem}; use crate::filesystem::fsnotify::{ - self, mark, FsEvent, FsNotifyBackend, FsNotifyGroup, FsNotifyMark, + self, mark, EnqueueResult, FsEvent, FsNotifyBackend, FsNotifyDeleteState, FsNotifyGroup, + FsNotifyMark, }; use crate::filesystem::vfs::fcntl::AtFlags; use crate::filesystem::vfs::file::{File, FileFlags, FileMode, FilePrivateData}; @@ -36,9 +38,10 @@ use crate::filesystem::vfs::{ FileSystem, FileType, FsInfo, IndexNode, InodeMode, Magic, Metadata, PollableInode, SuperBlock, NAME_MAX, VFS_MAX_FOLLOW_SYMLINK_TIMES, }; +use crate::libs::casting::DowncastArc; use crate::libs::mutex::{Mutex, MutexGuard}; -use crate::libs::spinlock::SpinLock; use crate::mm::MemoryManagementArch; +use crate::process::namespace::NamespaceOps; use crate::process::ProcessManager; use crate::syscall::table::{FormattedSyscallParam, Syscall}; use crate::syscall::user_access::vfs_check_and_clone_cstr; @@ -76,9 +79,8 @@ mod user_mask { /// add_watch 传入的合法控制位集合(用于 `from_bits` 校验)。 pub const WATCH_CONTROL: u32 = IN_ONLYDIR | IN_DONT_FOLLOW | IN_EXCL_UNLINK | IN_MASK_CREATE | IN_MASK_ADD | IN_ONESHOT; - - /// 仅 add-time 控制位(更新/订阅时应从 mask 剥离)。 - pub const ADD_TIME_ONLY: u32 = IN_ONLYDIR | IN_DONT_FOLLOW | IN_MASK_CREATE | IN_MASK_ADD; + pub const ALL_INOTIFY_BITS: u32 = + WATCH_CONTROL | 0x0000_0fff | IN_UNMOUNT | IN_Q_OVERFLOW | IN_IGNORED | IN_ISDIR; } bitflags::bitflags! { @@ -97,8 +99,63 @@ const MAX_USER_INSTANCES: usize = 128; const MAX_USER_WATCHES: usize = 8192; const MAX_QUEUED_EVENTS: usize = 16384; -/// 全局 inotify 实例计数(近似 per-user;DragonOS 当前单用户语义)。 -static TOTAL_INSTANCES: AtomicUsize = AtomicUsize::new(0); +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct InotifyQuotaKey { + user_namespace: usize, + euid: usize, +} + +#[derive(Clone, Copy, Debug, Default)] +struct InotifyQuotaCounts { + instances: usize, + watches: usize, +} + +lazy_static::lazy_static! { + static ref INOTIFY_QUOTAS: Mutex> = + Mutex::new(HashMap::new()); +} + +fn current_quota_key() -> InotifyQuotaKey { + let cred = ProcessManager::current_pcb().cred(); + InotifyQuotaKey { + user_namespace: cred.user_ns.ns_common().nsid.data(), + euid: cred.euid.data(), + } +} + +fn reserve_instance(key: InotifyQuotaKey) -> Result<(), SystemError> { + let mut quotas = INOTIFY_QUOTAS.lock(); + quotas.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + let counts = quotas.entry(key).or_default(); + if counts.instances >= MAX_USER_INSTANCES { + return Err(SystemError::EMFILE); + } + counts.instances += 1; + Ok(()) +} + +fn reserve_watch(key: InotifyQuotaKey) -> Result<(), SystemError> { + let mut quotas = INOTIFY_QUOTAS.lock(); + quotas.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + let counts = quotas.entry(key).or_default(); + if counts.watches >= MAX_USER_WATCHES { + return Err(SystemError::ENOSPC); + } + counts.watches += 1; + Ok(()) +} + +fn release_quota(key: InotifyQuotaKey, instances: usize, watches: usize) { + let mut quotas = INOTIFY_QUOTAS.lock(); + if let Some(counts) = quotas.get_mut(&key) { + counts.instances = counts.instances.saturating_sub(instances); + counts.watches = counts.watches.saturating_sub(watches); + if counts.instances == 0 && counts.watches == 0 { + quotas.remove(&key); + } + } +} // ============================================================================ // 后端数据结构 @@ -112,15 +169,19 @@ struct InotifyEventInfo { mask: u32, cookie: u32, /// 子项名(目录 watch 的子事件才有)。 - name: Option>, + name: Option, } /// 事件队列。受 `events` 锁保护。 #[derive(Debug)] struct InotifyQueue { list: VecDeque, - /// 置位后已插入一个 `IN_Q_OVERFLOW(wd=-1)`;消费该事件时清零。 - overflowed: bool, + /// A logical overflow record. It cannot be stored in `list`: queue + /// overflow and allocator failure are exactly the cases where growing the + /// deque is not reliable. `pre_overflow_remaining` preserves its position + /// relative to ordinary records accepted before and after the loss. + overflow_pending: bool, + pre_overflow_remaining: usize, } /// watch descriptor 表。受 `wd` 锁保护。 @@ -128,7 +189,7 @@ struct InotifyQueue { struct WdTable { /// 单调分配 wd(1..=i32::MAX-1,饱和见 §6.2;-1 被 Q_OVERFLOW 占用)。 counter: i32, - map: BTreeMap>, + map: HashMap>, } /// inotify 后端共享状态:同时被 `InotifyBackend`(在 group 内)与 `InotifyInode`(read 入口)持有。 @@ -136,25 +197,32 @@ struct WdTable { /// 事件锁与 wd 锁分离,使 read(消费)与 add_watch/rm_watch(wd 管理)互不阻塞。 #[derive(Debug)] pub struct InotifyState { - /// 事件锁:handle_event(生产) 与 read(消费) 竞争。irqsave:fsnotify 可在持 VFS 锁时调用。 - events: SpinLock, + /// 事件锁:所有 hook 都来自可睡眠的 VFS/file 操作上下文。 + events: Mutex, + /// One read must consume a contiguous queue prefix even though events are + /// serialized outside `events`. + read_consumer: Mutex<()>, max_queued_events: usize, /// wd 锁:add_watch/rm_watch 竞争。 wd: Mutex, + quota_key: InotifyQuotaKey, } impl InotifyState { - fn new() -> Self { + fn new(quota_key: InotifyQuotaKey) -> Self { Self { - events: SpinLock::new(InotifyQueue { + events: Mutex::new(InotifyQueue { list: VecDeque::new(), - overflowed: false, + overflow_pending: false, + pre_overflow_remaining: 0, }), + read_consumer: Mutex::new(()), max_queued_events: MAX_QUEUED_EVENTS, wd: Mutex::new(WdTable { counter: 0, - map: BTreeMap::new(), + map: HashMap::new(), }), + quota_key, } } } @@ -166,28 +234,43 @@ struct InotifyBackend { } impl InotifyBackend { - /// 事件是否可合并(仅 ACCESS/MODIFY 且无 name,见 §6.3)。 - fn mergeable(mask: FsEvent) -> bool { - let core = mask - FsEvent::ISDIR; - core == FsEvent::ACCESS || core == FsEvent::MODIFY - } - /// 入队一个事件(调用方已持 events 锁)。 - fn enqueue_locked(q: &mut InotifyQueue, max: usize, ev: InotifyEventInfo) { - if q.list.len() >= max { - // 队列满:丢弃当前事件,置 overflow 标志并插入单个 Q_OVERFLOW(仅一次)。 - if !q.overflowed { - q.overflowed = true; - q.list.push_back(InotifyEventInfo { - wd: -1, - mask: user_mask::IN_Q_OVERFLOW, - cookie: 0, - name: None, - }); + fn enqueue_locked(q: &mut InotifyQueue, max: usize, ev: InotifyEventInfo) -> EnqueueResult { + // Linux compares wd/mask/name, but deliberately not the move cookie. + // IN_IGNORED is never merged. Do not merge across a pending logical + // overflow boundary. + if ev.mask != user_mask::IN_IGNORED + && (!q.overflow_pending || q.pre_overflow_remaining < q.list.len()) + && q.list.back().is_some_and(|tail| { + tail.wd == ev.wd && tail.mask == ev.mask && tail.name == ev.name + }) + { + return EnqueueResult::Merged; + } + + if q.list.len().saturating_add(usize::from(q.overflow_pending)) >= max { + if !q.overflow_pending { + q.overflow_pending = true; + q.pre_overflow_remaining = q.list.len(); + return EnqueueResult::DroppedQueueFull; } - return; + return EnqueueResult::DroppedQueueFull; + } + if q.list.try_reserve(1).is_err() { + Self::record_overflow(q); + return EnqueueResult::AllocationFailed; } q.list.push_back(ev); + EnqueueResult::Queued + } + + fn record_overflow(q: &mut InotifyQueue) -> bool { + if q.overflow_pending { + return false; + } + q.overflow_pending = true; + q.pre_overflow_remaining = q.list.len(); + true } } @@ -199,68 +282,118 @@ impl FsNotifyBackend for InotifyBackend { mask: FsEvent, name: Option<&str>, cookie: u32, - ) { + ) -> EnqueueResult { // 用户订阅 mask(ISDIR 始终保留,由 dispatch 设置)。 let subscribed = mark.mask.load(Ordering::Relaxed); let user_mask = mask.bits() & (subscribed | FsEvent::ISDIR.bits()); if user_mask == 0 { - return; + return EnqueueResult::Filtered; } - // name 截断到 NAME_MAX。 + // Truncate without allocating. Most events merge or hit a full queue; + // decide those cases using the borrowed name before making a copy. let name = name.map(|n| { - if n.len() > NAME_MAX { - &n[..NAME_MAX] - } else { - n + let mut end = core::cmp::min(n.len(), NAME_MAX); + while !n.is_char_boundary(end) { + end -= 1; } + &n[..end] }); + let early = { + let mut queue = self.state.events.lock(); + let readable_before = !queue.list.is_empty() || queue.overflow_pending; + let merge = user_mask != user_mask::IN_IGNORED + && (!queue.overflow_pending || queue.pre_overflow_remaining < queue.list.len()) + && queue.list.back().is_some_and(|tail| { + tail.wd == mark.wd && tail.mask == user_mask && tail.name.as_deref() == name + }); + let result = if merge { + Some(EnqueueResult::Merged) + } else if queue + .list + .len() + .saturating_add(usize::from(queue.overflow_pending)) + >= self.state.max_queued_events + { + Self::record_overflow(&mut queue); + Some(EnqueueResult::DroppedQueueFull) + } else { + None + }; + let wake = !readable_before && (!queue.list.is_empty() || queue.overflow_pending); + (result, wake) + }; + if early.1 { + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + } + if let Some(result) = early.0 { + return result; + } + + let name = match name { + Some(n) => { + let mut owned = String::new(); + if owned.try_reserve_exact(n.len()).is_err() { + let wake = Self::record_overflow(&mut self.state.events.lock()); + if wake { + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + } + return EnqueueResult::AllocationFailed; + } + owned.push_str(n); + Some(owned) + } + None => None, + }; let ev = InotifyEventInfo { wd: mark.wd, mask: user_mask, cookie, - name: name.map(Box::::from), + name, }; - let wake = { - let mut q = self.state.events.lock_irqsave(); - // 合并:末尾事件 (wd, mask, cookie, name) 完全相同且可合并类 → 丢弃。 - if Self::mergeable(mask) && ev.name.is_none() { - if let Some(tail) = q.list.back() { - if tail.wd == ev.wd - && tail.mask == ev.mask - && tail.cookie == ev.cookie - && tail.name.is_none() - { - return; - } - } - } - Self::enqueue_locked(&mut q, self.state.max_queued_events, ev); - true + let (result, wake) = { + let mut queue = self.state.events.lock(); + let readable_before = !queue.list.is_empty() || queue.overflow_pending; + let result = Self::enqueue_locked(&mut queue, self.state.max_queued_events, ev); + let readable_after = !queue.list.is_empty() || queue.overflow_pending; + (result, !readable_before && readable_after) }; // 唤醒 read 等待者与 epoll。 - group.wait_queue.wakeup_all(None); - let _ = EventPoll::wakeup_epoll( - &group.epitems, - EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, - ); - let _ = wake; + if wake { + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + } + result } fn free_mark(&self, mark: &FsNotifyMark) { let mut t = self.state.wd.lock(); - t.map.remove(&mark.wd); + if t.map.remove(&mark.wd).is_some() { + release_quota(self.state.quota_key, 0, 1); + } } fn notify_ignored(&self, group: &FsNotifyGroup, mark: &FsNotifyMark) { // 投递 IN_IGNORED(watch 被撤销:rm_watch/oneshot/DELETE_SELF/UNMOUNT)。 let wake = { - let mut q = self.state.events.lock_irqsave(); - Self::enqueue_locked( - &mut q, + let mut queue = self.state.events.lock(); + let readable_before = !queue.list.is_empty() || queue.overflow_pending; + let _ = Self::enqueue_locked( + &mut queue, self.state.max_queued_events, InotifyEventInfo { wd: mark.wd, @@ -269,18 +402,20 @@ impl FsNotifyBackend for InotifyBackend { name: None, }, ); - true + !readable_before && (!queue.list.is_empty() || queue.overflow_pending) }; - group.wait_queue.wakeup_all(None); - let _ = EventPoll::wakeup_epoll( - &group.epitems, - EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, - ); - let _ = wake; + if wake { + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + } } fn queue_nonempty(&self) -> bool { - !self.state.events.lock_irqsave().list.is_empty() + let q = self.state.events.lock(); + !q.list.is_empty() || q.overflow_pending } } @@ -310,7 +445,13 @@ impl FileSystem for InotifyFs { } fn root_inode(&self) -> Arc { // 不会被真正调用:inotify 不挂载。 - Arc::new(InotifyInode::new(false)) + Arc::new(InotifyInode::new( + false, + InotifyQuotaKey { + user_namespace: 0, + euid: 0, + }, + )) } fn info(&self) -> FsInfo { @@ -348,9 +489,17 @@ pub struct InotifyInode { } impl InotifyInode { + fn validate_watch_mask(mask: u32) -> Result<(), SystemError> { + if mask == 0 || (mask & !user_mask::ALL_INOTIFY_BITS) != 0 { + Err(SystemError::EINVAL) + } else { + Ok(()) + } + } + /// 创建一个新的 inotify 实例(inotify_init1 用)。 - pub fn new(nonblock: bool) -> Self { - let state = Arc::new(InotifyState::new()); + fn new(nonblock: bool, quota_key: InotifyQuotaKey) -> Self { + let state = Arc::new(InotifyState::new(quota_key)); let backend = Box::new(InotifyBackend { state: state.clone(), }); @@ -407,29 +556,34 @@ impl InotifyInode { /// `inotify_add_watch`:在 `inode` 上建立(或更新)一个 watch。 pub fn add_watch(&self, inode: Arc, mask: u32) -> Result { - // mask 校验:仅允许已知位。 - if (mask - & !(user_mask::WATCH_CONTROL - | 0x00000fff // IN_ALL_EVENTS 低 12 位 - | user_mask::IN_UNMOUNT - | user_mask::IN_Q_OVERFLOW - | user_mask::IN_IGNORED)) - != 0 + if let Some(mounted) = inode + .clone() + .downcast_arc::() { - return Err(SystemError::EINVAL); + return mounted.with_fsnotify_admission(|| { + mounted.with_fsnotify_watch_lifecycle(|lifecycle| { + self.add_watch_inner(inode, mask, Some(lifecycle)) + }) + }); } + self.add_watch_inner(inode, mask, None) + } - // mask 必须含至少一个事件位(低 12 位 IN_ALL_EVENTS)。 - if (mask & 0x0000_0fff) == 0 { - return Err(SystemError::EINVAL); - } + fn add_watch_inner( + &self, + inode: Arc, + mask: u32, + delete_lifecycle: Option>>, + ) -> Result { + // mask 校验:仅允许已知位。 + Self::validate_watch_mask(mask)?; // IN_MASK_ADD 与 IN_MASK_CREATE 互斥。 if (mask & user_mask::IN_MASK_ADD) != 0 && (mask & user_mask::IN_MASK_CREATE) != 0 { return Err(SystemError::EINVAL); } let md = inode.metadata()?; - let target_identity = (md.inode_id, md.dev_id); + let target_identity = fsnotify::target_for_inode(&inode)?.id; // IN_ONLYDIR:目标必须是目录。 if (mask & user_mask::IN_ONLYDIR) != 0 && md.file_type != FileType::Dir { @@ -439,8 +593,10 @@ impl InotifyInode { // 读权限检查(防止通过监听泄露文件名/元数据)。 check_inode_permission(&inode, &md, PermissionMask::MAY_READ)?; - // 订阅位:剥离 add-time 控制位,保留 ONESHOT/EXCL_UNLINK。 - let event_mask = mask & !user_mask::ADD_TIME_ONLY; + // Linux stores only user event bits and adds UNMOUNT implicitly; + // ONESHOT/EXCL_UNLINK are mark flags, while ISDIR/Q_OVERFLOW/IGNORED + // are output-only bits even though the syscall accepts them. + let event_mask = (mask & 0x0000_0fff) | user_mask::IN_UNMOUNT; // 查找同 inode 上已有 mark(同 group);若无,则在**同一把 marks 锁**内完成 // 新建与插入,避免并发 add_watch 同一 inode 产生重复 mark(TOCTOU:查重与插入 @@ -449,53 +605,86 @@ impl InotifyInode { // 锁序 marks → wd → FSNOTIFY 为此处引入的嵌套;全代码库无反向获取 // (destroy_mark/dispatch 均先释放 marks 再取 wd/FSNOTIFY),故无死锁。 let mut marks = self.group.marks.lock(); - if let Some(existing) = marks.iter().find(|m| m.identity() == target_identity) { + if let Some(existing) = marks.get(&target_identity) { if (mask & user_mask::IN_MASK_CREATE) != 0 { return Err(SystemError::EEXIST); } + let _dispatch = existing.dispatch_lock.lock(); + if !existing.active.load(Ordering::Acquire) { + let stale = existing.clone(); + drop(_dispatch); + drop(marks); + mark::destroy_mark(&stale); + return self.add_watch_inner(inode, mask, delete_lifecycle); + } if (mask & user_mask::IN_MASK_ADD) != 0 { existing.mask.fetch_or(event_mask, Ordering::Relaxed); // OR 语义:任一来源设置 oneshot 即生效(新 mask 或已有状态)。 if (mask & user_mask::IN_ONESHOT) != 0 { existing.oneshot.store(true, Ordering::Relaxed); } + if (mask & user_mask::IN_EXCL_UNLINK) != 0 { + existing.excl_unlink.store(true, Ordering::Relaxed); + } } else { existing.mask.store(event_mask, Ordering::Relaxed); existing .oneshot .store((mask & user_mask::IN_ONESHOT) != 0, Ordering::Relaxed); + existing + .excl_unlink + .store((mask & user_mask::IN_EXCL_UNLINK) != 0, Ordering::Relaxed); } return Ok(existing.wd); } - // watch 上限(唯一全局计数器:同时服务快速路径与上限检查)。 - fsnotify::try_reserve_watch(MAX_USER_WATCHES)?; + // Reserve every fallible container allocation before publication. + marks.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; // 分配 wd(饱和到 i32::MAX-1;-1 被 Q_OVERFLOW 占用)。 let wd = { let mut t = self.state.wd.lock(); if t.counter >= i32::MAX - 1 { - fsnotify::adjust_total_watches(-1); return Err(SystemError::ENOSPC); } + t.map.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; t.counter += 1; t.counter }; - let mark = Arc::new(FsNotifyMark { + let mark = Arc::try_new(FsNotifyMark { wd, group: Arc::downgrade(&self.group), - inode: inode.clone(), + _inode: fsnotify::canonical_inode(inode.clone()), + _delete_lifecycle: delete_lifecycle, + object_id: target_identity, + dispatch_lock: Mutex::new(()), + active: AtomicBool::new(true), mask: AtomicU32::new(event_mask), oneshot: AtomicBool::new((mask & user_mask::IN_ONESHOT) != 0), - excl_unlink: (mask & user_mask::IN_EXCL_UNLINK) != 0, - }); + excl_unlink: AtomicBool::new((mask & user_mask::IN_EXCL_UNLINK) != 0), + }) + .map_err(|_| SystemError::ENOMEM)?; + + // Quota is committed only after all local allocations succeeded. Any + // failure at the final global-index publication is rolled back below. + reserve_watch(self.state.quota_key)?; + fsnotify::adjust_total_watches(1); // 持 marks 锁完成全部插入(wd 表 / group.marks / 全局索引)。 self.state.wd.lock().map.insert(wd, Arc::downgrade(&mark)); - marks.push(mark.clone()); + marks.insert(target_identity, mark.clone()); + // The global index insertion is the publication point. Keep the group + // management lock held until every structure dispatch relies on is + // complete, so there is no visible-but-half-initialized window. + if let Err(error) = fsnotify::index_add(&mark) { + marks.remove(&target_identity); + self.state.wd.lock().map.remove(&wd); + fsnotify::adjust_total_watches(-1); + release_quota(self.state.quota_key, 0, 1); + return Err(error); + } drop(marks); - fsnotify::index_add(&mark); // 注:watch 计数已由 try_reserve_watch 原子 +1(含上限检查),此处不可再次 +1, // 否则每次 add 会 +2 而 destroy 仅 -1,导致 TOTAL_WATCHES 永不归零(fast-path 短路 // 失效,read/write/close 热路径永久付出 fsnotify 锁开销)且上限提前触顶。 @@ -506,8 +695,8 @@ impl InotifyInode { /// `inotify_rm_watch`:按 wd 移除一个 watch。 pub fn rm_watch(&self, wd: i32) -> Result<(), SystemError> { let mark = { - let mut t = self.state.wd.lock(); - match t.map.remove(&wd) { + let t = self.state.wd.lock(); + match t.map.get(&wd) { Some(w) => w.upgrade().ok_or(SystemError::EINVAL), None => Err(SystemError::EINVAL), } @@ -524,14 +713,18 @@ impl InotifyInode { core::mem::take(&mut *g) }; let n = marks.len(); - for m in &marks { + for m in marks.values() { + let dispatch = m.dispatch_lock.lock(); + m.active.store(false, Ordering::Release); + drop(dispatch); fsnotify::index_remove(m); self.state.wd.lock().map.remove(&m.wd); } if n > 0 { fsnotify::adjust_total_watches(-(n as i32)); + release_quota(self.state.quota_key, 0, n); } - TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); + release_quota(self.state.quota_key, 1, 0); // 唤醒任何阻塞在 read 的线程(队列不再增长)。 self.group.wait_queue.wakeup_all(None); } @@ -599,13 +792,60 @@ impl IndexNode for InotifyInode { return Err(SystemError::EINVAL); } + let _consumer = self.state.read_consumer.lock(); + loop { - let mut written; - { - let mut q = self.state.events.lock_irqsave(); - if q.list.is_empty() { + let mut written = 0; + loop { + let mut blocked_by_size = false; + let next = { + let mut q = self.state.events.lock(); + if q.overflow_pending && q.pre_overflow_remaining == 0 { + if written + 16 > len { + blocked_by_size = true; + None + } else { + q.overflow_pending = false; + q.pre_overflow_remaining = 0; + Some(InotifyEventInfo { + wd: -1, + mask: user_mask::IN_Q_OVERFLOW, + cookie: 0, + name: None, + }) + } + } else if let Some(front) = q.list.front() { + let record_len = Self::record_len(front.name.as_deref()); + if written + record_len > len { + blocked_by_size = true; + None + } else { + let ev = q.list.pop_front().expect("front existed under events lock"); + if q.overflow_pending && q.pre_overflow_remaining > 0 { + q.pre_overflow_remaining -= 1; + } + Some(ev) + } + } else { + None + } + }; + + let Some(ev) = next else { + let _ = blocked_by_size; + break; + }; + let rl = Self::record_len(ev.name.as_deref()); + written += Self::serialize(&ev, &mut buf[written..written + rl]); + } + + if written == 0 { + let empty = { + let q = self.state.events.lock(); + q.list.is_empty() && !q.overflow_pending + }; + if empty { // 空队列 - drop(q); if self.nonblock.load(Ordering::Relaxed) { return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); } @@ -619,23 +859,6 @@ impl IndexNode for InotifyInode { )?; continue; } - - // 逐个从头部打包完整事件,放不下的留在队列下次读。 - written = 0; - while let Some(front) = q.list.front() { - let rl = Self::record_len(front.name.as_deref()); - if written + rl > len { - break; - } - let ev = q.list.pop_front().unwrap(); - if ev.mask == user_mask::IN_Q_OVERFLOW { - q.overflowed = false; - } - written += Self::serialize(&ev, &mut buf[written..written + rl]); - } - } - - if written == 0 { // 首个事件即放不下(name 过大)。 return Err(SystemError::EINVAL); } @@ -694,14 +917,11 @@ impl IndexNode for InotifyInode { pub fn do_inotify_init1(flags: u32) -> Result { let flags = InotifyInitFlags::from_bits(flags).ok_or(SystemError::EINVAL)?; - // 实例上限。 - if TOTAL_INSTANCES.fetch_add(1, Ordering::Relaxed) >= MAX_USER_INSTANCES { - TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); - return Err(SystemError::EMFILE); - } + let quota_key = current_quota_key(); + reserve_instance(quota_key)?; let nonblock = flags.contains(InotifyInitFlags::IN_NONBLOCK); - let inode = Arc::new(InotifyInode::new(nonblock)); + let inode = Arc::new(InotifyInode::new(nonblock, quota_key)); let file_flags = FileFlags::O_RDONLY | (if nonblock { FileFlags::O_NONBLOCK @@ -710,7 +930,7 @@ pub fn do_inotify_init1(flags: u32) -> Result { }); let file = File::new(inode, file_flags).inspect_err(|_| { // File::new 失败:file 未创建,不会 drop→shutdown,需手动回退实例计数。 - TOTAL_INSTANCES.fetch_sub(1, Ordering::Relaxed); + release_quota(quota_key, 1, 0); })?; // 防递归:inotify fd 自身的 read/write 不应产生事件。 file.set_mode_flags(FileMode::FMODE_NONOTIFY); @@ -792,9 +1012,9 @@ impl Syscall for SysInotifyAddWatchHandle { let path_ptr = Self::pathname(args); let mask = Self::mask(args); - let path = vfs_check_and_clone_cstr(path_ptr, Some(crate::filesystem::vfs::MAX_PATHLEN))? - .into_string() - .map_err(|_| SystemError::EINVAL)?; + // Linux validates unknown/zero masks before fdget. Pathname copying is + // deliberately later, after fd/type/control validation. + InotifyInode::validate_watch_mask(mask)?; // 取 inotify fd 对应的 InotifyInode(file Arc 保活,ino 借用其 inode)。 let file: Arc = { @@ -805,10 +1025,18 @@ impl Syscall for SysInotifyAddWatchHandle { .ok_or(SystemError::EBADF)? }; let inode = file.inode(); + // IN_MASK_ADD and IN_MASK_CREATE are checked after fdget but before + // verifying the descriptor type, matching Linux error priority. + if (mask & user_mask::IN_MASK_ADD) != 0 && (mask & user_mask::IN_MASK_CREATE) != 0 { + return Err(SystemError::EINVAL); + } let inotify_inode = inode .as_any_ref() .downcast_ref::() .ok_or(SystemError::EINVAL)?; + let path = vfs_check_and_clone_cstr(path_ptr, Some(crate::filesystem::vfs::MAX_PATHLEN))? + .into_string() + .map_err(|_| SystemError::EINVAL)?; // 解析路径(IN_DONT_FOLLOW:不跟随末尾 symlink)。 let pcb = ProcessManager::current_pcb(); let (inode_begin, remain_path) = user_path_at(&pcb, AtFlags::AT_FDCWD.bits(), &path)?; diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 79a106da2c..6796346ac5 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -4,12 +4,7 @@ use core::{ }; use crate::filesystem::fsnotify::{self, FsEvent}; -use alloc::{ - boxed::Box, - string::String, - sync::{Arc, Weak}, - vec::Vec, -}; +use alloc::{string::String, sync::Arc, vec::Vec}; use log::error; use system_error::SystemError; @@ -682,10 +677,6 @@ pub struct File { /// via `eventpoll_release(file)`. DragonOS keeps the same lifetime edge /// here so fd numbers can be safely reused after close. epitems: Arc, - /// fsnotify 内容事件用:open 时解析的父目录(Weak)+子项名。仅当系统存在 watch 时填充, - /// 供 do_write/do_read/Drop/IN_OPEN 向父目录 watch 投递子项内容事件(issue B 修复)。 - /// 局限:open 后 rename 会使快照 stale(dentry.parent 原地变更);EXCL_UNLINK 覆盖 unlink。 - inotify_parent: Option<(Weak, Box)>, /// One semantic inode pin per open file description. Duplicated file /// descriptors and VMAs share this `File`, while `O_PATH` still owns it. /// Declared last so the explicit `Drop` body runs `close()` before release. @@ -1082,13 +1073,7 @@ impl File { && !self.mode.read().contains(FileMode::FMODE_NONOTIFY) && fsnotify::has_any_watch() { - // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 - match self.inotify_parent_for_fsnotify() { - Some((parent, name)) => { - fsnotify::fsnotify(FsEvent::MODIFY, Some((&parent, name)), Some(&self.inode), 0) - } - None => fsnotify::fsnotify(FsEvent::MODIFY, None, Some(&self.inode), 0), - } + self.notify_fs_event(FsEvent::MODIFY); } Ok(written_len) } @@ -1279,19 +1264,6 @@ impl File { .downcast_arc::() .map(|mnt_inode| mnt_inode.mount_fs() as Arc); - // fsnotify:解析父目录+子项名快照存入 File,供内容事件 hook 向父目录 watch - // 投递子项 IN_MODIFY/ACCESS/OPEN/CLOSE(issue B)。 - // 始终解析(不门控 has_any_watch):若 open 时无 watch 则设为 None,之后加 watch - // 也不生效——watch 永远收不到已 open 文件的内容事件。解析本身轻量(一次 downcast - // + do_parent/dname),非 MountFSInode 直接 None。 - // 设备/套接字等非 MountFSInode 解析为 None。 - // 注意:MountFSInode::as_any_ref() 返回内部 inode 的 Any,因此 - // downcast_ref::() 永远失败。必须用 downcast_arc。 - let inotify_parent = inode - .clone() - .downcast_arc::() - .and_then(|mnt| mnt.fsnotify_parent_and_name()) - .map(|(parent, name)| (Arc::downgrade(&parent), name.into_boxed_str())); let f = File { open_file_id: alloc_open_file_id(), inode, @@ -1312,7 +1284,6 @@ impl File { wb_error_seq: Mutex::new(wb_error_seq), sb_error_seq: Mutex::new(sb_error_seq), epitems: Arc::new(EPollItemList::default()), - inotify_parent, _inode_retention: inode_retention, _mount_guard: mount_guard, }; @@ -1320,12 +1291,25 @@ impl File { return Ok(f); } - /// 解析 fsnotify 内容事件的 parent 参数:upgrade 构造期存储的 Weak 父目录。 - /// 返回 (owned Arc 父目录, 借用的子项名);调用方须绑定返回值以延长大 Arc 生命周期。 - /// 仅对 MountFSInode 上的普通文件返回 Some。 - fn inotify_parent_for_fsnotify(&self) -> Option<(Arc, &str)> { - let (parent, name) = self.inotify_parent.as_ref()?; - Some((parent.upgrade()?, name.as_ref())) + /// Dispatch using one coherent dentry snapshot. This keeps the read/write + /// hot path free of namespace walks and temporary String allocations. + pub(crate) fn notify_fs_event(&self, mask: FsEvent) { + if let Some(mounted) = self.inode.clone().downcast_arc::() { + let (child, parent) = mounted.fsnotify_snapshot(); + if let Some((parent, name)) = parent.as_ref() { + fsnotify::fsnotify_targets( + mask, + Some((parent, name.0.as_str())), + Some(&child), + 0, + true, + ); + } else { + fsnotify::fsnotify_targets(mask, None, Some(&child), 0, true); + } + } else { + fsnotify::fsnotify(mask, None, Some(&self.inode), 0); + } } /// Create a file object for sockets created by socket syscalls. @@ -1612,13 +1596,7 @@ impl File { && !self.mode.read().contains(FileMode::FMODE_NONOTIFY) && fsnotify::has_any_watch() { - // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 - match self.inotify_parent_for_fsnotify() { - Some((parent, name)) => { - fsnotify::fsnotify(FsEvent::ACCESS, Some((&parent, name)), Some(&self.inode), 0) - } - None => fsnotify::fsnotify(FsEvent::ACCESS, None, Some(&self.inode), 0), - } + self.notify_fs_event(FsEvent::ACCESS); } Ok(len) } @@ -2035,6 +2013,13 @@ impl File { // read_dir_impl has released readdir_state before this metadata update, // avoiding a cross-filesystem lock-order dependency. self.touch_atime_after_access(); + if !self + .mode + .read() + .intersects(FileMode::FMODE_PATH | FileMode::FMODE_NONOTIFY) + { + self.notify_fs_event(FsEvent::ACCESS); + } result } @@ -2221,7 +2206,6 @@ impl File { wb_error_seq: Mutex::new(*self.wb_error_seq.lock()), sb_error_seq: Mutex::new(*self.sb_error_seq.lock()), epitems: Arc::new(EPollItemList::default()), - inotify_parent: self.inotify_parent.clone(), _inode_retention: inode_retention, _mount_guard: mount_guard, }; @@ -2520,19 +2504,16 @@ impl Drop for File { } // fsnotify:最后一次 close → IN_CLOSE_WRITE / IN_CLOSE_NOWRITE(FMODE_NONOTIFY 短路)。 // 此时 self.inode 仍存活(Drop 在 inode.close() 之前),可安全取 inode_id。 - if !self.mode.read().contains(FileMode::FMODE_NONOTIFY) && fsnotify::has_any_watch() { - let m = if self.mode.read().contains(FileMode::FMODE_WRITER) { + let mode = *self.mode.read(); + if !mode.intersects(FileMode::FMODE_NONOTIFY | FileMode::FMODE_PATH) + && fsnotify::has_any_watch() + { + let m = if mode.contains(FileMode::FMODE_WRITE) { FsEvent::CLOSE_WRITE } else { FsEvent::CLOSE_NOWRITE }; - // 同时通知父目录 watch(带子名,issue B)与子项自身 watch。 - match self.inotify_parent_for_fsnotify() { - Some((parent, name)) => { - fsnotify::fsnotify(m, Some((&parent, name)), Some(&self.inode), 0) - } - None => fsnotify::fsnotify(m, None, Some(&self.inode), 0), - } + self.notify_fs_event(m); } if self.flags().contains(FileFlags::FASYNC) { diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index 294815eefa..cca9ce9f9a 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -9,6 +9,7 @@ use crate::{ driver::base::device::device_number::{DeviceNumber, Major}, exception::workqueue::{schedule_work, Work}, filesystem::{ + fsnotify::{self, FsNotifyDeleteState, FsNotifyObjectId, FsNotifyTarget}, page_cache::PageCache, vfs::{fcntl::AtFlags, syscall::RenameFlags, vcore::do_mkdir_at}, }, @@ -62,6 +63,7 @@ use system_error::SystemError; /// mount's propagation state -> propagation group allocator. /// A lower layer must never acquire the lifecycle/topology layers in reverse. pub(crate) static MOUNT_LIFECYCLE_LOCK: Mutex<()> = Mutex::new(()); +static NEXT_SUPERBLOCK_FSNOTIFY_ID: AtomicUsize = AtomicUsize::new(1); lazy_static! { /// Serializes pathname rendering against alias rename/disconnect. Mount @@ -663,6 +665,10 @@ unsafe impl Sync for MountExternalGuard {} #[derive(Debug)] pub struct SuperBlockState { + /// Monotonic identity used by fsnotify; unlike an Arc address it cannot be + /// confused after allocator address reuse. + fsnotify_id: usize, + fsnotify_object_locks: Mutex>>>, /// User namespace that owns this superblock, matching Linux `s_user_ns`. /// Bind mounts and mount-namespace copies retain the same owner. owner_user_ns: Arc, @@ -723,6 +729,9 @@ pub struct VfsDentry { /// follow later FUSE invalidations, otherwise the original key could no /// longer be removed deterministically. registry_generation: u64, + fsnotify_superblock: usize, + fsnotify_delete_lifecycle: Arc>, + file_type: FileType, /// Serializes exact-edge attach/detach against rename/unlink/rmdir of this /// alias without holding a global mount lock across filesystem I/O. mount_gate: Mutex<()>, @@ -777,7 +786,12 @@ impl VfsDentry { /// Magic-link targets use this to retain a mount projection for open and /// bind mount. Anonymous dentries deliberately never enter the ordinary /// dentry registry and therefore cannot participate in lookup or rename. - fn new_anonymous(inode: Arc, dname: DName) -> Result, SystemError> { + fn new_anonymous( + inode: Arc, + dname: DName, + fsnotify_superblock: usize, + fsnotify_delete_lifecycle: Arc>, + ) -> Result, SystemError> { let metadata = inode.metadata()?; let generation = inode.inode_generation(); Ok(Arc::new(Self { @@ -785,6 +799,9 @@ impl VfsDentry { inode, registry_child: metadata.inode_id, registry_generation: generation, + fsnotify_superblock, + fsnotify_delete_lifecycle, + file_type: metadata.file_type, mount_gate: Mutex::new(()), children_gate: Mutex::new(()), mount_edges: AtomicUsize::new(0), @@ -799,6 +816,23 @@ impl VfsDentry { } } +impl Drop for VfsDentry { + fn drop(&mut self) { + if !self.state.lock().disconnected { + return; + } + let mut lifecycle = self.fsnotify_delete_lifecycle.lock(); + fsnotify::notify_dentry_detach( + FsNotifyObjectId { + superblock: self.fsnotify_superblock, + inode: self.registry_child, + generation: self.registry_generation, + }, + &mut lifecycle, + ); + } +} + fn dentry_is_descendant_of(dentry: &Arc, ancestor: &Arc) -> bool { let mut current = Some(dentry.clone()); let mut visited = hashbrown::HashSet::new(); @@ -872,6 +906,8 @@ impl SuperBlockState { pub fn new(flags: MountFlags) -> Self { let flags = flags & MountFlags::SB_SETTABLE_MASK; Self { + fsnotify_id: NEXT_SUPERBLOCK_FSNOTIFY_ID.fetch_add(1, Ordering::Relaxed), + fsnotify_object_locks: Mutex::new(HashMap::new()), owner_user_ns: ProcessManager::current_user_ns(), flags: RwSem::new(flags), synchronous: AtomicBool::new(flags.contains(MountFlags::SYNCHRONOUS)), @@ -894,6 +930,37 @@ impl SuperBlockState { &self.owner_user_ns } + fn fsnotify_object_lock( + &self, + inode: InodeId, + generation: u64, + nlinks: usize, + ) -> Result>, SystemError> { + let mut locks = self.fsnotify_object_locks.lock(); + if let Some(lock) = locks.get(&(inode, generation)).and_then(Weak::upgrade) { + return Ok(lock); + } + locks.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + let lock = Arc::try_new(Mutex::new(FsNotifyDeleteState::new(nlinks))) + .map_err(|_| SystemError::ENOMEM)?; + locks.insert((inode, generation), Arc::downgrade(&lock)); + if locks.len().is_multiple_of(256) { + locks.retain(|_, lock| lock.strong_count() != 0); + } + Ok(lock) + } + + fn fsnotify_object_state( + &self, + inode: InodeId, + generation: u64, + ) -> Option>> { + self.fsnotify_object_locks + .lock() + .get(&(inode, generation)) + .and_then(Weak::upgrade) + } + fn activate_mount(&self, construction_reserved: bool) -> Result<(), SystemError> { let mut lifecycle = self.lifecycle.lock(); if lifecycle.state != SuperBlockLifecycleState::Active { @@ -965,7 +1032,8 @@ impl SuperBlockState { inode: Arc, name: Option, ) -> Result, SystemError> { - let child = inode.metadata()?.inode_id; + let metadata = inode.metadata()?; + let child = metadata.inode_id; let child_generation = inode.inode_generation(); let key = DentryRegistryKey { parent: parent.map(|dentry| dentry.id), @@ -973,6 +1041,20 @@ impl SuperBlockState { child_generation, name: name.clone(), }; + { + let registry = self.dentry_registry.lock(); + if let Some(dentry) = registry.get(&key).and_then(Weak::upgrade) { + if !dentry.is_disconnected() { + return Ok(dentry); + } + } + } + + // Only a registry miss needs to consult or allocate the shared + // fsnotify lifecycle state. Recheck the registry afterwards because a + // concurrent lookup may have installed the same edge meanwhile. + let delete_lifecycle = + self.fsnotify_object_lock(child, child_generation, metadata.nlinks)?; let mut registry = self.dentry_registry.lock(); if let Some(dentry) = registry.get(&key).and_then(Weak::upgrade) { if !dentry.is_disconnected() { @@ -987,6 +1069,9 @@ impl SuperBlockState { inode, registry_child: child, registry_generation: child_generation, + fsnotify_superblock: self.fsnotify_id, + fsnotify_delete_lifecycle: delete_lifecycle, + file_type: metadata.file_type, mount_gate: Mutex::new(()), children_gate: Mutex::new(()), mount_edges: AtomicUsize::new(0), @@ -1126,6 +1211,13 @@ impl SuperBlockState { ); } else { dentry.state.lock().disconnected = true; + if let Some(fuse) = dentry + .inode + .as_any_ref() + .downcast_ref::() + { + let _ = fuse.note_link_removed(false); + } } } } @@ -2637,6 +2729,7 @@ impl MountFS { log::warn!("final superblock eviction drain failed: {:?}", err); sb_state.record_wb_error(err); } + fsnotify::notify_unmount(sb_state.fsnotify_id); self.inner_filesystem.on_umount(); sb_state.finish_shutdown(); } @@ -3117,7 +3210,18 @@ impl MountFSInode { dname: DName, mount_fs: Arc, ) -> Result, SystemError> { - let dentry = VfsDentry::new_anonymous(inner_inode, dname)?; + let metadata = inner_inode.metadata()?; + let delete_lifecycle = mount_fs.super_block_state.fsnotify_object_lock( + metadata.inode_id, + inner_inode.inode_generation(), + metadata.nlinks, + )?; + let dentry = VfsDentry::new_anonymous( + inner_inode, + dname, + mount_fs.super_block_state.fsnotify_id, + delete_lifecycle, + )?; // Anonymous magic-link targets have no directory edge and therefore // cannot be rediscovered by dentry ID. Caching their wrappers would // retain one stale Weak key for every namespace-link traversal. @@ -3373,6 +3477,50 @@ impl MountFSInode { self.dentry.inode.clone() } + /// Stable, metadata-I/O-free identity for fsnotify. + pub(crate) fn fsnotify_target(&self) -> (usize, InodeId, u64, FileType, bool) { + let disconnected = self.dentry.state.lock().disconnected; + ( + self.mount_fs.super_block_state.fsnotify_id, + self.dentry.registry_child, + self.dentry.registry_generation, + self.dentry.file_type, + disconnected, + ) + } + + pub(crate) fn with_fsnotify_watch_lifecycle( + &self, + operation: impl FnOnce(Arc>) -> Result, + ) -> Result { + let lifecycle = self.dentry.fsnotify_delete_lifecycle.clone(); + let state = lifecycle.lock(); + let result = operation(lifecycle.clone()); + let committed = state.committed() && result.is_ok(); + drop(state); + if committed { + let target = self.fsnotify_target(); + fsnotify::notify_object_delete(FsNotifyObjectId { + superblock: target.0, + inode: target.1, + generation: target.2, + }); + } + result + } + + pub(crate) fn with_fsnotify_admission( + &self, + operation: impl FnOnce() -> Result, + ) -> Result { + let state = &self.mount_fs.super_block_state; + let _admission = state.umount_read(); + if state.lifecycle.lock().state != SuperBlockLifecycleState::Active { + return Err(SystemError::ESTALE); + } + operation() + } + /// @brief Wrap a MountFSInode object in an Arc pointer. /// The main purpose of this function is to initialize the self-referencing Weak pointer within the MountFSInode object. /// This function should only be called in constructors. @@ -3546,23 +3694,40 @@ impl MountFSInode { } } - /// 供 fsnotify 内容事件 hook 解析「该 inode 的父目录 + 自身 dentry 名」。 - /// - /// 在 File 构造期(open)调用一次,结果快照存入 File,供 do_write/do_read/Drop/ - /// IN_OPEN 向父目录 watch 投递子项内容事件(IN_MODIFY/ACCESS/OPEN/CLOSE/ATTRIB)。 - /// - 根或断连 dentry(`do_parent` 返回自身)→ None,避免把内容事件误投给自身 watch; - /// - 非 MountFSInode(设备/套接字等)由调用方 downcast 判定,本方法不涉及。 - pub fn fsnotify_parent_and_name(&self) -> Option<(Arc, alloc::string::String)> { - let parent = self.do_parent().ok()?; - // do_parent 对根/无父 dentry 返回自身:必须排除,否则内容事件以 parent=自身 投递, - // fsnotify 用自身 inode_id 反查会把 MODIFY 当作「父目录的子项事件」误投给自身 watch。 - if let Some(self_arc) = self.self_ref.upgrade() { - if Arc::ptr_eq(&parent, &self_arc) { + /// Capture child identity and its current parent/name from dentry state. + /// This avoids path walking and String allocation on read/write/close. + pub(crate) fn fsnotify_snapshot(&self) -> (FsNotifyTarget, Option<(FsNotifyTarget, DName)>) { + let (parent_dentry, name, disconnected) = { + let state = self.dentry.state.lock(); + (state.parent.clone(), state.name.clone(), state.disconnected) + }; + let child = FsNotifyTarget { + id: FsNotifyObjectId { + superblock: self.dentry.fsnotify_superblock, + inode: self.dentry.registry_child, + generation: self.dentry.registry_generation, + }, + is_dir: self.dentry.file_type == FileType::Dir, + disconnected, + }; + let parent = parent_dentry.zip(name).and_then(|(parent, name)| { + if Arc::ptr_eq(&parent, &self.dentry) { return None; } - } - let name = self.dname().ok()?; - Some((parent as Arc, name.0.as_str().to_string())) + Some(( + FsNotifyTarget { + id: FsNotifyObjectId { + superblock: parent.fsnotify_superblock, + inode: parent.registry_child, + generation: parent.registry_generation, + }, + is_dir: parent.file_type == FileType::Dir, + disconnected: false, + }, + name, + )) + }); + (child, parent) } fn do_absolute_path(&self) -> Result { @@ -4115,7 +4280,18 @@ impl IndexNode for MountFSInode { .map(|mnt| mnt.dentry.inode.clone()) .unwrap_or_else(|| other.clone()); - return self.dentry.inode.link(name, &other_inner); + let object_state = other + .clone() + .downcast_arc::() + .map(|inode| inode.dentry.fsnotify_delete_lifecycle.clone()); + let mut delete_lifecycle = object_state.as_ref().map(|state| state.lock()); + let result = self.dentry.inode.link(name, &other_inner); + if result.is_ok() { + if let Some(state) = delete_lifecycle.as_mut() { + fsnotify::note_link_added(state); + } + } + result } fn symlink(&self, name: &str, target: &str) -> Result, SystemError> { @@ -4171,8 +4347,28 @@ impl IndexNode for MountFSInode { { return Err(SystemError::EBUSY); } - // Delegate to the inner inode's unlink method to delete this inode + // Serialize last-link publication against relink and final dentry + // detach for this lifecycle protocol. + let object_state = child + .as_ref() + .map(|child| child.fsnotify_delete_lifecycle.clone()) + .or_else(|| { + self.mount_fs.super_block_state.fsnotify_object_state( + inner.metadata().ok()?.inode_id, + inner.inode_generation(), + ) + }); + let mut delete_lifecycle = object_state.as_ref().map(|state| state.lock()); self.dentry.inode.unlink_with_context(name, context)?; + let last_link = delete_lifecycle + .as_mut() + .is_some_and(|state| fsnotify::note_link_removed(state)); + if let Some(fuse) = inner + .as_any_ref() + .downcast_ref::() + { + let _ = fuse.note_link_removed(false); + } context.ensure_locked(); let _namespace_guard = self .mount_fs @@ -4181,9 +4377,16 @@ impl IndexNode for MountFSInode { .write(); drop(inner); if let Some(child) = child.as_ref() { + if last_link { + fsnotify::mark_delete_pending( + delete_lifecycle + .as_mut() + .expect("last link has lifecycle state"), + ); + } self.mount_fs.super_block_state.disconnect_dentry(child); } - return Ok(()); + Ok(()) } #[inline] @@ -4220,8 +4423,23 @@ impl IndexNode for MountFSInode { { return Err(SystemError::EBUSY); } - // Delegate to the inner inode's rmdir method to delete this inode + let object_state = child + .as_ref() + .map(|child| child.fsnotify_delete_lifecycle.clone()) + .or_else(|| { + self.mount_fs.super_block_state.fsnotify_object_state( + inner.metadata().ok()?.inode_id, + inner.inode_generation(), + ) + }); + let mut delete_lifecycle = object_state.as_ref().map(|state| state.lock()); self.dentry.inode.rmdir_with_context(name, context)?; + if let Some(fuse) = inner + .as_any_ref() + .downcast_ref::() + { + let _ = fuse.note_link_removed(true); + } context.ensure_locked(); let _namespace_guard = self .mount_fs @@ -4230,9 +4448,12 @@ impl IndexNode for MountFSInode { .write(); drop(inner); if let Some(child) = child.as_ref() { + if let Some(state) = delete_lifecycle.as_mut() { + fsnotify::mark_delete_pending(state); + } self.mount_fs.super_block_state.disconnect_dentry(child); } - return Ok(()); + Ok(()) } #[inline] @@ -4310,6 +4531,10 @@ impl IndexNode for MountFSInode { { return Err(SystemError::EBUSY); } + let object_state = target_dentry + .as_ref() + .map(|target| target.fsnotify_delete_lifecycle.clone()); + let mut delete_lifecycle = object_state.as_ref().map(|state| state.lock()); self.dentry.inode.move_to_with_context( old_name, &target_inner, @@ -4317,6 +4542,11 @@ impl IndexNode for MountFSInode { flags, context, )?; + if !flags.contains(RenameFlags::EXCHANGE) { + if let Some(state) = delete_lifecycle.as_mut() { + fsnotify::note_link_removed(state); + } + } context.ensure_locked(); let _namespace_guard = self .mount_fs diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index ef25a07b90..c4d85d8bf9 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -136,7 +136,7 @@ pub fn do_fchmod(inode: Arc, mode: InodeMode) -> Result, uid: usize, gid: usize) -> Result Result = None; - let resolved = match resolved { - Ok(OwnedLookupOutcome::Found(resolved)) => resolved, - Ok(OwnedLookupOutcome::MissingFinal { - parent: parent_resolved, - name: filename, - must_be_dir, - }) => { - // 文件不存在,且需要创建 - if how.o_flags.contains(FileFlags::O_CREAT) - && !how.o_flags.contains(FileFlags::O_DIRECTORY) - { - // A trailing slash may come from the expanded symlink target, - // not only from the original userspace pathname. - if must_be_dir { - return Err(SystemError::EISDIR); - } + // Match Linux's get_unused_fd_flags() ordering: reserve the descriptor + // before pathname lookup and before create/truncate/open side effects. + let cloexec = how.o_flags.contains(FileFlags::O_CLOEXEC); + let fd_table = ProcessManager::current_pcb().fd_table(); + let reservation = fd_table.write().reserve_fd(cloexec)?; + let open_result = (|| -> Result { + let path = path.trim(); + // Linux makes O_CREAT|O_EXCL imply O_NOFOLLOW for the final component. + let follow_symlink = !(how.o_flags.contains(FileFlags::O_NOFOLLOW) + || how.o_flags.contains(FileFlags::O_CREAT) && how.o_flags.contains(FileFlags::O_EXCL)); + // 检查空字符串路径 + if path.is_empty() { + return Err(SystemError::ENOENT); + } - // 检查文件名长度 - if filename.len() > crate::filesystem::vfs::NAME_MAX { - return Err(SystemError::ENAMETOOLONG); - } - let parent_inode = parent_resolved.inode(); - let parent_md = parent_inode.metadata()?; - // 父节点必须是目录 - if parent_md.file_type != FileType::Dir { - return Err(SystemError::ENOTDIR); - } - // Linux 语义:创建文件需要对父目录拥有 W+X(写+搜索)权限 - check_parent_dir_permission_inode(&parent_inode, &parent_md)?; - - // 计算创建 mode:应用 umask,遵循 open/creat 语义 - let pcb = ProcessManager::current_pcb(); - let umask = pcb.fs_struct().umask(); - let create_mode = apply_umask_for_create(how.mode, umask); - // Let filesystems with an atomic create/open operation carry - // the returned handle directly into the new File. ENOSYS - // preserves the generic create-then-open fallback. - let mut create_flags = how.o_flags; - if create_flags.contains(FileFlags::O_EXCL) { - create_flags.remove(FileFlags::O_TRUNC); + // 检查路径末尾斜杠 - 如果以斜杠结尾,目标必须是目录 + let path_ends_with_slash = path.ends_with('/'); + + let (start_path, path) = + user_resolved_path_at(&ProcessManager::current_pcb(), dirfd, path)?; + let inode_begin = start_path.inode(); + let resolved = inode_begin.lookup_follow_symlink_or_missing_owned( + &start_path, + &path, + VFS_MAX_FOLLOW_SYMLINK_TIMES, + follow_symlink, + ); + let mut created = false; + let mut preopened: Option = None; + let resolved = match resolved { + Ok(OwnedLookupOutcome::Found(resolved)) => resolved, + Ok(OwnedLookupOutcome::MissingFinal { + parent: parent_resolved, + name: filename, + must_be_dir, + }) => { + // 文件不存在,且需要创建 + if how.o_flags.contains(FileFlags::O_CREAT) + && !how.o_flags.contains(FileFlags::O_DIRECTORY) + { + // A trailing slash may come from the expanded symlink target, + // not only from the original userspace pathname. + if must_be_dir { + return Err(SystemError::EISDIR); + } + + // 检查文件名长度 + if filename.len() > crate::filesystem::vfs::NAME_MAX { + return Err(SystemError::ENAMETOOLONG); + } + let parent_inode = parent_resolved.inode(); + let parent_md = parent_inode.metadata()?; + // 父节点必须是目录 + if parent_md.file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); + } + // Linux 语义:创建文件需要对父目录拥有 W+X(写+搜索)权限 + check_parent_dir_permission_inode(&parent_inode, &parent_md)?; + + // 计算创建 mode:应用 umask,遵循 open/creat 语义 + let pcb = ProcessManager::current_pcb(); + let umask = pcb.fs_struct().umask(); + let create_mode = apply_umask_for_create(how.mode, umask); + // Let filesystems with an atomic create/open operation carry + // the returned handle directly into the new File. ENOSYS + // preserves the generic create-then-open fallback. + let mut create_flags = how.o_flags; + if create_flags.contains(FileFlags::O_EXCL) { + create_flags.remove(FileFlags::O_TRUNC); + } + let inode: Arc = + match parent_inode.create_and_open(&filename, create_mode, &create_flags) { + Ok(opened) => { + let inode = opened.inode(); + preopened = Some(opened); + inode + } + Err(SystemError::ENOSYS) => { + parent_inode.create(&filename, FileType::File, create_mode)? + } + Err(err) => return Err(err), + }; + // fsnotify:创建成功 → 父目录得 IN_CREATE(子项是普通文件,IN_ISDIR 不置位)。 + fsnotify::fsnotify( + FsEvent::CREATE, + Some((&parent_inode, &filename)), + Some(&inode), + 0, + ); + created = true; + let created_path = ResolvedPath::new(inode)?; + drop(parent_resolved); + created_path + } else { + return Err(SystemError::ENOENT); } - let inode: Arc = - match parent_inode.create_and_open(&filename, create_mode, &create_flags) { - Ok(opened) => { - let inode = opened.inode(); - preopened = Some(opened); - inode - } - Err(SystemError::ENOSYS) => { - parent_inode.create(&filename, FileType::File, create_mode)? - } - Err(err) => return Err(err), - }; - // fsnotify:创建成功 → 父目录得 IN_CREATE(子项是普通文件,IN_ISDIR 不置位)。 - fsnotify::fsnotify( - FsEvent::CREATE, - Some((&parent_inode, &filename)), - Some(&inode), - 0, - ); - created = true; - let created_path = ResolvedPath::new(inode)?; - drop(parent_resolved); - created_path - } else { - return Err(SystemError::ENOENT); } + Err(errno) => return Err(errno), + }; + drop(start_path); + let inode = resolved.inode(); + let metadata = inode.metadata()?; + let file_type: FileType = metadata.file_type; + + if !how.o_flags.contains(FileFlags::O_PATH) + && (file_type == FileType::CharDevice || file_type == FileType::BlockDevice) + && inode.mount_flags().contains(MountFlags::NODEV) + { + return Err(SystemError::EACCES); } - Err(errno) => return Err(errno), - }; - drop(start_path); - let inode = resolved.inode(); - let metadata = inode.metadata()?; - let file_type: FileType = metadata.file_type; - if !how.o_flags.contains(FileFlags::O_PATH) - && (file_type == FileType::CharDevice || file_type == FileType::BlockDevice) - && inode.mount_flags().contains(MountFlags::NODEV) - { - return Err(SystemError::EACCES); - } - - // 如果路径以斜杠结尾,而目标不是目录,返回 ENOTDIR - if path_ends_with_slash && file_type != FileType::Dir { - return Err(SystemError::ENOTDIR); - } - // 已存在的文件且指定了 O_CREAT|O_EXCL - if how.o_flags.contains(FileFlags::O_CREAT) - && how.o_flags.contains(FileFlags::O_EXCL) - && !created - { - return Err(SystemError::EEXIST); - } - if how.o_flags.contains(FileFlags::O_NOFOLLOW) - && !how.o_flags.contains(FileFlags::O_PATH) - && file_type == FileType::SymLink - { - return Err(SystemError::ELOOP); - } - // 对已存在的目录使用 O_CREAT 视为错误 - if how.o_flags.contains(FileFlags::O_CREAT) && !created && file_type == FileType::Dir { - return Err(SystemError::EISDIR); - } - // 目录相关检查 - if file_type == FileType::Dir { - // 目录上不支持 O_TRUNC - if how.o_flags.contains(FileFlags::O_TRUNC) { - return Err(SystemError::EISDIR); + // 如果路径以斜杠结尾,而目标不是目录,返回 ENOTDIR + if path_ends_with_slash && file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); } - // 目录上不允许写访问 - let acc_mode = how.o_flags.access_flags(); - if acc_mode == FileFlags::O_WRONLY || acc_mode == FileFlags::O_RDWR { - return Err(SystemError::EISDIR); + // 已存在的文件且指定了 O_CREAT|O_EXCL + if how.o_flags.contains(FileFlags::O_CREAT) + && how.o_flags.contains(FileFlags::O_EXCL) + && !created + { + return Err(SystemError::EEXIST); } - } - // 非 O_PATH 需要检查访问权限(read/write/truncate) - // Linux 语义:若本次 open() 触发了创建,则不应因“新 inode 的 mode”而拒绝 - // 当前这次 open() 的访问模式;权限在“后续 reopen()”时生效。 - if !how.o_flags.contains(FileFlags::O_PATH) && !created { - let acc_mode = how.o_flags.access_flags(); - let mut need = PermissionMask::empty(); - match acc_mode { - FileFlags::O_RDONLY => need.insert(PermissionMask::MAY_READ), - FileFlags::O_WRONLY => need.insert(PermissionMask::MAY_WRITE), - FileFlags::O_RDWR => need.insert(PermissionMask::MAY_READ | PermissionMask::MAY_WRITE), - _ => {} + if how.o_flags.contains(FileFlags::O_NOFOLLOW) + && !how.o_flags.contains(FileFlags::O_PATH) + && file_type == FileType::SymLink + { + return Err(SystemError::ELOOP); } - if how.o_flags.contains(FileFlags::O_TRUNC) { - need.insert(PermissionMask::MAY_WRITE); + // 对已存在的目录使用 O_CREAT 视为错误 + if how.o_flags.contains(FileFlags::O_CREAT) && !created && file_type == FileType::Dir { + return Err(SystemError::EISDIR); } - if !need.is_empty() { - super::permission::check_inode_permission(&inode, &metadata, need)?; + // 目录相关检查 + if file_type == FileType::Dir { + // 目录上不支持 O_TRUNC + if how.o_flags.contains(FileFlags::O_TRUNC) { + return Err(SystemError::EISDIR); + } + // 目录上不允许写访问 + let acc_mode = how.o_flags.access_flags(); + if acc_mode == FileFlags::O_WRONLY || acc_mode == FileFlags::O_RDWR { + return Err(SystemError::EISDIR); + } + } + // 非 O_PATH 需要检查访问权限(read/write/truncate) + // Linux 语义:若本次 open() 触发了创建,则不应因“新 inode 的 mode”而拒绝 + // 当前这次 open() 的访问模式;权限在“后续 reopen()”时生效。 + if !how.o_flags.contains(FileFlags::O_PATH) && !created { + let acc_mode = how.o_flags.access_flags(); + let mut need = PermissionMask::empty(); + match acc_mode { + FileFlags::O_RDONLY => need.insert(PermissionMask::MAY_READ), + FileFlags::O_WRONLY => need.insert(PermissionMask::MAY_WRITE), + FileFlags::O_RDWR => { + need.insert(PermissionMask::MAY_READ | PermissionMask::MAY_WRITE) + } + _ => {} + } + if how.o_flags.contains(FileFlags::O_TRUNC) { + need.insert(PermissionMask::MAY_WRITE); + } + if !need.is_empty() { + super::permission::check_inode_permission(&inode, &metadata, need)?; + } } - } - // 如果要打开的是文件夹,而目标不是文件夹 - if how.o_flags.contains(FileFlags::O_DIRECTORY) && file_type != FileType::Dir { - return Err(SystemError::ENOTDIR); - } + // 如果要打开的是文件夹,而目标不是文件夹 + if how.o_flags.contains(FileFlags::O_DIRECTORY) && file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); + } - // Linux resolves path/file-type and ordinary DAC errors before applying - // O_NOATIME's owner/CAP_FOWNER restriction. In particular, O_NOFOLLOW on - // a final symlink must report ELOOP rather than an ownership-based EPERM. - if how.o_flags.contains(FileFlags::O_NOATIME) { - super::permission::check_noatime_permission(&metadata)?; - } + // Linux resolves path/file-type and ordinary DAC errors before applying + // O_NOATIME's owner/CAP_FOWNER restriction. In particular, O_NOFOLLOW on + // a final symlink must report ELOOP rather than an ownership-based EPERM. + if how.o_flags.contains(FileFlags::O_NOATIME) { + super::permission::check_noatime_permission(&metadata)?; + } - // Linux rejects unsupported direct I/O from do_dentry_open(), after - // may_open() has enforced O_NOATIME ownership. - if file_type == FileType::Dir && how.o_flags.contains(FileFlags::O_DIRECT) { - return Err(SystemError::EINVAL); - } + // Linux rejects unsupported direct I/O from do_dentry_open(), after + // may_open() has enforced O_NOATIME ownership. + if file_type == FileType::Dir && how.o_flags.contains(FileFlags::O_DIRECT) { + return Err(SystemError::EINVAL); + } - // Linux reaches the socket file operation only after DAC and O_NOATIME - // checks; sock_no_open() then rejects pathname-based opens with ENXIO. - if file_type == FileType::Socket { - return Err(SystemError::ENXIO); - } + // Linux reaches the socket file operation only after DAC and O_NOATIME + // checks; sock_no_open() then rejects pathname-based opens with ENXIO. + if file_type == FileType::Socket { + return Err(SystemError::ENXIO); + } - // 如果O_TRUNC,并且是普通文件,清空文件 - // 注意:必须在创建 File 对象之前截断 - // 因为 O_TRUNC 的截断基于文件系统权限,而不是打开模式 - // 例如:open(file, O_RDONLY | O_TRUNC) 是合法的,只要用户对文件有写权限 - if file_type == FileType::File && inode.truncate_before_open(&how.o_flags) { - vfs_truncate(inode.clone(), 0)?; - } - let (inode, mount_guard, operation_guard) = resolved.into_parts(); - let file: File = match preopened { - Some(opened) => { - File::new_preopened_with_mount_guard(opened, how.o_flags, mount_guard, operation_guard)? + // 如果O_TRUNC,并且是普通文件,清空文件 + // 注意:必须在创建 File 对象之前截断 + // 因为 O_TRUNC 的截断基于文件系统权限,而不是打开模式 + // 例如:open(file, O_RDONLY | O_TRUNC) 是合法的,只要用户对文件有写权限 + if file_type == FileType::File && inode.truncate_before_open(&how.o_flags) { + vfs_truncate(inode.clone(), 0)?; + } + let (inode, mount_guard, operation_guard) = resolved.into_parts(); + let file: File = match preopened { + Some(opened) => File::new_preopened_with_mount_guard( + opened, + how.o_flags, + mount_guard, + operation_guard, + )?, + None => File::new_with_mount_guard(inode, how.o_flags, mount_guard, operation_guard)?, + }; + Ok(file) + })(); + + let file = match open_result { + Ok(file) => file, + Err(error) => { + fd_table.write().release_reserved_fd(reservation); + return Err(error); } - None => File::new_with_mount_guard(inode, how.o_flags, mount_guard, operation_guard)?, }; - let cloexec = how.o_flags.contains(FileFlags::O_CLOEXEC); let opened_inode = file.inode(); - - // 把文件对象存入pcb - let r = ProcessManager::current_pcb() - .fd_table() + let r = fd_table .write() - .alloc_fd(file, None, cloexec) + .install_reserved_fd(reservation, file) .map(|fd| fd as usize); // fsnotify:打开成功 → 被打开 inode 得 IN_OPEN。 - if r.is_ok() { + if r.is_ok() && !how.o_flags.contains(FileFlags::O_PATH) { // 同时通知父目录 watch(带子名,issue B)与被打开 inode 自身 watch。 if fsnotify::has_any_watch() { - let parent = opened_inode - .clone() - .downcast_arc::() - .and_then(|mnt| mnt.fsnotify_parent_and_name()); - match parent { - Some((p, n)) => fsnotify::fsnotify( - FsEvent::OPEN, - Some((&p, n.as_str())), - Some(&opened_inode), - 0, - ), + match opened_inode.clone().downcast_arc::() { + Some(mounted) => { + let (child, parent) = mounted.fsnotify_snapshot(); + if let Some((parent, name)) = parent.as_ref() { + fsnotify::fsnotify_targets( + FsEvent::OPEN, + Some((parent, name.0.as_str())), + Some(&child), + 0, + true, + ); + } else { + fsnotify::fsnotify_targets(FsEvent::OPEN, None, Some(&child), 0, true); + } + } None => fsnotify::fsnotify(FsEvent::OPEN, None, Some(&opened_inode), 0), } } else { @@ -513,7 +535,7 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result 1),覆盖一个链接后 inode 仍存活。 + fsnotify::fsnotify(FsEvent::MOVE_SELF, None, Some(&old_inode), 0); + // Replacing a target is part of the rename pair, not a parent DELETE. + // Linux reports ATTRIB on the displaced inode; DELETE_SELF is tied to + // the later dentry/inode detach lifecycle. if let Some(displaced) = &displaced { - // 容错:metadata 读失败(如 FUSE)不影响 rename 的成功返回值; - // 失败时按「不同 inode + nlinks 未知」处理(保守发 DELETE_SELF)。 - let same_inode = displaced - .metadata() - .ok() - .zip(old_inode.metadata().ok()) - .map(|(d, o)| d.inode_id == o.inode_id) - .unwrap_or(false); - let mut mask = FsEvent::DELETE; - if !same_inode && displaced_nlinks.is_none_or(|n| n <= 1) { - mask |= FsEvent::DELETE_SELF; - } - fsnotify::fsnotify( - mask, - Some((&new_parent_inode, new_filename)), - Some(displaced), - 0, - ); + fsnotify::fsnotify(FsEvent::ATTRIB, None, Some(displaced), 0); } } return Ok(0); diff --git a/kernel/src/filesystem/vfs/vcore.rs b/kernel/src/filesystem/vfs/vcore.rs index ab0faffcd2..e224b2f73e 100644 --- a/kernel/src/filesystem/vfs/vcore.rs +++ b/kernel/src/filesystem/vfs/vcore.rs @@ -680,9 +680,10 @@ pub fn do_remove_dir(dirfd: i32, path: &str) -> Result { // 删除文件夹 parent_inode.rmdir(filename)?; - // fsnotify:父目录得 IN_DELETE(子项是目录→IN_ISDIR),子项得 IN_DELETE_SELF+IN_IGNORED。 + // DELETE_SELF is emitted later when the disconnected dentry finally + // detaches from the inode. fsnotify::fsnotify( - FsEvent::DELETE | FsEvent::DELETE_SELF, + FsEvent::DELETE, Some((&parent_inode, filename)), Some(&target_inode), 0, @@ -726,26 +727,17 @@ pub fn do_unlink_at(dirfd: i32, path: &str) -> Result { return Err(SystemError::EISDIR); } - // 缓存 unlink 前的 nlinks——unlink 后 FUSE daemon 的 GETATTR 可能失败或返回过时值。 - // 用 unlink 前的缓存值判断是否为最后一个链接:nlinks_before <= 1 则 inode 被销毁。 - let nlinks_before = target_inode.metadata().map(|m| m.nlinks).unwrap_or(1); - // 在父目录上执行 unlink 操作 parent_inode.unlink(filename)?; - // Linux 语义:unlink 只删除目录项本身;只有当该 inode 的硬链接数(nlinks)归零时 - // inode 才真正被销毁,此时才发 IN_DELETE_SELF(随后 mark 被撤销并投递 IN_IGNORED)。 - // 存在硬链接时 inode 仍然存活,不应误撤销其上的 watch。 - let mut mask = FsEvent::DELETE; - if nlinks_before <= 1 { - mask |= FsEvent::DELETE_SELF; - } + // Linux publishes the link-count ATTRIB before the parent DELETE record. + // DELETE_SELF remains deferred to final dentry detach. + fsnotify::fsnotify(FsEvent::ATTRIB, None, Some(&target_inode), 0); fsnotify::fsnotify( - mask, + FsEvent::DELETE, Some((&parent_inode, filename)), Some(&target_inode), 0, ); - return Ok(0); } @@ -816,8 +808,7 @@ where let (md, mask) = prepare_write_side_effect_metadata(md, len); let r = do_resize(&inode, &md, mask); if r.is_ok() { - // fsnotify:截断改变数据与元数据 → IN_MODIFY + IN_ATTRIB。 - fsnotify::fsnotify(FsEvent::MODIFY | FsEvent::ATTRIB, None, Some(&inode), 0); + fsnotify::fsnotify_inode(FsEvent::MODIFY, &inode); } r } @@ -1032,9 +1023,9 @@ pub fn vfs_fallocate_file( current_file_lock_owner_id(), file.private_data.lock(), ); - // fsnotify:写类 fallocate(非 KEEP_SIZE-only)改变数据 → IN_MODIFY。 - if r.is_ok() && (mode as u32) != FALLOC_FL_KEEP_SIZE { - fsnotify::fsnotify(FsEvent::MODIFY, None, Some(&inode), 0); + // Linux reports successful fallocate, including KEEP_SIZE, as MODIFY. + if r.is_ok() { + file.notify_fs_event(FsEvent::MODIFY); } r } diff --git a/user/apps/tests/dunitest/suites/normal/inotify_events.cc b/user/apps/tests/dunitest/suites/normal/inotify_events.cc index 02bf95d2b1..f41141ea5e 100644 --- a/user/apps/tests/dunitest/suites/normal/inotify_events.cc +++ b/user/apps/tests/dunitest/suites/normal/inotify_events.cc @@ -215,6 +215,132 @@ TEST(InotifySelfEvents, UnlinkWatchedFileDeliversDeleteSelfAndIgnored) { close(ifd); } +TEST(InotifySelfEvents, OpenUnlinkDefersDeleteSelfUntilClose) { + const std::string path = "/tmp/dunitest_inotify_delself_open"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, path.c_str(), IN_ATTRIB | IN_MODIFY | IN_DELETE_SELF), 0); + + ASSERT_EQ(unlink(path.c_str()), 0); + ASSERT_EQ(write(fd, "x", 1), 1); + auto before_close = drain_events(ifd); + EXPECT_TRUE(saw_self(before_close, IN_ATTRIB)); + EXPECT_TRUE(saw_self(before_close, IN_MODIFY)); + EXPECT_FALSE(saw_self(before_close, IN_DELETE_SELF)); + EXPECT_FALSE(saw_self(before_close, IN_IGNORED)); + + ASSERT_EQ(close(fd), 0); + auto after_close = drain_events(ifd); + EXPECT_TRUE(saw_self(after_close, IN_DELETE_SELF)); + EXPECT_TRUE(saw_self(after_close, IN_IGNORED)); + close(ifd); +} + +TEST(InotifySelfEvents, HardLinkCreatedBeforeWatchKeepsWatchAlive) { + const std::string first = "/tmp/dunitest_inotify_link_watch_a"; + const std::string second = "/tmp/dunitest_inotify_link_watch_b"; + int fd = open(first.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + ASSERT_EQ(link(first.c_str(), second.c_str()), 0); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, first.c_str(), IN_DELETE_SELF), 0); + + ASSERT_EQ(unlink(first.c_str()), 0); + auto after_first = drain_events(ifd); + EXPECT_FALSE(saw_self(after_first, IN_DELETE_SELF)); + EXPECT_FALSE(saw_self(after_first, IN_IGNORED)); + + ASSERT_EQ(unlink(second.c_str()), 0); + auto after_second = drain_events(ifd); + EXPECT_TRUE(saw_self(after_second, IN_DELETE_SELF)); + EXPECT_TRUE(saw_self(after_second, IN_IGNORED)); + close(ifd); +} + +TEST(InotifySelfEvents, WatchUnlinkedOpenFileThroughProcFd) { + const std::string path = "/tmp/dunitest_inotify_procfd_unlinked"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + ASSERT_EQ(unlink(path.c_str()), 0); + + char procfd[64]; + snprintf(procfd, sizeof(procfd), "/proc/self/fd/%d", fd); + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, procfd, IN_DELETE_SELF), 0) + << "watching unlinked proc fd: " << strerror(errno); + + ASSERT_EQ(close(fd), 0); + auto evs = drain_events(ifd); + EXPECT_TRUE(saw_self(evs, IN_DELETE_SELF)); + EXPECT_TRUE(saw_self(evs, IN_IGNORED)); + close(ifd); +} + +TEST(InotifyExcludeUnlinked, FtruncateRemainsADentryEvent) { + const std::string path = "/tmp/dunitest_inotify_excl_ftruncate"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, path.c_str(), IN_MODIFY | IN_EXCL_UNLINK), 0); + + ASSERT_EQ(unlink(path.c_str()), 0); + ASSERT_EQ(ftruncate(fd, 4096), 0); + auto evs = drain_events(ifd); + EXPECT_TRUE(saw_self(evs, IN_MODIFY)); + + close(fd); + close(ifd); +} + +TEST(InotifyRenameEvents, SameInodeAliasIsNoOp) { + const std::string first = "/tmp/dunitest_inotify_alias_a"; + const std::string second = "/tmp/dunitest_inotify_alias_b"; + int fd = open(first.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + ASSERT_EQ(link(first.c_str(), second.c_str()), 0); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, "/tmp", IN_MOVED_FROM | IN_MOVED_TO), 0); + ASSERT_EQ(rename(first.c_str(), second.c_str()), 0); + auto evs = drain_events(ifd); + EXPECT_FALSE(saw(evs, IN_MOVED_FROM, "dunitest_inotify_alias_a")); + EXPECT_FALSE(saw(evs, IN_MOVED_TO, "dunitest_inotify_alias_b")); + + close(ifd); + unlink(first.c_str()); + unlink(second.c_str()); +} + +TEST(InotifyOpenEvents, OPathProducesNoOpenOrClose) { + const std::string path = "/tmp/dunitest_inotify_opath"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, path.c_str(), IN_OPEN | IN_CLOSE), 0); + + int pathfd = open(path.c_str(), O_PATH); + ASSERT_GE(pathfd, 0); + close(pathfd); + auto evs = drain_events(ifd); + EXPECT_FALSE(saw_self(evs, IN_OPEN)); + EXPECT_FALSE(saw_self(evs, IN_CLOSE_WRITE)); + EXPECT_FALSE(saw_self(evs, IN_CLOSE_NOWRITE)); + + close(ifd); + unlink(path.c_str()); +} + // --------------------------------------------------------------------------- // IN_MOVE_SELF: renaming a watched file delivers IN_MOVE_SELF. // --------------------------------------------------------------------------- From 9115c6b3af3e8c64970733c418ea9a14d7e58090 Mon Sep 17 00:00:00 2001 From: longjin Date: Thu, 20 Aug 2026 08:33:27 +0000 Subject: [PATCH 3/3] fix(fsnotify): address follow-up semantic reviews Close lifecycle epoch races when final deletion notifications overlap relink and keep FUSE atomic-truncate cache invalidation atomic. Match Linux mount-boundary rename handling, lookup error precedence, no-op ordering, open and truncate event order, exec open notification, EXCL_UNLINK data kinds, and chown ABI/symlink semantics. Extend inotify and FUSE regression coverage for bind mounts, rename precedence, O_TRUNC, exec, chown flags and symlinks, event merging, and cache refresh. Signed-off-by: longjin --- kernel/src/filesystem/fsnotify/mod.rs | 4 +- kernel/src/filesystem/fuse/inode/file.rs | 9 + kernel/src/filesystem/fuse/inode/vfs.rs | 2 +- kernel/src/filesystem/overlayfs/inode.rs | 2 +- kernel/src/filesystem/vfs/file.rs | 11 + kernel/src/filesystem/vfs/mod.rs | 5 +- kernel/src/filesystem/vfs/mount/mod.rs | 29 +- kernel/src/filesystem/vfs/open.rs | 118 +++---- .../filesystem/vfs/syscall/rename_utils.rs | 92 +++--- .../filesystem/vfs/syscall/sys_fchownat.rs | 4 + .../dunitest/suites/fuse/fuse_extended.cc | 53 ++- .../suites/fuse/fuse_test_simplefs_local.h | 3 + .../dunitest/suites/normal/inotify_events.cc | 302 ++++++++++++++++++ 13 files changed, 512 insertions(+), 122 deletions(-) diff --git a/kernel/src/filesystem/fsnotify/mod.rs b/kernel/src/filesystem/fsnotify/mod.rs index ba0cffa21d..ab6a0324ab 100644 --- a/kernel/src/filesystem/fsnotify/mod.rs +++ b/kernel/src/filesystem/fsnotify/mod.rs @@ -521,7 +521,9 @@ pub(crate) fn fsnotify_targets( if !mask_matches { continue; } - // IN_EXCL_UNLINK:仅对子项的「内容类」事件、且子项已 unlink 时抑制。 + // IN_EXCL_UNLINK: suppress path-data content events for an + // unlinked dentry on both parent and direct inode marks. Dentry + // data events such as ftruncate remain visible, matching Linux. if mark.excl_unlink.load(Ordering::Relaxed) && path_event && routed.intersects(content_type) diff --git a/kernel/src/filesystem/fuse/inode/file.rs b/kernel/src/filesystem/fuse/inode/file.rs index de9c0b5884..ee4fb68236 100644 --- a/kernel/src/filesystem/fuse/inode/file.rs +++ b/kernel/src/filesystem/fuse/inode/file.rs @@ -1210,8 +1210,17 @@ impl FuseNode { let mut guard = self.cached_metadata.lock(); if let Some(md) = guard.as_mut() { md.size = 0; + let now = PosixTimeSpec::now(); + md.mtime = now; + md.ctime = now; self.bump_attr_version(); } + // The daemon owns the atomic truncate and any negotiated killpriv + // transition. Force the next attribute read to observe its mode + // and timestamps instead of retaining the pre-open TTL snapshot. + self.cached_metadata_deadline_ticks + .store(0, Ordering::Release); + drop(guard); } else if (fopen_flags & FOPEN_KEEP_CACHE) == 0 { self.invalidate_clean_page_cache()?; } diff --git a/kernel/src/filesystem/fuse/inode/vfs.rs b/kernel/src/filesystem/fuse/inode/vfs.rs index af9f585539..45cba3ac82 100644 --- a/kernel/src/filesystem/fuse/inode/vfs.rs +++ b/kernel/src/filesystem/fuse/inode/vfs.rs @@ -249,7 +249,7 @@ impl IndexNode for FuseNode { } } - fn truncate_before_open(&self, flags: &FileFlags) -> bool { + fn requires_separate_open_truncate(&self, flags: &FileFlags) -> bool { flags.contains(FileFlags::O_TRUNC) && !self .conn diff --git a/kernel/src/filesystem/overlayfs/inode.rs b/kernel/src/filesystem/overlayfs/inode.rs index 69c3f22251..2bfcee3ac9 100644 --- a/kernel/src/filesystem/overlayfs/inode.rs +++ b/kernel/src/filesystem/overlayfs/inode.rs @@ -325,7 +325,7 @@ impl IndexNode for OvlInode { file::open(self, data, flags) } - fn truncate_before_open(&self, _flags: &FileFlags) -> bool { + fn requires_separate_open_truncate(&self, _flags: &FileFlags) -> bool { false } diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 6796346ac5..f9d640c883 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1312,6 +1312,17 @@ impl File { } } + /// Notify a successful userspace-visible open. Callers decide which File + /// constructions represent VFS open/exec rather than internal kernel I/O. + pub(crate) fn notify_open_event(&self) { + let mode = *self.mode.read(); + if !mode.intersects(FileMode::FMODE_NONOTIFY | FileMode::FMODE_PATH) + && fsnotify::has_any_watch() + { + self.notify_fs_event(FsEvent::OPEN); + } + } + /// Create a file object for sockets created by socket syscalls. /// /// These should not be subject to open(2) pathname semantics. diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 386c949b69..a5185250fd 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -547,7 +547,10 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { !self.is_stream() } - fn truncate_before_open(&self, flags: &FileFlags) -> bool { + /// Whether VFS must issue a separate truncate after a successful open. + /// `false` means the filesystem's open operation completed O_TRUNC + /// atomically and VFS must only publish the resulting metadata event. + fn requires_separate_open_truncate(&self, flags: &FileFlags) -> bool { flags.contains(FileFlags::O_TRUNC) } diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index cca9ce9f9a..9c6ee3e192 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -3129,8 +3129,12 @@ impl Drop for MountSnapshotGuard { } impl MountFSInode { + pub(crate) fn same_mount_ref(&self, other: &MountFSInode) -> bool { + Arc::ptr_eq(&self.mount_fs, &other.mount_fs) + } + pub(crate) fn same_path_ref(&self, other: &MountFSInode) -> bool { - Arc::ptr_eq(&self.mount_fs, &other.mount_fs) && self.dentry.id == other.dentry.id + self.same_mount_ref(other) && self.dentry.id == other.dentry.id } pub(crate) fn is_disconnected(&self) -> bool { @@ -3494,17 +3498,18 @@ impl MountFSInode { operation: impl FnOnce(Arc>) -> Result, ) -> Result { let lifecycle = self.dentry.fsnotify_delete_lifecycle.clone(); + let object_id = FsNotifyObjectId { + superblock: self.dentry.fsnotify_superblock, + inode: self.dentry.registry_child, + generation: self.dentry.registry_generation, + }; let state = lifecycle.lock(); let result = operation(lifecycle.clone()); - let committed = state.committed() && result.is_ok(); - drop(state); - if committed { - let target = self.fsnotify_target(); - fsnotify::notify_object_delete(FsNotifyObjectId { - superblock: target.0, - inode: target.1, - generation: target.2, - }); + if state.committed() && result.is_ok() { + // Keep the committed check and deletion notification in the same + // object lifecycle epoch. A concurrent linkat(AT_EMPTY_PATH) + // cannot relink the inode and publish a new watch between them. + fsnotify::notify_object_delete(object_id); } result } @@ -3938,8 +3943,8 @@ impl IndexNode for MountFSInode { .mmap_file(file, start, len, offset, vm_flags) } - fn truncate_before_open(&self, flags: &FileFlags) -> bool { - self.dentry.inode.truncate_before_open(flags) + fn requires_separate_open_truncate(&self, flags: &FileFlags) -> bool { + self.dentry.inode.requires_separate_open_truncate(flags) } fn sync(&self) -> Result<(), SystemError> { diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index c4d85d8bf9..ba5aa9d25c 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -1,19 +1,18 @@ use crate::filesystem::fsnotify::{self, FsEvent}; -use crate::libs::casting::DowncastArc; use alloc::sync::Arc; use system_error::SystemError; use super::{ fcntl::AtFlags, file::{File, FileFlags, PreopenedFile}, - mount::{MountFSInode, MountFlags}, + mount::MountFlags, permission::PermissionMask, syscall::{OpenHow, OpenHowResolve}, utils::{ should_remove_sgid_on_chown, user_path_at, user_resolved_path_at, OwnedLookupOutcome, ResolvedPath, }, - vcore::{check_parent_dir_permission_inode, vfs_truncate}, + vcore::{check_parent_dir_permission_inode, current_file_lock_owner_id, vfs_truncate_file}, FileType, FsPermissionPolicy, IndexNode, InodeMode, SetMetadataMask, MAX_PATHLEN, VFS_MAX_FOLLOW_SYMLINK_TIMES, }; @@ -148,34 +147,28 @@ pub fn do_fchownat( flag: AtFlags, ) -> Result { // 检查flag是否合法 - if flag.contains(!(AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH)) { + let allowed_flags = AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH; + if flag.intersects(!allowed_flags) { return Err(SystemError::EINVAL); } - let follow_symlink = flag.contains(!AtFlags::AT_SYMLINK_NOFOLLOW); + let follow_symlink = !flag.contains(AtFlags::AT_SYMLINK_NOFOLLOW); let (inode, path) = user_path_at(&ProcessManager::current_pcb(), dirfd, path)?; - // 如果找不到文件,则返回错误码ENOENT let inode = if follow_symlink { - inode.lookup_follow_symlink2(path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES, false) + inode.lookup_follow_symlink(path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES) } else { - inode.lookup(path.as_str()) - }; - - if inode.is_err() { - let errno = inode.clone().unwrap_err(); - // 文件不存在 - if errno == SystemError::ENOENT { - return Err(SystemError::ENOENT); - } - } - - let inode = inode.unwrap(); + inode.lookup_follow_symlink2(path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES, false) + }?; return chown_common(inode, uid, gid); } fn chown_common(inode: Arc, uid: usize, gid: usize) -> Result { + // The syscall ABI declares these arguments as 32-bit uid_t/gid_t. Truncate + // register-width inputs before interpreting (uid_t)-1 as "no change". + let uid = uid as u32 as usize; + let gid = gid as u32 as usize; let mut meta = inode.metadata()?; let cred = ProcessManager::current_pcb().cred(); let current_uid = cred.uid.data(); @@ -185,11 +178,10 @@ fn chown_common(inode: Arc, uid: usize, gid: usize) -> Result, uid: usize, gid: usize) -> Result, uid: usize, gid: usize) -> Result Result File::new_preopened_with_mount_guard( @@ -494,6 +485,22 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result File::new_with_mount_guard(inode, how.o_flags, mount_guard, operation_guard)?, }; + + // Linux emits OPEN from do_dentry_open() before handle_truncate(). + // Filesystems with atomic O_TRUNC have already completed the resize by + // this point, but the externally visible events retain that ordering. + file.notify_open_event(); + if do_truncate { + if truncate_in_vfs { + vfs_truncate_file(file.inode(), 0, current_file_lock_owner_id(), || { + file.private_data.lock() + })?; + } else { + // open(O_TRUNC) metadata notification is a dentry-data event, + // unlike read/write path-data events subject to EXCL_UNLINK. + fsnotify::fsnotify_inode(FsEvent::MODIFY, &file.inode()); + } + } Ok(file) })(); @@ -504,38 +511,11 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result() { - Some(mounted) => { - let (child, parent) = mounted.fsnotify_snapshot(); - if let Some((parent, name)) = parent.as_ref() { - fsnotify::fsnotify_targets( - FsEvent::OPEN, - Some((parent, name.0.as_str())), - Some(&child), - 0, - true, - ); - } else { - fsnotify::fsnotify_targets(FsEvent::OPEN, None, Some(&child), 0, true); - } - } - None => fsnotify::fsnotify(FsEvent::OPEN, None, Some(&opened_inode), 0), - } - } else { - fsnotify::fsnotify(FsEvent::OPEN, None, Some(&opened_inode), 0); - } - } - - r + result } /// 为exec打开可执行文件 @@ -602,10 +582,10 @@ pub fn do_open_execat_with_flags( } super::permission::check_inode_permission(&inode, &metadata, PermissionMask::MAY_EXEC)?; - super::permission::check_inode_permission(&inode, &metadata, PermissionMask::MAY_READ)?; // 创建File对象,使用O_RDONLY | O_CLOEXEC let file = File::new(inode, FileFlags::O_RDONLY | FileFlags::O_CLOEXEC)?; + file.notify_open_event(); Ok(Arc::new(file)) } diff --git a/kernel/src/filesystem/vfs/syscall/rename_utils.rs b/kernel/src/filesystem/vfs/syscall/rename_utils.rs index d6f37c2c26..631f119459 100644 --- a/kernel/src/filesystem/vfs/syscall/rename_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/rename_utils.rs @@ -1,4 +1,5 @@ use crate::filesystem::fsnotify::{self, FsEvent}; +use crate::filesystem::vfs::mount::MountFSInode; use crate::filesystem::vfs::permission::PermissionMask; use crate::filesystem::vfs::syscall::RenameFlags; use crate::filesystem::vfs::utils::is_ancestor; @@ -7,6 +8,7 @@ use crate::filesystem::vfs::utils::user_path_at; use crate::filesystem::vfs::SystemError; use crate::filesystem::vfs::VFS_MAX_FOLLOW_SYMLINK_TIMES; use crate::filesystem::vfs::{MAX_PATHLEN, NAME_MAX}; +use crate::libs::casting::DowncastArc; use crate::process::ProcessManager; use crate::syscall::user_access::vfs_check_and_clone_cstr; use alloc::sync::Arc; @@ -68,6 +70,18 @@ pub fn do_renameat2( Some(p) => new_inode_begin.lookup_follow_symlink(p, VFS_MAX_FOLLOW_SYMLINK_TIMES)?, }; + // Linux rejects rename across mount objects even when two bind mounts + // expose the same superblock and inode. Check this before final lookup and + // same-inode no-op handling so a cross-mount alias cannot hide EXDEV. + if let (Some(old_mount), Some(new_mount)) = ( + old_parent_inode.clone().downcast_arc::(), + new_parent_inode.clone().downcast_arc::(), + ) { + if !old_mount.same_mount_ref(&new_mount) { + return Err(SystemError::EXDEV); + } + } + // 检查单个文件名长度 if old_filename.len() > NAME_MAX || new_filename.len() > NAME_MAX { return Err(SystemError::ENAMETOOLONG); @@ -77,23 +91,12 @@ pub fn do_renameat2( return Err(SystemError::EEXIST); } - // RENAME_EXCHANGE: 目标必须存在 - if flags.contains(RenameFlags::EXCHANGE) && new_parent_inode.find(new_filename).is_err() { - return Err(SystemError::ENOENT); - } - if old_filename == "." || old_filename == ".." || new_filename == "." || new_filename == ".." { return Err(SystemError::EBUSY); } let old_inode = old_parent_inode.lookup(old_filename)?; let old_inode_type = old_inode.metadata()?.file_type; - if old_inode_type == crate::filesystem::vfs::FileType::Dir { - // 仅当把目录移动到其自身或其子树下时拦截 - if is_ancestor(&old_inode, &new_parent_inode) { - return Err(SystemError::EINVAL); - } - } // RENAME_EXCHANGE 目标必须存在;预先 lookup 供事件投递复用(move_to 后原位置查不到)。 let exchange_new_inode = if flags.contains(RenameFlags::EXCHANGE) { @@ -114,6 +117,48 @@ pub fn do_renameat2( } } + // 非 EXCHANGE:预先取出可能被覆盖的目标 inode(move_to 会静默销毁它), + // 否则其上的 watch 会沦为持续产生事件的「幽灵 watch」。只有 ENOENT + // 表示目标不存在;I/O、权限等 lookup 错误必须原样返回。 + let displaced = if !flags.contains(RenameFlags::EXCHANGE) { + match new_parent_inode.find(new_filename) { + Ok(inode) => Some(inode), + Err(SystemError::ENOENT) => None, + Err(error) => return Err(error), + } + } else { + None + }; + + // Linux resolves the destination and handles NOREPLACE/same-inode no-op + // before checking directory mutation permissions. + if flags.contains(RenameFlags::NOREPLACE) && displaced.is_some() { + return Err(SystemError::EEXIST); + } + + if !flags.contains(RenameFlags::EXCHANGE) + && Arc::ptr_eq(&old_parent_inode, &new_parent_inode) + && old_filename == new_filename + { + return Ok(0); + } + + if let Some(target) = displaced.as_ref() { + let source_id = fsnotify::target_for_inode(&old_inode)?.id; + let target_id = fsnotify::target_for_inode(target)?.id; + if source_id == target_id { + return Ok(0); + } + } + + // Ancestor traps are evaluated after a positive NOREPLACE destination and + // same-inode no-op, matching Linux's lookup/error precedence. + if old_inode_type == crate::filesystem::vfs::FileType::Dir + && is_ancestor(&old_inode, &new_parent_inode) + { + return Err(SystemError::EINVAL); + } + // 不要在这里检查 new_parent 是否是 old 的祖先: // 这会把同目录/向上移动的合法情况误判为 ENOTEMPTY。 // 非空目录覆盖应由具体文件系统在 move_to/rename 实现中返回 ENOTEMPTY。 @@ -133,31 +178,6 @@ pub fn do_renameat2( PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC, )?; - // 非 EXCHANGE:预先取出可能被覆盖的目标 inode(move_to 会静默销毁它), - // 否则其上的 watch 会沦为持续产生事件的「幽灵 watch」。 - let displaced = if !flags.contains(RenameFlags::EXCHANGE) { - new_parent_inode.find(new_filename).ok() - } else { - None - }; - - if !flags.contains(RenameFlags::EXCHANGE) - && Arc::ptr_eq(&old_parent_inode, &new_parent_inode) - && old_filename == new_filename - { - return Ok(0); - } - - // Linux treats rename onto another hard-link alias of the same inode as a - // complete no-op. This test must precede the filesystem mutation. - if let Some(target) = displaced.as_ref() { - let source_md = old_inode.metadata()?; - let target_md = target.metadata()?; - if source_md.inode_id == target_md.inode_id && source_md.dev_id == target_md.dev_id { - return Ok(0); - } - } - old_parent_inode.move_to(old_filename, &new_parent_inode, new_filename, flags)?; if flags.contains(RenameFlags::EXCHANGE) { diff --git a/kernel/src/filesystem/vfs/syscall/sys_fchownat.rs b/kernel/src/filesystem/vfs/syscall/sys_fchownat.rs index a5f0ce211e..4e1eee0b0f 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fchownat.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fchownat.rs @@ -29,6 +29,10 @@ impl Syscall for SysFchownatHandle { .into_string() .map_err(|_| SystemError::EINVAL)?; let pathname = pathname.as_str().trim(); + let allowed = AtFlags::AT_SYMLINK_NOFOLLOW.bits() | AtFlags::AT_EMPTY_PATH.bits(); + if flags & !allowed != 0 { + return Err(SystemError::EINVAL); + } let flags = AtFlags::from_bits_truncate(flags); return do_fchownat(dirfd, pathname, uid, gid, flags); } diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc index 492b4780d5..7a98ecfaab 100644 --- a/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc +++ b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1437,7 +1439,7 @@ static int ext_test_positive_lookup_cache_respects_entry_ttl() { const char *mp = "/tmp/test_fuse_lookup_cache"; char hello[256]; char missing[256]; - struct stat st; + struct stat st = {}; char buf[32]; if (ensure_dir(mp) != 0) { @@ -4090,6 +4092,14 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { const char *mp = "/tmp/test_fuse_atomic_otrunc"; int requested = O_RDWR | O_TRUNC; int f = -1; + int ifd = -1; + struct stat st = {}; + unsigned char event_buf[512]; + ssize_t event_len = -1; + int open_index = -1; + int modify_index = -1; + int event_index = 0; + uint32_t getattr_before = 0; if (ensure_dir(mp) != 0) { printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); return -1; @@ -4107,6 +4117,7 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { volatile uint32_t last_open_flags = 0; volatile uint32_t open_count = 0; volatile uint32_t setattr_count = 0; + volatile uint32_t getattr_count = 0; struct fuse_daemon_args args; memset(&args, 0, sizeof(args)); @@ -4117,8 +4128,10 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { args.stop_on_destroy = 1; args.open_count = &open_count; args.setattr_count = &setattr_count; + args.getattr_count = &getattr_count; args.last_open_in_flags = &last_open_flags; args.init_out_flags_override = FUSE_INIT_EXT | FUSE_MAX_PAGES | FUSE_ATOMIC_O_TRUNC; + args.attr_valid_sec = 60; pthread_t th; if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { @@ -4145,14 +4158,46 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { char path[256]; snprintf(path, sizeof(path), "%s/hello.txt", mp); + ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (ifd < 0 || inotify_add_watch(ifd, path, IN_OPEN | IN_MODIFY) < 0) { + printf("[FAIL] inotify watch for atomic O_TRUNC: %s (errno=%d)\n", strerror(errno), + errno); + goto fail; + } f = open(path, requested); if (f < 0) { printf("[FAIL] open(%s): %s (errno=%d)\n", path, strerror(errno), errno); goto fail; } + getattr_before = getattr_count; + if (fstat(f, &st) != 0 || st.st_size != 0 || getattr_count <= getattr_before) { + printf("[FAIL] atomic O_TRUNC cached size=%lld errno=%d\n", (long long)st.st_size, + errno); + goto fail; + } close(f); f = -1; + if (fuseg_wait_readable(ifd, 1000) != 0) { + printf("[FAIL] no inotify events for atomic O_TRUNC\n"); + goto fail; + } + event_len = read(ifd, event_buf, sizeof(event_buf)); + for (size_t off = 0; event_len > 0 && off + sizeof(struct inotify_event) <= (size_t)event_len; + event_index++) { + const struct inotify_event *event = (const struct inotify_event *)(event_buf + off); + if ((event->mask & IN_OPEN) && open_index < 0) + open_index = event_index; + if ((event->mask & IN_MODIFY) && modify_index < 0) + modify_index = event_index; + off += sizeof(*event) + event->len; + } + if (open_index < 0 || modify_index <= open_index) { + printf("[FAIL] atomic O_TRUNC event order open=%d modify=%d\n", open_index, + modify_index); + goto fail; + } + usleep(100 * 1000); if (open_count != 1 || (last_open_flags & O_TRUNC) == 0) { printf("[FAIL] open counters/flags open=%u flags=0%o\n", open_count, last_open_flags); @@ -4163,6 +4208,9 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { goto fail; } + close(ifd); + ifd = -1; + if (umount(mp) != 0) { printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); goto fail_no_umount; @@ -4177,6 +4225,9 @@ static int ext_test_atomic_otrunc_uses_open_without_setattr() { if (f >= 0) { close(f); } + if (ifd >= 0) { + close(ifd); + } umount(mp); fail_no_umount: stop = 1; diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h b/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h index 4a7dd041de..63b3011cb3 100644 --- a/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h +++ b/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h @@ -736,6 +736,7 @@ struct fuse_daemon_args { volatile uint32_t *init_in_max_readahead; volatile uint32_t *access_count; volatile uint32_t *lookup_count; + volatile uint32_t *getattr_count; volatile uint32_t *flush_count; volatile uint32_t *write_count_at_flush; volatile uint32_t *last_flush_uid; @@ -1067,6 +1068,8 @@ static inline int fuse_handle_one(struct fuse_daemon_args *a, const unsigned cha } case FUSE_GETATTR: { (void)payload; + if (a->getattr_count) + (*a->getattr_count)++; struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); if (!node) { return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); diff --git a/user/apps/tests/dunitest/suites/normal/inotify_events.cc b/user/apps/tests/dunitest/suites/normal/inotify_events.cc index f41141ea5e..25fbc9072f 100644 --- a/user/apps/tests/dunitest/suites/normal/inotify_events.cc +++ b/user/apps/tests/dunitest/suites/normal/inotify_events.cc @@ -18,8 +18,11 @@ #include #include #include +#include #include +#include #include +#include #include #include @@ -75,6 +78,13 @@ bool saw_self(const std::vector &evs, uint32_t bit) { return false; } +int first_event_index(const std::vector &evs, uint32_t bit) { + for (size_t i = 0; i < evs.size(); i++) { + if (evs[i].mask & bit) return static_cast(i); + } + return -1; +} + } // namespace // --------------------------------------------------------------------------- @@ -182,6 +192,71 @@ TEST(InotifyAttribEvent, ChmodDeliversAttrib) { unlink(path.c_str()); } +TEST(InotifyAttribEvent, ChownNoopOnlyNotifiesWhenSpecialBitsChange) { + const std::string path = "/tmp/dunitest_inotify_chown_noop"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, path.c_str(), IN_ATTRIB), 0); + + ASSERT_EQ(chown(path.c_str(), static_cast(-1), static_cast(-1)), 0); + EXPECT_FALSE(saw_self(drain_events(ifd), IN_ATTRIB)); + + // Raw syscall registers may contain non-zero high bits; uid_t/gid_t are + // still 32-bit and Linux truncates them before interpreting -1. + constexpr unsigned long kHighBitsNoChange = 0x1ffffffffUL; + ASSERT_EQ(syscall(SYS_chown, path.c_str(), kHighBitsNoChange, kHighBitsNoChange), 0); + EXPECT_FALSE(saw_self(drain_events(ifd), IN_ATTRIB)); + + ASSERT_EQ(chmod(path.c_str(), 04755), 0); + (void)drain_events(ifd); + ASSERT_EQ(chown(path.c_str(), static_cast(-1), static_cast(-1)), 0); + auto evs = drain_events(ifd); + EXPECT_TRUE(saw_self(evs, IN_ATTRIB)); + struct stat st {}; + ASSERT_EQ(stat(path.c_str(), &st), 0); + EXPECT_EQ(st.st_mode & S_ISUID, 0U); + + close(ifd); + unlink(path.c_str()); +} + +TEST(InotifyAttribEvent, ChownFollowsSymlinksAndRejectsInvalidFlags) { + const std::string target = "/tmp/dunitest_inotify_chown_target"; + const std::string link_path = "/tmp/dunitest_inotify_chown_link"; + const std::string loop_a = "/tmp/dunitest_inotify_chown_loop_a"; + const std::string loop_b = "/tmp/dunitest_inotify_chown_loop_b"; + int fd = open(target.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + ASSERT_EQ(symlink(target.c_str(), link_path.c_str()), 0); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, target.c_str(), IN_ATTRIB), 0); + ASSERT_EQ(chown(link_path.c_str(), 0, static_cast(-1)), 0); + EXPECT_TRUE(saw_self(drain_events(ifd), IN_ATTRIB)); + + errno = 0; + EXPECT_EQ(syscall(SYS_fchownat, AT_FDCWD, target.c_str(), -1, -1, 0x80000000U), -1); + EXPECT_EQ(errno, EINVAL); + + ASSERT_EQ(symlink(loop_b.c_str(), loop_a.c_str()), 0); + ASSERT_EQ(symlink(loop_a.c_str(), loop_b.c_str()), 0); + errno = 0; + EXPECT_EQ(chown(loop_a.c_str(), 0, static_cast(-1)), -1); + EXPECT_EQ(errno, ELOOP); + + close(ifd); + unlink(loop_a.c_str()); + unlink(loop_b.c_str()); + unlink(link_path.c_str()); + unlink(target.c_str()); +} + // --------------------------------------------------------------------------- // IN_DELETE_SELF + IN_IGNORED: unlinking a watched file delivers // IN_DELETE_SELF, followed by IN_IGNORED (watch auto-revoked). @@ -299,6 +374,37 @@ TEST(InotifyExcludeUnlinked, FtruncateRemainsADentryEvent) { close(ifd); } +TEST(InotifyExcludeUnlinked, PathEventsAreSuppressedForDirectAndParentMarks) { + const std::string dir = "/tmp/dunitest_inotify_excl_path"; + const std::string path = dir + "/file"; + mkdir(dir.c_str(), 0777); + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + + int direct_ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + int parent_ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(direct_ifd, 0); + ASSERT_GE(parent_ifd, 0); + ASSERT_GE(inotify_add_watch(direct_ifd, path.c_str(), IN_MODIFY | IN_EXCL_UNLINK), 0); + ASSERT_GE(inotify_add_watch(parent_ifd, dir.c_str(), IN_MODIFY | IN_EXCL_UNLINK), 0); + + ASSERT_EQ(unlink(path.c_str()), 0); + ASSERT_EQ(write(fd, "x", 1), 1); + EXPECT_FALSE(saw_self(drain_events(direct_ifd), IN_MODIFY)); + EXPECT_FALSE(saw(drain_events(parent_ifd), IN_MODIFY, "file")); + + // ftruncate is a dentry-data event on Linux, not a path-data event, so + // IN_EXCL_UNLINK does not suppress it for either mark. + ASSERT_EQ(ftruncate(fd, 2), 0); + EXPECT_TRUE(saw_self(drain_events(direct_ifd), IN_MODIFY)); + EXPECT_TRUE(saw(drain_events(parent_ifd), IN_MODIFY, "file")); + + close(fd); + close(direct_ifd); + close(parent_ifd); + rmdir(dir.c_str()); +} + TEST(InotifyRenameEvents, SameInodeAliasIsNoOp) { const std::string first = "/tmp/dunitest_inotify_alias_a"; const std::string second = "/tmp/dunitest_inotify_alias_b"; @@ -320,6 +426,126 @@ TEST(InotifyRenameEvents, SameInodeAliasIsNoOp) { unlink(second.c_str()); } +TEST(InotifyRenameEvents, NoOpAndNoReplacePrecedeDirectoryWriteChecks) { + const std::string dir = "/tmp/dunitest_rename_readonly_alias"; + const std::string first = dir + "/a"; + const std::string second = dir + "/b"; + mkdir(dir.c_str(), 0755); + int fd = open(first.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + ASSERT_EQ(link(first.c_str(), second.c_str()), 0); + ASSERT_EQ(chmod(dir.c_str(), 0555), 0); + + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + if (setgid(65534) != 0 || setuid(65534) != 0) _exit(10); + if (rename(first.c_str(), second.c_str()) != 0) _exit(11); + errno = 0; + constexpr unsigned int kRenameNoReplace = 1; + if (syscall(SYS_renameat2, AT_FDCWD, first.c_str(), AT_FDCWD, second.c_str(), + kRenameNoReplace) != -1 || + errno != EEXIST) + _exit(12); + _exit(0); + } + int status = 0; + ASSERT_EQ(waitpid(child, &status, 0), child); + EXPECT_TRUE(WIFEXITED(status)); + EXPECT_EQ(WEXITSTATUS(status), 0); + + chmod(dir.c_str(), 0755); + unlink(first.c_str()); + unlink(second.c_str()); + rmdir(dir.c_str()); +} + +TEST(InotifyRenameEvents, NoReplaceExistingTargetPrecedesAncestorTrap) { + const std::string root = "/tmp/dunitest_rename_noreplace_trap"; + const std::string source = root + "/source"; + const std::string child = source + "/child"; + const std::string target = child + "/existing"; + mkdir(root.c_str(), 0755); + mkdir(source.c_str(), 0755); + mkdir(child.c_str(), 0755); + int fd = open(target.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + constexpr unsigned int kRenameNoReplace = 1; + errno = 0; + EXPECT_EQ(syscall(SYS_renameat2, AT_FDCWD, source.c_str(), AT_FDCWD, target.c_str(), + kRenameNoReplace), + -1); + EXPECT_EQ(errno, EEXIST); + + unlink(target.c_str()); + rmdir(child.c_str()); + rmdir(source.c_str()); + rmdir(root.c_str()); +} + +TEST(InotifyRenameEvents, SameInodeAcrossBindMountsReturnsExdev) { + const std::string root = "/tmp/dunitest_rename_bind_exdev"; + const std::string source = root + "/source"; + const std::string alias = root + "/alias"; + const std::string source_file = source + "/file"; + const std::string alias_file = alias + "/file"; + mkdir(root.c_str(), 0777); + mkdir(source.c_str(), 0777); + mkdir(alias.c_str(), 0777); + int fd = open(source_file.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + ASSERT_EQ(mount(source.c_str(), alias.c_str(), nullptr, MS_BIND, nullptr), 0) + << strerror(errno); + + errno = 0; + EXPECT_EQ(rename(source_file.c_str(), alias_file.c_str()), -1); + EXPECT_EQ(errno, EXDEV); + + EXPECT_EQ(umount(alias.c_str()), 0) << strerror(errno); + unlink(source_file.c_str()); + rmdir(alias.c_str()); + rmdir(source.c_str()); + rmdir(root.c_str()); +} + +TEST(InotifyRenameEvents, LinuxMergesIdenticalMoveEventsIgnoringCookie) { + const std::string root = "/tmp/dunitest_inotify_cookie_merge"; + const std::string watched = root + "/watched"; + const std::string outside = root + "/outside"; + const std::string watched_file = watched + "/file"; + const std::string outside_file = outside + "/file"; + mkdir(root.c_str(), 0777); + mkdir(watched.c_str(), 0777); + mkdir(outside.c_str(), 0777); + int fd = open(watched_file.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, watched.c_str(), IN_MOVED_FROM), 0); + ASSERT_EQ(rename(watched_file.c_str(), outside_file.c_str()), 0); + ASSERT_EQ(rename(outside_file.c_str(), watched_file.c_str()), 0); + ASSERT_EQ(rename(watched_file.c_str(), outside_file.c_str()), 0); + + auto evs = drain_events(ifd); + size_t moved_from_count = 0; + for (const auto &event : evs) { + if ((event.mask & IN_MOVED_FROM) && event.name == "file") moved_from_count++; + } + EXPECT_EQ(moved_from_count, 1U); + + close(ifd); + unlink(outside_file.c_str()); + rmdir(outside.c_str()); + rmdir(watched.c_str()); + rmdir(root.c_str()); +} + TEST(InotifyOpenEvents, OPathProducesNoOpenOrClose) { const std::string path = "/tmp/dunitest_inotify_opath"; int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); @@ -341,6 +567,82 @@ TEST(InotifyOpenEvents, OPathProducesNoOpenOrClose) { unlink(path.c_str()); } +TEST(InotifyOpenEvents, OTruncDeliversOpenBeforeModify) { + const std::string path = "/tmp/dunitest_inotify_otrunc_order"; + int fd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, "payload", 7), 7); + close(fd); + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, path.c_str(), IN_OPEN | IN_MODIFY), 0); + fd = open(path.c_str(), O_WRONLY | O_TRUNC); + ASSERT_GE(fd, 0); + close(fd); + + auto evs = drain_events(ifd); + int open_index = first_event_index(evs, IN_OPEN); + int modify_index = first_event_index(evs, IN_MODIFY); + ASSERT_GE(open_index, 0); + ASSERT_GE(modify_index, 0); + EXPECT_LT(open_index, modify_index); + + close(ifd); + unlink(path.c_str()); +} + +TEST(InotifyOpenEvents, NewlyCreatedOTruncFileHasNoModifyEvent) { + const std::string dir = "/tmp/dunitest_inotify_otrunc_create"; + const std::string path = dir + "/file"; + mkdir(dir.c_str(), 0777); + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, dir.c_str(), IN_CREATE | IN_OPEN | IN_MODIFY), 0); + + int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + auto evs = drain_events(ifd); + EXPECT_TRUE(saw(evs, IN_CREATE, "file")); + EXPECT_TRUE(saw(evs, IN_OPEN, "file")); + EXPECT_FALSE(saw(evs, IN_MODIFY, "file")); + + close(ifd); + unlink(path.c_str()); + rmdir(dir.c_str()); +} + +TEST(InotifyOpenEvents, ExecDeliversOpenAndClose) { + char executable[4096]; + ssize_t length = readlink("/proc/self/exe", executable, sizeof(executable) - 1); + ASSERT_GT(length, 0); + executable[length] = '\0'; + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + ASSERT_GE(ifd, 0); + ASSERT_GE(inotify_add_watch(ifd, executable, IN_OPEN | IN_CLOSE_NOWRITE), 0); + + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + execl(executable, executable, "--gtest_filter=NoSuchExecProbe.*", nullptr); + _exit(127); + } + int status = 0; + ASSERT_EQ(waitpid(child, &status, 0), child); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); + + auto evs = drain_events(ifd); + int open_index = first_event_index(evs, IN_OPEN); + int close_index = first_event_index(evs, IN_CLOSE_NOWRITE); + ASSERT_GE(open_index, 0); + ASSERT_GE(close_index, 0); + EXPECT_LT(open_index, close_index); + close(ifd); +} + // --------------------------------------------------------------------------- // IN_MOVE_SELF: renaming a watched file delivers IN_MOVE_SELF. // ---------------------------------------------------------------------------