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..4314733751 --- /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 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, FsNotifyObjectId}; + +/// 一个通知消费者。一个 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(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 new file mode 100644 index 0000000000..5f1994e513 --- /dev/null +++ b/kernel/src/filesystem/fsnotify/mark.rs @@ -0,0 +1,90 @@ +//! [`FsNotifyMark`]:一个 watch(group + inode + mask + wd)及其生命周期管理。 + +use alloc::sync::{Arc, Weak}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use crate::filesystem::vfs::IndexNode; + +use super::{ + adjust_total_watches, index_remove, FsNotifyDeleteState, FsNotifyGroup, FsNotifyObjectId, +}; +use crate::libs::mutex::Mutex; + +/// 一个 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, + /// 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: AtomicBool, +} + +impl FsNotifyMark { + /// 取被监听 inode 的标识:(inode_id, dev_id) 复合键。 + /// + /// FUSE 等多挂载场景可能复用相同 inode 号(如 FUSE_ROOT_ID=1), + /// 必须用 (inode_id, dev_id) 组合区分不同挂载上的 inode,否则会跨挂载 + /// 误匹配 mark,导致事件泄露或误判已有 watch。 + pub fn identity(&self) -> FsNotifyObjectId { + self.object_id + } +} + +/// 撤销一个 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; + }; + + // 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 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 { + // 投递 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..ab6a0324ab --- /dev/null +++ b/kernel/src/filesystem/fsnotify/mod.rs @@ -0,0 +1,565 @@ +//! 文件系统事件通知统一层(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::{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 按需设置)。 +// +// 参考: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, + ) -> EnqueueResult; + /// 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 MarkList = Arc>>; +type MarkIndex = HashMap; + +lazy_static::lazy_static! { + 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 即可。 +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 +} + +pub(crate) fn index_add(mark: &Arc) -> Result<(), SystemError> { + let key = mark.identity(); + 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; + 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; + } + match replacement { + Some(entries) => { + idx.insert(key, entries); + } + None => { + idx.remove(&key); + } + } + return; + } +} + +/// 统一事件投递入口。在 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, +) { + 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 { + return; + } + + // ② 预取 inode 元数据(inode 活着、metadata 只读不锁,安全)。 + // 事件的「主体」是 child(被创建/删除/移动/修改的对象);IN_ISDIR 由主体是否为 + // 目录决定,对 parent/child 两类 watch 一视同仁。若无 child(仅父目录自身事件 + // 的退化情况),ISDIR 不置位。 + // 用 (inode_id, dev_id) 复合键:FUSE 多挂载复用相同 inode 号,必须加 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; + } + + // ③ 收集候选 mark 快照:临界区仅做哈希查表(秒放),不做后端工作。 + // (mark 强引用, name, is_parent) + 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 模型): + // - 命名空间事件 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::CLOSE_WRITE + | FsEvent::CLOSE_NOWRITE + | FsEvent::OPEN; + + // ④ 锁外投递。 + 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 + } 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: 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) + && child_unlinked + { + continue; + } + } + + // dispatch 设置 ISDIR(主体是目录时)。 + let mut delivered = routed; + if event_is_dir && !routed.intersects(self_only) { + delivered |= FsEvent::ISDIR; + } + + let group = mark.group.upgrade(); + let enqueue_result = if let Some(group) = group.as_ref() { + group + .backend + .handle_event(&group, &mark, delivered, name, cookie) + } else { + EnqueueResult::Filtered + }; + + // 撤销:inode 死亡(无条件)或 oneshot(订阅匹配后触发一次即撤销)。 + 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/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/inotify.rs b/kernel/src/filesystem/inotify.rs new file mode 100644 index 0000000000..dbeb97301a --- /dev/null +++ b/kernel/src/filesystem/inotify.rs @@ -0,0 +1,1098 @@ +//! inotify 文件系统事件通知设备层。 +//! +//! 实现伪文件(`InotifyInode`:`IndexNode + PollableInode`)与 inotify 后端 +//!(`InotifyBackend`:`FsNotifyBackend`),以及 4 个 syscall handler。 +//! +//! 模式照搬 `eventfd.rs`:伪 FS + 伪 Inode + epoll 集成。 +//! +//! 详见 `docs/kernel/filesystem/inotify.md` §4。 + +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, Ordering}; + +use alloc::boxed::Box; +use hashbrown::HashMap; +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, EnqueueResult, FsEvent, FsNotifyBackend, FsNotifyDeleteState, 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::casting::DowncastArc; +use crate::libs::mutex::{Mutex, MutexGuard}; +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; + +// ============================================================================ +// 用户态 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; + pub const ALL_INOTIFY_BITS: u32 = + WATCH_CONTROL | 0x0000_0fff | IN_UNMOUNT | IN_Q_OVERFLOW | IN_IGNORED | IN_ISDIR; +} + +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; + +#[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); + } + } +} + +// ============================================================================ +// 后端数据结构 +// ============================================================================ + +/// 队列里的一个事件(已格式化为 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, + /// 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` 锁保护。 +#[derive(Debug)] +struct WdTable { + /// 单调分配 wd(1..=i32::MAX-1,饱和见 §6.2;-1 被 Q_OVERFLOW 占用)。 + counter: i32, + map: HashMap>, +} + +/// inotify 后端共享状态:同时被 `InotifyBackend`(在 group 内)与 `InotifyInode`(read 入口)持有。 +/// +/// 事件锁与 wd 锁分离,使 read(消费)与 add_watch/rm_watch(wd 管理)互不阻塞。 +#[derive(Debug)] +pub struct InotifyState { + /// 事件锁:所有 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(quota_key: InotifyQuotaKey) -> Self { + Self { + events: Mutex::new(InotifyQueue { + list: VecDeque::new(), + overflow_pending: false, + pre_overflow_remaining: 0, + }), + read_consumer: Mutex::new(()), + max_queued_events: MAX_QUEUED_EVENTS, + wd: Mutex::new(WdTable { + counter: 0, + map: HashMap::new(), + }), + quota_key, + } + } +} + +/// inotify 后端(实现 [`FsNotifyBackend`])。 +#[derive(Debug)] +struct InotifyBackend { + state: Arc, +} + +impl InotifyBackend { + /// 入队一个事件(调用方已持 events 锁)。 + 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 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 + } +} + +impl FsNotifyBackend for InotifyBackend { + fn handle_event( + &self, + group: &FsNotifyGroup, + mark: &FsNotifyMark, + 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 EnqueueResult::Filtered; + } + + // 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| { + 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, + }; + + 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。 + 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(); + 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 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, + mask: user_mask::IN_IGNORED, + cookie: 0, + name: None, + }, + ); + !readable_before && (!queue.list.is_empty() || queue.overflow_pending) + }; + if wake { + group.wait_queue.wakeup_all(None); + let _ = EventPoll::wakeup_epoll( + &group.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + } + } + + fn queue_nonempty(&self) -> bool { + let q = self.state.events.lock(); + !q.list.is_empty() || q.overflow_pending + } +} + +// ============================================================================ +// 伪文件系统 +// ============================================================================ + +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, + InotifyQuotaKey { + user_namespace: 0, + euid: 0, + }, + )) + } + + 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 { + 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 用)。 + fn new(nonblock: bool, quota_key: InotifyQuotaKey) -> Self { + let state = Arc::new(InotifyState::new(quota_key)); + 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 { + if let Some(mounted) = inode + .clone() + .downcast_arc::() + { + 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) + } + + 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 = fsnotify::target_for_inode(&inode)?.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)?; + + // 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:查重与插入 + // 不在同一锁内时,两个线程可各自通过「无 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.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); + } + + // 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 { + return Err(SystemError::ENOSPC); + } + t.map.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + t.counter += 1; + t.counter + }; + + let mark = Arc::try_new(FsNotifyMark { + wd, + group: Arc::downgrade(&self.group), + _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: 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.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); + // 注: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 t = self.state.wd.lock(); + match t.map.get(&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.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); + } + release_quota(self.state.quota_key, 1, 0); + // 唤醒任何阻塞在 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); + } + + let _consumer = self.state.read_consumer.lock(); + + loop { + 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 { + // 空队列 + 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; + } + // 首个事件即放不下(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)?; + + let quota_key = current_quota_key(); + reserve_instance(quota_key)?; + + let nonblock = flags.contains(InotifyInitFlags::IN_NONBLOCK); + let inode = Arc::new(InotifyInode::new(nonblock, quota_key)); + 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,需手动回退实例计数。 + release_quota(quota_key, 1, 0); + })?; + // 防递归: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); + + // 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 = { + 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(); + // 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)?; + 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/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 7f15de2a4a..f9d640c883 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -3,6 +3,7 @@ use core::{ sync::atomic::{AtomicUsize, Ordering}, }; +use crate::filesystem::fsnotify::{self, FsEvent}; use alloc::{string::String, sync::Arc, vec::Vec}; use log::error; use system_error::SystemError; @@ -1067,6 +1068,13 @@ impl File { } } + // fsnotify:写成功后投递 IN_MODIFY(FMODE_NONOTIFY 短路,防 inotify fd 递归)。 + if written_len > 0 + && !self.mode.read().contains(FileMode::FMODE_NONOTIFY) + && fsnotify::has_any_watch() + { + self.notify_fs_event(FsEvent::MODIFY); + } Ok(written_len) } /// @brief 创建一个新的文件对象 @@ -1283,6 +1291,38 @@ impl File { return Ok(f); } + /// 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); + } + } + + /// 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. @@ -1561,6 +1601,14 @@ 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() + { + self.notify_fs_event(FsEvent::ACCESS); + } Ok(len) } @@ -1976,6 +2024,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 } @@ -2262,6 +2317,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 +2513,19 @@ 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。 + 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 + }; + self.notify_fs_event(m); + } 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..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) } @@ -2029,6 +2032,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..9c6ee3e192 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(); } @@ -3036,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 { @@ -3117,7 +3214,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 +3481,51 @@ 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 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()); + 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 + } + + 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,6 +3699,42 @@ impl MountFSInode { } } + /// 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; + } + 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 { self.do_absolute_path_impl(false) } @@ -3754,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> { @@ -4096,7 +4285,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> { @@ -4152,8 +4352,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 @@ -4162,9 +4382,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] @@ -4201,8 +4428,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 @@ -4211,9 +4453,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] @@ -4291,6 +4536,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, @@ -4298,6 +4547,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 acb8272a87..ba5aa9d25c 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -1,3 +1,4 @@ +use crate::filesystem::fsnotify::{self, FsEvent}; use alloc::sync::Arc; use system_error::SystemError; @@ -11,7 +12,7 @@ use super::{ 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, }; @@ -133,6 +134,8 @@ pub fn do_fchmod(inode: Arc, mode: InodeMode) -> Result 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(); @@ -181,11 +178,10 @@ fn chown_common(inode: Arc, uid: usize, gid: usize) -> Result, uid: usize, gid: usize) -> 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), - }; - 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); + } + // 对已存在的目录使用 O_CREAT 视为错误 + if how.o_flags.contains(FileFlags::O_CREAT) && !created && file_type == FileType::Dir { + return Err(SystemError::EISDIR); } - if how.o_flags.contains(FileFlags::O_TRUNC) { - need.insert(PermissionMask::MAY_WRITE); + // 目录相关检查 + 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); + } } - if !need.is_empty() { - super::permission::check_inode_permission(&inode, &metadata, need)?; + // 非 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)? + // Existing regular files opened with O_TRUNC are modified after the + // filesystem open succeeds. Newly created files are already empty and + // Linux clears O_TRUNC for this phase. + let do_truncate = + !created && file_type == FileType::File && how.o_flags.contains(FileFlags::O_TRUNC); + let truncate_in_vfs = do_truncate && inode.requires_separate_open_truncate(&how.o_flags); + 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)?, + }; + + // 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) + })(); + + 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); - - // 把文件对象存入pcb - let r = ProcessManager::current_pcb() - .fd_table() + let result = fd_table .write() - .alloc_fd(file, None, cloexec) + .install_reserved_fd(reservation, file) .map(|fd| fd as usize); - - return r; + result } /// 为exec打开可执行文件 @@ -545,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)) } @@ -661,6 +698,8 @@ pub fn do_utimensat( | SetMetadataMask::TIMES_BY_WRITE, )?; } + // fsnotify:时间戳变更 → IN_ATTRIB。 + fsnotify::fsnotify_inode(FsEvent::ATTRIB, &inode); 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..8083a023f2 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,18 @@ 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, + ); + fsnotify::fsnotify(FsEvent::ATTRIB, None, 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..631f119459 100644 --- a/kernel/src/filesystem/vfs/syscall/rename_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/rename_utils.rs @@ -1,3 +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; @@ -6,8 +8,10 @@ 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; /// # 修改文件名 /// /// @@ -66,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); @@ -75,31 +91,72 @@ 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); - } - } - 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); } + let old_id = fsnotify::target_for_inode(&old_inode)?.id; + let new_id = fsnotify::target_for_inode(new_inode)?.id; + if old_id == new_id { + return Ok(0); + } + } + + // 非 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 的祖先: @@ -122,5 +179,64 @@ pub fn do_renameat2( )?; 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, + 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, + ); + fsnotify::fsnotify(FsEvent::MOVE_SELF, None, Some(&old_inode), 0); + let cookie2 = fsnotify::next_cookie(); + fsnotify::fsnotify( + FsEvent::MOVED_FROM, + 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, + ); + fsnotify::fsnotify(FsEvent::MOVE_SELF, None, Some(new_inode), 0); + } else { + // 普通 rename(可能覆盖目标):单 cookie 配对 MOVED_FROM/MOVED_TO + MOVE_SELF。 + let cookie = fsnotify::next_cookie(); + fsnotify::fsnotify( + FsEvent::MOVED_FROM, + 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, + ); + 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 { + fsnotify::fsnotify(FsEvent::ATTRIB, None, 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)?; + // DELETE_SELF is emitted later when the disconnected dentry finally + // detaches from the inode. + fsnotify::fsnotify( + FsEvent::DELETE, + Some((&parent_inode, filename)), + Some(&target_inode), + 0, + ); return Ok(0); } @@ -713,7 +731,15 @@ pub fn do_unlink_at(dirfd: i32, path: &str) -> Result { // 在父目录上执行 unlink 操作 parent_inode.unlink(filename)?; - + // 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( + FsEvent::DELETE, + Some((&parent_inode, filename)), + Some(&target_inode), + 0, + ); return Ok(0); } @@ -782,7 +808,11 @@ 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::fsnotify_inode(FsEvent::MODIFY, &inode); + } + r } pub(crate) fn prepare_write_side_effect_metadata( @@ -920,6 +950,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 { @@ -982,16 +1018,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(), - ) + ); + // 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/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_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..25fbc9072f --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/inotify_events.cc @@ -0,0 +1,762 @@ +// 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 + +#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; +} + +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 + +// --------------------------------------------------------------------------- +// 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()); +} + +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). +// --------------------------------------------------------------------------- +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); +} + +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(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"; + 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(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); + 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()); +} + +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. +// --------------------------------------------------------------------------- +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 8cf6f05a51..22bd86527d 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -97,4 +97,6 @@ normal/af_packet_e2e normal/af_packet_mcast normal/rtnetlink_link_semantics normal/tty_termios +normal/inotify_dir_watch +normal/inotify_events normal/sched_tracepoint