From 4a7e253691b7d4b8b98f10da3dcc4b03a91b12ee Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Tue, 14 Apr 2026 17:38:32 -0700 Subject: [PATCH 1/2] inotify implementation --- etc/syscalls_linux_aarch64.md | 6 +- src/arch/arm64/exceptions/syscall.rs | 6 + src/fs/fops.rs | 4 + src/fs/mod.rs | 43 +- src/fs/reg.rs | 9 +- src/fs/syscalls/at/chmod.rs | 3 +- src/fs/syscalls/at/chown.rs | 3 +- src/fs/syscalls/at/utime.rs | 3 +- src/fs/syscalls/chmod.rs | 6 +- src/fs/syscalls/chown.rs | 6 +- src/fs/syscalls/setxattr.rs | 3 +- src/process/inotify.rs | 598 +++++++++++++++++++++++++++ src/process/mod.rs | 1 + usertest/src/inotify.rs | 119 ++++++ usertest/src/main.rs | 1 + 15 files changed, 794 insertions(+), 17 deletions(-) create mode 100644 src/process/inotify.rs create mode 100644 usertest/src/inotify.rs diff --git a/etc/syscalls_linux_aarch64.md b/etc/syscalls_linux_aarch64.md index 8696d05e..3fe2887c 100644 --- a/etc/syscalls_linux_aarch64.md +++ b/etc/syscalls_linux_aarch64.md @@ -27,9 +27,9 @@ | 0x17 (23) | dup | (unsigned int fildes) | __arm64_sys_dup | true | | 0x18 (24) | dup3 | (unsigned int oldfd, unsigned int newfd, int flags) | __arm64_sys_dup3 | true | | 0x19 (25) | fcntl | (unsigned int fd, unsigned int cmd, unsigned long arg) | __arm64_sys_fcntl | true | -| 0x1a (26) | inotify_init1 | (int flags) | __arm64_sys_inotify_init1 | false | -| 0x1b (27) | inotify_add_watch | (int fd, const char *pathname, u32 mask) | __arm64_sys_inotify_add_watch | false | -| 0x1c (28) | inotify_rm_watch | (int fd, __s32 wd) | __arm64_sys_inotify_rm_watch | false | +| 0x1a (26) | inotify_init1 | (int flags) | __arm64_sys_inotify_init1 | true | +| 0x1b (27) | inotify_add_watch | (int fd, const char *pathname, u32 mask) | __arm64_sys_inotify_add_watch | true | +| 0x1c (28) | inotify_rm_watch | (int fd, __s32 wd) | __arm64_sys_inotify_rm_watch | true | | 0x1d (29) | ioctl | (unsigned int fd, unsigned int cmd, unsigned long arg) | __arm64_sys_ioctl | true | | 0x1e (30) | ioprio_set | (int which, int who, int ioprio) | __arm64_sys_ioprio_set | false | | 0x1f (31) | ioprio_get | (int which, int who) | __arm64_sys_ioprio_get | false | diff --git a/src/arch/arm64/exceptions/syscall.rs b/src/arch/arm64/exceptions/syscall.rs index 6ecef8c9..cfec5561 100644 --- a/src/arch/arm64/exceptions/syscall.rs +++ b/src/arch/arm64/exceptions/syscall.rs @@ -83,6 +83,7 @@ use crate::{ fcntl::sys_fcntl, select::{sys_ppoll, sys_pselect6}, }, + inotify::{sys_inotify_add_watch, sys_inotify_init1, sys_inotify_rm_watch}, pidfd::sys_pidfd_open, prctl::sys_prctl, ptrace::{TracePoint, ptrace_stop, sys_ptrace}, @@ -160,6 +161,11 @@ pub async fn handle_syscall(mut ctx: ProcessCtx) { ) .await } + 0x1a => sys_inotify_init1(&ctx, arg1 as _).await, + 0x1b => { + sys_inotify_add_watch(&ctx, arg1.into(), TUA::from_value(arg2 as _), arg3 as _).await + } + 0x1c => sys_inotify_rm_watch(&ctx, arg1.into(), arg2 as i32).await, 0x5 => { sys_setxattr( &ctx, diff --git a/src/fs/fops.rs b/src/fs/fops.rs index c60c8628..807c911d 100644 --- a/src/fs/fops.rs +++ b/src/fs/fops.rs @@ -151,4 +151,8 @@ pub trait FileOps: Send + Sync { ) -> Option<&mut crate::process::thread_group::signal::signalfd::SignalFd> { None } + + fn as_inotify(&mut self) -> Option<&mut crate::process::inotify::Inotify> { + None + } } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 0db5a581..2fc0ec32 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,7 +1,10 @@ use crate::clock::realtime::date; use crate::{ drivers::{DM, Driver}, - process::Task, + process::{ + Task, + inotify::{notify_create, notify_delete, notify_delete_self, notify_modify, notify_move}, + }, sync::SpinLock, }; use alloc::{borrow::ToOwned, boxed::Box, collections::btree_map::BTreeMap, sync::Arc, vec::Vec}; @@ -377,9 +380,11 @@ impl VFS { return Err(FsError::NotADirectory.into()); } - parent_inode + let target_inode = parent_inode .create(file_name, FileType::File, mode, Some(date())) - .await? + .await?; + notify_create(parent_inode.id(), file_name, false).await; + target_inode } else { // O_CREAT was not specified, so NotFound is the correct error. return Err(FsError::NotFound.into()); @@ -409,6 +414,7 @@ impl VFS { { // TODO: Check for write permissions on the inode itself. target_inode.truncate(0).await?; + notify_modify(target_inode.id()).await; } match attr.file_type { @@ -485,6 +491,7 @@ impl VFS { parent_inode .create(dir_name, FileType::Directory, mode, Some(date())) .await?; + notify_create(parent_inode.id(), dir_name, true).await; Ok(()) } @@ -547,6 +554,9 @@ impl VFS { let name = path.file_name().ok_or(FsError::InvalidInput)?; parent_inode.unlink(name).await?; + let is_dir = attr.file_type == FileType::Directory; + notify_delete(parent_inode.id(), name, is_dir).await; + notify_delete_self(target_inode.id(), is_dir).await; Ok(()) } @@ -558,7 +568,9 @@ impl VFS { name: &str, ) -> Result<()> { // just delegate to inode only, all handling is done at the syscall level - new_parent.link(name, target).await + new_parent.link(name, target).await?; + notify_create(new_parent.id(), name, false).await; + Ok(()) } pub async fn symlink( @@ -584,7 +596,9 @@ impl VFS { return Err(FsError::NotADirectory.into()); } - parent_inode.symlink(name, target).await + parent_inode.symlink(name, target).await?; + notify_create(parent_inode.id(), name, false).await; + Ok(()) } Err(e) => Err(e), } @@ -598,9 +612,24 @@ impl VFS { new_name: &str, no_replace: bool, ) -> Result<()> { + let target_inode = old_parent_inode.lookup(old_name).await?; + let target_attr = target_inode.getattr().await?; + new_parent_inode - .rename_from(old_parent_inode, old_name, new_name, no_replace) - .await + .rename_from(old_parent_inode.clone(), old_name, new_name, no_replace) + .await?; + + notify_move( + old_parent_inode.id(), + old_name, + new_parent_inode.id(), + new_name, + target_inode.id(), + target_attr.file_type == FileType::Directory, + ) + .await; + + Ok(()) } pub async fn exchange( diff --git a/src/fs/reg.rs b/src/fs/reg.rs index fba4c74c..71860af7 100644 --- a/src/fs/reg.rs +++ b/src/fs/reg.rs @@ -5,6 +5,7 @@ use crate::{ page::ClaimedPage, uaccess::{copy_from_user_slice, copy_to_user_slice}, }, + process::inotify::notify_modify, }; use alloc::{boxed::Box, sync::Arc}; use async_trait::async_trait; @@ -88,11 +89,17 @@ impl FileOps for RegFile { buf = buf.add_bytes(bytes_written); } + if total_bytes_written > 0 { + notify_modify(self.inode.id()).await; + } + Ok(total_bytes_written) } async fn truncate(&mut self, _ctx: &FileCtx, new_size: usize) -> Result<()> { - self.inode.truncate(new_size as _).await + self.inode.truncate(new_size as _).await?; + notify_modify(self.inode.id()).await; + Ok(()) } fn poll_read_ready(&self) -> Pin> + 'static + Send>> { diff --git a/src/fs/syscalls/at/chmod.rs b/src/fs/syscalls/at/chmod.rs index 321a5040..6d44ffca 100644 --- a/src/fs/syscalls/at/chmod.rs +++ b/src/fs/syscalls/at/chmod.rs @@ -11,7 +11,7 @@ use ringbuf::Arc; use crate::{ fs::syscalls::at::{AtFlags, resolve_at_start_node, resolve_path_flags}, memory::uaccess::cstr::UserCStr, - process::{Task, fd_table::Fd}, + process::{Task, fd_table::Fd, inotify::notify_attrib}, sched::syscall_ctx::ProcessCtx, }; @@ -45,6 +45,7 @@ pub async fn sys_fchmodat( attr.permissions = mode; node.setattr(attr).await?; + notify_attrib(node.id()).await; Ok(0) } diff --git a/src/fs/syscalls/at/chown.rs b/src/fs/syscalls/at/chown.rs index f0e67126..65fcc799 100644 --- a/src/fs/syscalls/at/chown.rs +++ b/src/fs/syscalls/at/chown.rs @@ -13,7 +13,7 @@ use libkernel::{ use crate::{ fs::syscalls::at::{AtFlags, resolve_at_start_node, resolve_path_flags}, memory::uaccess::cstr::UserCStr, - process::fd_table::Fd, + process::{fd_table::Fd, inotify::notify_attrib}, sched::syscall_ctx::ProcessCtx, }; @@ -51,6 +51,7 @@ pub async fn sys_fchownat( } } node.setattr(attr).await?; + notify_attrib(node.id()).await; Ok(0) } diff --git a/src/fs/syscalls/at/utime.rs b/src/fs/syscalls/at/utime.rs index 1f1c03db..e81c9906 100644 --- a/src/fs/syscalls/at/utime.rs +++ b/src/fs/syscalls/at/utime.rs @@ -16,7 +16,7 @@ use crate::{ clock::{realtime::date, timespec::TimeSpec}, fs::syscalls::at::{AtFlags, resolve_at_start_node, resolve_path_flags}, memory::uaccess::{copy_from_user, cstr::UserCStr}, - process::fd_table::Fd, + process::{fd_table::Fd, inotify::notify_attrib}, sched::syscall_ctx::ProcessCtx, }; @@ -88,6 +88,7 @@ pub async fn sys_utimensat( } node.setattr(attr).await?; + notify_attrib(node.id()).await; Ok(0) } diff --git a/src/fs/syscalls/chmod.rs b/src/fs/syscalls/chmod.rs index 417b0dca..0f824544 100644 --- a/src/fs/syscalls/chmod.rs +++ b/src/fs/syscalls/chmod.rs @@ -4,7 +4,10 @@ use libkernel::{ fs::attr::FilePermissions, }; -use crate::{process::fd_table::Fd, sched::syscall_ctx::ProcessCtx}; +use crate::{ + process::{fd_table::Fd, inotify::notify_attrib}, + sched::syscall_ctx::ProcessCtx, +}; pub async fn sys_fchmod(ctx: &ProcessCtx, fd: Fd, mode: u16) -> Result { let task = ctx.shared().clone(); @@ -24,6 +27,7 @@ pub async fn sys_fchmod(ctx: &ProcessCtx, fd: Fd, mode: u16) -> Result { attr.permissions = permissions; inode.setattr(attr).await?; + notify_attrib(inode.id()).await; Ok(0) } diff --git a/src/fs/syscalls/chown.rs b/src/fs/syscalls/chown.rs index d462da69..85bcf705 100644 --- a/src/fs/syscalls/chown.rs +++ b/src/fs/syscalls/chown.rs @@ -6,7 +6,10 @@ use libkernel::{ }, }; -use crate::{process::fd_table::Fd, sched::syscall_ctx::ProcessCtx}; +use crate::{ + process::{fd_table::Fd, inotify::notify_attrib}, + sched::syscall_ctx::ProcessCtx, +}; pub async fn sys_fchown(ctx: &ProcessCtx, fd: Fd, owner: i32, group: i32) -> Result { let task = ctx.shared().clone(); @@ -35,6 +38,7 @@ pub async fn sys_fchown(ctx: &ProcessCtx, fd: Fd, owner: i32, group: i32) -> Res } } inode.setattr(attr).await?; + notify_attrib(inode.id()).await; Ok(0) } diff --git a/src/fs/syscalls/setxattr.rs b/src/fs/syscalls/setxattr.rs index 737e935f..2b79326b 100644 --- a/src/fs/syscalls/setxattr.rs +++ b/src/fs/syscalls/setxattr.rs @@ -1,7 +1,7 @@ use crate::fs::VFS; use crate::memory::uaccess::copy_from_user_slice; use crate::memory::uaccess::cstr::UserCStr; -use crate::process::fd_table::Fd; +use crate::process::{fd_table::Fd, inotify::notify_attrib}; use crate::sched::syscall_ctx::ProcessCtx; use alloc::sync::Arc; use alloc::vec; @@ -47,6 +47,7 @@ async fn setxattr( flags.contains(SetXattrFlags::REPLACE), ) .await?; + notify_attrib(node.id()).await; Ok(size) } diff --git a/src/process/inotify.rs b/src/process/inotify.rs new file mode 100644 index 00000000..90356e68 --- /dev/null +++ b/src/process/inotify.rs @@ -0,0 +1,598 @@ +use alloc::{ + boxed::Box, + collections::{BTreeMap, VecDeque}, + sync::{Arc, Weak}, + vec, + vec::Vec, +}; +use async_trait::async_trait; +use core::{ + ffi::c_char, + future::Future, + mem::size_of, + pin::Pin, + sync::atomic::{AtomicU32, AtomicUsize, Ordering}, +}; +use libkernel::{ + error::{FsError, KernelError, Result}, + fs::{FileType, Inode, InodeId, OpenFlags, attr::AccessMode, path::Path}, + memory::address::{TUA, UA}, +}; + +use crate::{ + drivers::timer::sleep, + fs::{ + VFS, + fops::FileOps, + open_file::{FileCtx, OpenFile}, + }, + memory::uaccess::{copy_to_user, copy_to_user_slice, cstr::UserCStr}, + process::fd_table::{Fd, FdFlags}, + sched::syscall_ctx::ProcessCtx, + sync::{Mutex, OnceLock}, +}; + +pub const IN_ACCESS: u32 = 0x0000_0001; +pub const IN_MODIFY: u32 = 0x0000_0002; +pub const IN_ATTRIB: u32 = 0x0000_0004; +pub const IN_CLOSE_WRITE: u32 = 0x0000_0008; +pub const IN_CLOSE_NOWRITE: u32 = 0x0000_0010; +pub const IN_OPEN: u32 = 0x0000_0020; +pub const IN_MOVED_FROM: u32 = 0x0000_0040; +pub const IN_MOVED_TO: u32 = 0x0000_0080; +pub const IN_CREATE: u32 = 0x0000_0100; +pub const IN_DELETE: u32 = 0x0000_0200; +pub const IN_DELETE_SELF: u32 = 0x0000_0400; +pub const IN_MOVE_SELF: u32 = 0x0000_0800; +pub const IN_UNMOUNT: u32 = 0x0000_2000; +pub const IN_Q_OVERFLOW: u32 = 0x0000_4000; +pub const IN_IGNORED: u32 = 0x0000_8000; +pub const IN_ALL_EVENTS: u32 = 0x0000_0fff; + +pub const IN_ONLYDIR: u32 = 0x0100_0000; +pub const IN_DONT_FOLLOW: u32 = 0x0200_0000; +pub const IN_EXCL_UNLINK: u32 = 0x0400_0000; +pub const IN_MASK_CREATE: u32 = 0x1000_0000; +pub const IN_MASK_ADD: u32 = 0x2000_0000; +pub const IN_ISDIR: u32 = 0x4000_0000; +pub const IN_ONESHOT: u32 = 0x8000_0000; + +const INOTIFY_ALLOWED_MASK: u32 = IN_ALL_EVENTS + | IN_ONLYDIR + | IN_DONT_FOLLOW + | IN_EXCL_UNLINK + | IN_MASK_CREATE + | IN_MASK_ADD + | IN_ONESHOT; +const INOTIFY_STORED_MASK: u32 = IN_ALL_EVENTS | IN_EXCL_UNLINK | IN_ONESHOT; +const INOTIFY_READ_FLAGS: u32 = OpenFlags::O_NONBLOCK.bits() | OpenFlags::O_CLOEXEC.bits(); + +static NEXT_INOTIFY_INSTANCE_ID: AtomicUsize = AtomicUsize::new(1); +static NEXT_INOTIFY_COOKIE: AtomicU32 = AtomicU32::new(1); +static INOTIFY_REGISTRY: OnceLock> = OnceLock::new(); + +fn registry() -> &'static Mutex { + INOTIFY_REGISTRY.get_or_init(|| Mutex::new(InotifyRegistry::default())) +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct InotifyEvent { + pub wd: i32, + pub mask: u32, + pub cookie: u32, + pub len: u32, +} + +unsafe impl crate::memory::uaccess::UserCopyable for InotifyEvent {} + +#[derive(Clone)] +struct QueuedEvent { + header: InotifyEvent, + name: Vec, +} + +impl QueuedEvent { + fn new(wd: i32, mask: u32, cookie: u32, name: Option<&str>) -> Self { + let name = name.map(encode_name).unwrap_or_default(); + + Self { + header: InotifyEvent { + wd, + mask, + cookie, + len: name.len() as u32, + }, + name, + } + } + + fn total_len(&self) -> usize { + size_of::() + self.name.len() + } +} + +fn encode_name(name: &str) -> Vec { + let base_len = name.len() + 1; + let padded_len = (base_len + 3) & !3; + let mut buf = vec![0; padded_len]; + buf[..name.len()].copy_from_slice(name.as_bytes()); + buf +} + +#[derive(Clone, Copy)] +struct Watch { + wd: i32, + inode_id: InodeId, + mask: u32, +} + +#[derive(Default)] +struct InotifyState { + next_wd: i32, + watches_by_wd: BTreeMap, + wd_by_inode: BTreeMap, + queue: VecDeque, +} + +impl InotifyState { + fn alloc_wd(&mut self) -> i32 { + let wd = self.next_wd.max(1); + self.next_wd = wd.saturating_add(1); + wd + } +} + +pub struct Inotify { + inner: Arc, +} + +impl Inotify { + pub fn new() -> Self { + Self { + inner: Arc::new(InotifyInner { + id: NEXT_INOTIFY_INSTANCE_ID.fetch_add(1, Ordering::Relaxed), + state: Mutex::new(InotifyState::default()), + }), + } + } + + async fn add_watch(&mut self, inode: Arc, mask: u32) -> Result { + self.inner.add_watch(inode, mask).await + } + + async fn rm_watch(&mut self, wd: i32) -> Result<()> { + self.inner.rm_watch(wd).await + } + + async fn release_all_watches(&mut self) { + self.inner.release_all_watches().await; + } + + async fn read_impl(&mut self, buf: UA, count: usize, nonblock: bool) -> Result { + if count < size_of::() { + return Err(KernelError::InvalidValue); + } + + let mut dst = buf; + let mut bytes_read = 0usize; + + loop { + let event = { + let mut state = self.inner.state.lock().await; + let next_len = state.queue.front().map(|event| event.total_len()); + + match next_len { + Some(next_len) if next_len <= count - bytes_read => state.queue.pop_front(), + Some(_) if bytes_read == 0 => return Err(KernelError::InvalidValue), + Some(_) => return Ok(bytes_read), + None if bytes_read > 0 => return Ok(bytes_read), + None if nonblock => return Err(KernelError::TryAgain), + None => None, + } + }; + + let Some(event) = event else { + sleep(core::time::Duration::from_millis(10)).await; + continue; + }; + + copy_to_user(TUA::from_value(dst.value()), event.header).await?; + dst = dst.add_bytes(size_of::()); + if !event.name.is_empty() { + copy_to_user_slice(&event.name, dst).await?; + dst = dst.add_bytes(event.name.len()); + } + + bytes_read += event.total_len(); + if bytes_read == count { + return Ok(bytes_read); + } + } + } +} + +#[async_trait] +impl FileOps for Inotify { + async fn read(&mut self, ctx: &mut FileCtx, buf: UA, count: usize) -> Result { + self.read_impl(buf, count, ctx.flags.contains(OpenFlags::O_NONBLOCK)) + .await + } + + async fn readat(&mut self, buf: UA, count: usize, _offset: u64) -> Result { + self.read_impl(buf, count, false).await + } + + async fn writeat(&mut self, _buf: UA, _count: usize, _offset: u64) -> Result { + Err(KernelError::InvalidValue) + } + + fn poll_read_ready(&self) -> Pin> + 'static + Send>> { + let inner = self.inner.clone(); + Box::pin(async move { + loop { + if !inner.state.lock().await.queue.is_empty() { + return Ok(()); + } + sleep(core::time::Duration::from_millis(10)).await; + } + }) + } + + async fn release(&mut self, _ctx: &FileCtx) -> Result<()> { + self.release_all_watches().await; + Ok(()) + } + + fn as_inotify(&mut self) -> Option<&mut crate::process::inotify::Inotify> { + Some(self) + } +} + +struct InotifyInner { + id: usize, + state: Mutex, +} + +impl InotifyInner { + async fn add_watch(self: &Arc, inode: Arc, mask: u32) -> Result { + let inode_id = inode.id(); + let requested = mask & INOTIFY_STORED_MASK; + let mask_add = mask & IN_MASK_ADD != 0; + let mask_create = mask & IN_MASK_CREATE != 0; + + let existing_wd = { + let mut state = self.state.lock().await; + + if let Some(&wd) = state.wd_by_inode.get(&inode_id) { + let watch = state + .watches_by_wd + .get_mut(&wd) + .ok_or(KernelError::InvalidValue)?; + + if mask_create { + return Err(FsError::AlreadyExists.into()); + } + + watch.mask = if mask_add { + watch.mask | requested + } else { + requested + }; + + Some(wd) + } else { + None + } + }; + + if let Some(wd) = existing_wd { + return Ok(wd); + } + + let wd = { + let mut state = self.state.lock().await; + let wd = state.alloc_wd(); + let watch = Watch { + wd, + inode_id, + mask: requested, + }; + state.wd_by_inode.insert(inode_id, wd); + state.watches_by_wd.insert(wd, watch); + wd + }; + + registry_add_watch(inode_id, self.clone(), wd).await; + + Ok(wd) + } + + async fn rm_watch(&self, wd: i32) -> Result<()> { + let inode_id = { + let mut state = self.state.lock().await; + let watch = state + .watches_by_wd + .remove(&wd) + .ok_or(KernelError::InvalidValue)?; + state.wd_by_inode.remove(&watch.inode_id); + watch.inode_id + }; + + registry_remove_watch(inode_id, self.id, wd).await; + self.enqueue_unconditional(wd, IN_IGNORED, 0, None).await; + + Ok(()) + } + + async fn release_all_watches(&self) { + let removed = { + let mut state = self.state.lock().await; + let removed = state + .watches_by_wd + .values() + .map(|watch| (watch.inode_id, watch.wd)) + .collect::>(); + state.watches_by_wd.clear(); + state.wd_by_inode.clear(); + state.queue.clear(); + removed + }; + + for (inode_id, wd) in removed { + registry_remove_watch(inode_id, self.id, wd).await; + } + } + + async fn enqueue_filtered(&self, wd: i32, mask: u32, cookie: u32, name: Option<&str>) { + let should_enqueue = { + let state = self.state.lock().await; + let Some(watch) = state.watches_by_wd.get(&wd) else { + return; + }; + + let always = IN_IGNORED | IN_Q_OVERFLOW | IN_UNMOUNT; + if mask & always != 0 { + true + } else { + (watch.mask & IN_ALL_EVENTS) & (mask & IN_ALL_EVENTS) != 0 + } + }; + + if should_enqueue { + self.enqueue_unconditional(wd, mask, cookie, name).await; + } + } + + async fn enqueue_unconditional(&self, wd: i32, mask: u32, cookie: u32, name: Option<&str>) { + self.state + .lock() + .await + .queue + .push_back(QueuedEvent::new(wd, mask, cookie, name)); + } +} + +#[derive(Default)] +struct InotifyRegistry { + by_inode: BTreeMap>, +} + +#[derive(Clone)] +struct RegistryEntry { + instance_id: usize, + wd: i32, + weak: Weak, +} + +async fn registry_add_watch(inode_id: InodeId, inner: Arc, wd: i32) { + let mut reg = registry().lock().await; + let entries = reg.by_inode.entry(inode_id).or_default(); + + if entries + .iter() + .any(|entry| entry.instance_id == inner.id && entry.wd == wd) + { + return; + } + + entries.push(RegistryEntry { + instance_id: inner.id, + wd, + weak: Arc::downgrade(&inner), + }); +} + +async fn registry_remove_watch(inode_id: InodeId, instance_id: usize, wd: i32) { + let mut reg = registry().lock().await; + + let should_remove = if let Some(entries) = reg.by_inode.get_mut(&inode_id) { + entries.retain(|entry| !(entry.instance_id == instance_id && entry.wd == wd)); + entries.is_empty() + } else { + false + }; + + if should_remove { + reg.by_inode.remove(&inode_id); + } +} + +async fn dispatch_event(inode_id: InodeId, mask: u32, cookie: u32, name: Option<&str>) { + let deliveries = { + let mut reg = registry().lock().await; + let mut deliveries = Vec::new(); + + let should_remove = if let Some(entries) = reg.by_inode.get_mut(&inode_id) { + entries.retain(|entry| { + if let Some(inner) = entry.weak.upgrade() { + deliveries.push((entry.wd, inner)); + true + } else { + false + } + }); + entries.is_empty() + } else { + false + }; + + if should_remove { + reg.by_inode.remove(&inode_id); + } + + deliveries + }; + + for (wd, inner) in deliveries { + inner.enqueue_filtered(wd, mask, cookie, name).await; + } +} + +pub async fn notify_modify(inode_id: InodeId) { + dispatch_event(inode_id, IN_MODIFY, 0, None).await; +} + +pub async fn notify_attrib(inode_id: InodeId) { + dispatch_event(inode_id, IN_ATTRIB, 0, None).await; +} + +pub async fn notify_create(parent_inode_id: InodeId, name: &str, is_dir: bool) { + let mask = IN_CREATE | if is_dir { IN_ISDIR } else { 0 }; + dispatch_event(parent_inode_id, mask, 0, Some(name)).await; +} + +pub async fn notify_delete(parent_inode_id: InodeId, name: &str, is_dir: bool) { + let mask = IN_DELETE | if is_dir { IN_ISDIR } else { 0 }; + dispatch_event(parent_inode_id, mask, 0, Some(name)).await; +} + +pub async fn notify_delete_self(inode_id: InodeId, is_dir: bool) { + let mask = IN_DELETE_SELF | if is_dir { IN_ISDIR } else { 0 }; + dispatch_event(inode_id, mask, 0, None).await; +} + +pub async fn notify_move( + old_parent_inode_id: InodeId, + old_name: &str, + new_parent_inode_id: InodeId, + new_name: &str, + target_inode_id: InodeId, + is_dir: bool, +) { + let cookie = NEXT_INOTIFY_COOKIE.fetch_add(1, Ordering::Relaxed); + let dir_flag = if is_dir { IN_ISDIR } else { 0 }; + + dispatch_event( + old_parent_inode_id, + IN_MOVED_FROM | dir_flag, + cookie, + Some(old_name), + ) + .await; + dispatch_event( + new_parent_inode_id, + IN_MOVED_TO | dir_flag, + cookie, + Some(new_name), + ) + .await; + dispatch_event(target_inode_id, IN_MOVE_SELF | dir_flag, cookie, None).await; +} + +pub async fn sys_inotify_init1(ctx: &ProcessCtx, flags: u32) -> Result { + if flags & !INOTIFY_READ_FLAGS != 0 { + return Err(KernelError::InvalidValue); + } + + let file_flags = if flags & OpenFlags::O_NONBLOCK.bits() != 0 { + OpenFlags::O_NONBLOCK + } else { + OpenFlags::empty() + }; + let fd_flags = if flags & OpenFlags::O_CLOEXEC.bits() != 0 { + FdFlags::CLOEXEC + } else { + FdFlags::empty() + }; + + let file = Arc::new(OpenFile::new(Box::new(Inotify::new()), file_flags)); + let fd = ctx + .shared() + .fd_table + .lock_save_irq() + .insert_with_flags(file, fd_flags)?; + + Ok(fd.as_raw() as usize) +} + +pub async fn sys_inotify_add_watch( + ctx: &ProcessCtx, + fd: Fd, + pathname: TUA, + mask: u32, +) -> Result { + if mask & !INOTIFY_ALLOWED_MASK != 0 + || mask & IN_ALL_EVENTS == 0 + || (mask & IN_MASK_ADD != 0 && mask & IN_MASK_CREATE != 0) + { + return Err(KernelError::InvalidValue); + } + + let task = ctx.shared().clone(); + let mut buf = [0; 1024]; + let path = Path::new( + UserCStr::from_ptr(pathname) + .copy_from_user(&mut buf) + .await?, + ); + let cwd = task.cwd.lock_save_irq().0.clone(); + + let inode = if mask & IN_DONT_FOLLOW != 0 { + VFS.resolve_path_nofollow(path, cwd, &task).await? + } else { + VFS.resolve_path(path, cwd, &task).await? + }; + + let attr = inode.getattr().await?; + + if mask & IN_ONLYDIR != 0 && attr.file_type != FileType::Directory { + return Err(FsError::NotADirectory.into()); + } + + { + let creds = task.creds.lock_save_irq(); + if attr + .check_access(creds.euid(), creds.egid(), creds.caps(), AccessMode::R_OK) + .is_err() + { + return Err(FsError::PermissionDenied.into()); + } + } + + let inotify_file = task + .fd_table + .lock_save_irq() + .get(fd) + .ok_or(KernelError::BadFd)?; + + let (ops, _) = &mut *inotify_file.lock().await; + let inotify = ops.as_inotify().ok_or(KernelError::InvalidValue)?; + + Ok(inotify.add_watch(inode, mask).await? as usize) +} + +pub async fn sys_inotify_rm_watch(ctx: &ProcessCtx, fd: Fd, wd: i32) -> Result { + let task = ctx.shared().clone(); + let inotify_file = task + .fd_table + .lock_save_irq() + .get(fd) + .ok_or(KernelError::BadFd)?; + + let (ops, _) = &mut *inotify_file.lock().await; + let inotify = ops.as_inotify().ok_or(KernelError::InvalidValue)?; + inotify.rm_watch(wd).await?; + + Ok(0) +} diff --git a/src/process/mod.rs b/src/process/mod.rs index 95737a21..1640b4da 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -43,6 +43,7 @@ pub mod epoll; pub mod exec; pub mod exit; pub mod fd_table; +pub mod inotify; pub mod owned; pub mod pidfd; pub mod prctl; diff --git a/usertest/src/inotify.rs b/usertest/src/inotify.rs new file mode 100644 index 00000000..18797a0e --- /dev/null +++ b/usertest/src/inotify.rs @@ -0,0 +1,119 @@ +use crate::register_test; +use std::{ + ffi::CString, + fs, io, + os::fd::RawFd, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +const IN_NONBLOCK: i32 = libc::O_NONBLOCK; +const IN_CREATE: u32 = 0x0000_0100; +const IN_DELETE: u32 = 0x0000_0200; +const IN_IGNORED: u32 = 0x0000_8000; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct InotifyEvent { + wd: i32, + mask: u32, + cookie: u32, + len: u32, +} + +fn read_event(fd: RawFd) -> Option<(InotifyEvent, String)> { + let mut buf = [0u8; 256]; + + for _ in 0..50 { + let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) }; + if n > 0 { + assert!(n as usize >= core::mem::size_of::()); + + let header = unsafe { *(buf.as_ptr().cast::()) }; + let name = if header.len == 0 { + String::new() + } else { + let name_bytes = &buf[core::mem::size_of::() + ..core::mem::size_of::() + header.len as usize]; + let nul = name_bytes + .iter() + .position(|b| *b == 0) + .unwrap_or(name_bytes.len()); + String::from_utf8_lossy(&name_bytes[..nul]).into_owned() + }; + + return Some((header, name)); + } + + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::EAGAIN) { + thread::sleep(Duration::from_millis(10)); + continue; + } + + panic!("read failed: {err}"); + } + + None +} + +fn test_inotify_create_and_rm_watch() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = format!("/tmp/inotify_test_{unique}"); + let file_name = "created.txt"; + let file_path = format!("{dir}/{file_name}"); + + fs::create_dir(&dir).expect("create_dir failed"); + + unsafe { + let fd = libc::syscall(libc::SYS_inotify_init1, IN_NONBLOCK) as i32; + assert!( + fd >= 0, + "inotify_init1 failed: {}", + io::Error::last_os_error() + ); + + let c_dir = CString::new(dir.clone()).unwrap(); + let wd = libc::syscall( + libc::SYS_inotify_add_watch, + fd, + c_dir.as_ptr(), + (IN_CREATE | IN_DELETE) as libc::c_uint, + ) as i32; + assert!( + wd >= 0, + "inotify_add_watch failed: {}", + io::Error::last_os_error() + ); + + fs::write(&file_path, b"hello").expect("write failed"); + + let (event, name) = read_event(fd).expect("timed out waiting for inotify event"); + assert_eq!(event.wd, wd); + assert_ne!(event.mask & IN_CREATE, 0, "missing IN_CREATE: {event:?}"); + assert_eq!(name, file_name); + + let ret = libc::syscall(libc::SYS_inotify_rm_watch, fd, wd); + assert_eq!( + ret, + 0, + "inotify_rm_watch failed: {}", + io::Error::last_os_error() + ); + + let (event, name) = read_event(fd).expect("timed out waiting for IN_IGNORED"); + assert_eq!(event.wd, wd); + assert_ne!(event.mask & IN_IGNORED, 0, "missing IN_IGNORED: {event:?}"); + assert!(name.is_empty()); + + libc::close(fd); + } + + fs::remove_file(&file_path).expect("remove_file failed"); + fs::remove_dir(&dir).expect("remove_dir failed"); +} + +register_test!(test_inotify_create_and_rm_watch); diff --git a/usertest/src/main.rs b/usertest/src/main.rs index 3272f05d..c1d9a875 100644 --- a/usertest/src/main.rs +++ b/usertest/src/main.rs @@ -8,6 +8,7 @@ use std::{ mod epoll; mod fs; mod futex; +mod inotify; mod signalfd; mod signals; mod socket; From 2dd2ce3448f5351056b0fea9e07f9cb690ca4de0 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Tue, 12 May 2026 08:40:20 -0700 Subject: [PATCH 2/2] fix clippy --- src/process/inotify.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/process/inotify.rs b/src/process/inotify.rs index 90356e68..a65959d2 100644 --- a/src/process/inotify.rs +++ b/src/process/inotify.rs @@ -35,8 +35,11 @@ use crate::{ pub const IN_ACCESS: u32 = 0x0000_0001; pub const IN_MODIFY: u32 = 0x0000_0002; pub const IN_ATTRIB: u32 = 0x0000_0004; +#[expect(unused)] pub const IN_CLOSE_WRITE: u32 = 0x0000_0008; +#[expect(unused)] pub const IN_CLOSE_NOWRITE: u32 = 0x0000_0010; +#[expect(unused)] pub const IN_OPEN: u32 = 0x0000_0020; pub const IN_MOVED_FROM: u32 = 0x0000_0040; pub const IN_MOVED_TO: u32 = 0x0000_0080; @@ -449,6 +452,11 @@ async fn dispatch_event(inode_id: InodeId, mask: u32, cookie: u32, name: Option< } } +#[expect(unused)] +pub async fn notify_access(inode_id: InodeId) { + dispatch_event(inode_id, IN_ACCESS, 0, None).await; +} + pub async fn notify_modify(inode_id: InodeId) { dispatch_event(inode_id, IN_MODIFY, 0, None).await; }