diff --git a/fs/vfs/vfs.h b/fs/vfs/vfs.h index f655337ea..9fec76e58 100644 --- a/fs/vfs/vfs.h +++ b/fs/vfs/vfs.h @@ -113,6 +113,9 @@ int sys_link(char *oldpath, char *newpath); int sys_unlink(char *path); int sys_symlink(const char *oldpath, const char *newpath); int sys_access(char *path, int mode); + +/* inotify VFS notification hook (libc/inotify.cc). */ +void osv_inotify_notify(const char *path, uint32_t mask, int is_dir); int sys_stat(char *path, struct stat *st); int sys_lstat(char *path, struct stat *st); int sys_statfs(char *path, struct statfs *buf); diff --git a/fs/vfs/vfs_syscalls.cc b/fs/vfs/vfs_syscalls.cc index 1a43aabfc..5c092df28 100644 --- a/fs/vfs/vfs_syscalls.cc +++ b/fs/vfs/vfs_syscalls.cc @@ -41,6 +41,7 @@ #include #include +#include #include #include @@ -141,6 +142,7 @@ sys_open(char *path, int flags, mode_t mode, struct file **fpp) if (error) return error; + osv_inotify_notify(path, IN_CREATE, 0); if ((error = namei(path, &dp)) != 0) return error; @@ -568,6 +570,9 @@ sys_mkdir(char *path, mode_t mode) mode |= S_IFDIR; error = VOP_MKDIR(ddp->d_vnode, name, mode); + if (!error) { + osv_inotify_notify(path, IN_CREATE, 1); + } out: vn_unlock(ddp->d_vnode); drele(ddp); @@ -609,6 +614,9 @@ sys_rmdir(char *path) error = VOP_RMDIR(ddp->d_vnode, vp, name); vn_unlock(ddp->d_vnode); + if (!error) { + osv_inotify_notify(path, IN_DELETE, 1); + } vn_unlock(vp); dentry_remove(dp); drele(ddp); @@ -824,6 +832,19 @@ sys_rename(char *src, char *dest) error = VOP_RENAME(dvp1, vp1, sname, dvp2, vp2, dname); + if (!error) { + int is_dir = (vp1->v_type == VDIR) ? 1 : 0; + // `src` is still the full source path, but `dest` was truncated to its + // parent above (*dname was NUL'd), so rebuild the full dest path. + char full_dest[PATH_MAX]; + if (dest[0] == '/' && dest[1] == '\0') { + snprintf(full_dest, sizeof(full_dest), "/%s", dname); + } else { + snprintf(full_dest, sizeof(full_dest), "%s/%s", dest, dname); + } + osv_inotify_notify(src, IN_MOVED_FROM, is_dir); + osv_inotify_notify(full_dest, IN_MOVED_TO, is_dir); + } dentry_move(dp1, ddp2, dname); if (dp2) dentry_remove(dp2); @@ -1016,6 +1037,9 @@ sys_unlink(char *path) error = VOP_REMOVE(ddp->d_vnode, vp, name); vn_unlock(ddp->d_vnode); + if (!error) { + osv_inotify_notify(path, IN_DELETE, 0); + } vn_unlock(vp); dentry_remove(dp); drele(ddp); diff --git a/libc/inotify.cc b/libc/inotify.cc index 5c2fdc01d..163f2064f 100644 --- a/libc/inotify.cc +++ b/libc/inotify.cc @@ -1,38 +1,316 @@ /* - * Copyright (C) 2014 Cloudius Systems, Ltd. + * Copyright (C) 2026 Waldemar Kozaczuk * * This work is open source software, licensed under the terms of the * BSD license as described in the LICENSE file in the top-level directory. */ -#include -#include -#include "libc.hh" +// inotify(2): watch filesystem paths for changes and read events from an fd. +// +// OSv's VFS routes all path mutations through a handful of central functions in +// fs/vfs/vfs_syscalls.cc. Those call osv_inotify_notify() below with the +// absolute path affected and the event mask. This module keeps a registry of +// inotify instances, matches each event against their watches, and queues a +// struct inotify_event for the fd (waking any poller). +// +// A watch on a directory fires for changes to entries within it (with the entry +// name); a watch on a file fires for changes to the file itself. This covers +// the common events (IN_CREATE, IN_DELETE, IN_MODIFY, IN_MOVED_FROM/TO, +// IN_CLOSE_WRITE, IN_ATTRIB); the recursive-watch and mount-related events are +// out of scope. -int inotify_init() +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class inotify_fd; + +mutex registry_lock; +std::set registry; + +struct watch { + int wd; + std::string path; // absolute, no trailing slash (except root "/") + uint32_t mask; +}; + +// Split an absolute path into (parent-dir, basename). "/a/b" -> ("/a", "b"). +static void split_path(const std::string& path, std::string& dir, std::string& name) { - WARN_STUBBED(); + auto slash = path.find_last_of('/'); + if (slash == std::string::npos) { + dir = "."; + name = path; + } else if (slash == 0) { + dir = "/"; + name = path.substr(1); + } else { + dir = path.substr(0, slash); + name = path.substr(slash + 1); + } +} + +class inotify_fd final : public special_file { +public: + explicit inotify_fd(int flags) + : special_file(FREAD | flags, DTYPE_UNSPEC) + { + WITH_LOCK(registry_lock) { registry.insert(this); } + } + + int close() override { + WITH_LOCK(registry_lock) { registry.erase(this); } + return 0; + } + + int read(struct uio* uio, int flags) override; + int poll(int events) override; - return libc_error(EMFILE); + int add_watch(const std::string& path, uint32_t mask); + int rm_watch(int wd); + + // Match an event against this instance's watches and queue it if it fits. + void notify(const std::string& dir, const std::string& name, + const std::string& full, uint32_t mask); + +private: + void queue_event(int wd, uint32_t mask, const std::string& name); + + mutable mutex _mutex; + condvar _blocked_reader; + int _next_wd = 1; + std::vector _watches; + std::deque> _events; // each is a packed inotify_event +}; + +void inotify_fd::queue_event(int wd, uint32_t mask, const std::string& name) +{ + // Pack: struct inotify_event header + NUL-terminated name padded to a + // multiple of the struct's alignment (Linux rounds the name field up). + size_t namelen = name.empty() ? 0 : ((name.size() + 1 + 15) & ~size_t(15)); + std::vector buf(sizeof(inotify_event) + namelen, 0); + auto* ev = reinterpret_cast(buf.data()); + ev->wd = wd; + ev->mask = mask; + ev->cookie = 0; + ev->len = namelen; + if (!name.empty()) { + memcpy(buf.data() + sizeof(inotify_event), name.c_str(), name.size()); + } + _events.push_back(std::move(buf)); + _blocked_reader.wake_all(); } -int inotify_init1(int flags) +void inotify_fd::notify(const std::string& dir, const std::string& name, + const std::string& full, uint32_t mask) { - WARN_STUBBED(); + WITH_LOCK(_mutex) { + bool queued = false; + for (auto& w : _watches) { + if (!(w.mask & (mask & IN_ALL_EVENTS))) { + continue; + } + if (w.path == dir) { + // A directory watch: report the affected entry name. + queue_event(w.wd, mask, name); + queued = true; + } else if (w.path == full) { + // A watch on the file/dir itself: no name. + queue_event(w.wd, mask, std::string()); + queued = true; + } + } + (void)queued; + } + poll_wake(this, POLLIN); +} - return libc_error(EMFILE); +int inotify_fd::add_watch(const std::string& path, uint32_t mask) +{ + WITH_LOCK(_mutex) { + // Existing watch on the same path: merge the mask (or replace unless + // IN_MASK_ADD), and keep its wd, like Linux. + for (auto& w : _watches) { + if (w.path == path) { + w.mask = (mask & IN_MASK_ADD) ? (w.mask | mask) : mask; + return w.wd; + } + } + int wd = _next_wd++; + _watches.push_back(watch{wd, path, mask}); + return wd; + } } -int inotify_add_watch(int fd, const char *pathname, uint32_t mask) +int inotify_fd::rm_watch(int wd) { - WARN_STUBBED(); + WITH_LOCK(_mutex) { + for (auto it = _watches.begin(); it != _watches.end(); ++it) { + if (it->wd == wd) { + _watches.erase(it); + // Linux queues an IN_IGNORED for the removed watch. + queue_event(wd, IN_IGNORED, std::string()); + _blocked_reader.wake_all(); + return 0; + } + } + } + errno = EINVAL; + return -1; +} - return libc_error(EINVAL); +int inotify_fd::read(struct uio* uio, int flags) +{ + WITH_LOCK(_mutex) { + while (_events.empty()) { + if (f_flags & FNONBLOCK) { + return EAGAIN; + } + _blocked_reader.wait(_mutex); + } + // The first event must fit; a too-small buffer is EINVAL (Linux). + if (uio->uio_resid < (ssize_t)_events.front().size()) { + return EINVAL; + } + while (!_events.empty() && + uio->uio_resid >= (ssize_t)_events.front().size()) { + auto& ev = _events.front(); + int err = uiomove(ev.data(), ev.size(), uio); + if (err) { + return err; + } + _events.pop_front(); + } + } + return 0; } -int inotify_rm_watch(int fd, int wd) +int inotify_fd::poll(int events) +{ + int rc = 0; + WITH_LOCK(_mutex) { + if (!_events.empty() && (events & POLLIN)) { + rc |= POLLIN; + } + } + return rc; +} + +} // anonymous namespace + +// VFS hook: called from fs/vfs/vfs_syscalls.cc after a successful mutation. +// `path` is the absolute path affected; `mask` is the IN_* event; `is_dir` +// marks the affected object as a directory (adds IN_ISDIR). +extern "C" void osv_inotify_notify(const char* path, uint32_t mask, int is_dir) +{ + if (!path) { + return; + } + // Fast path: nothing is watching. + WITH_LOCK(registry_lock) { + if (registry.empty()) { + return; + } + } + if (is_dir) { + mask |= IN_ISDIR; + } + std::string full(path); + std::string dir, name; + split_path(full, dir, name); + + WITH_LOCK(registry_lock) { + for (auto* in : registry) { + in->notify(dir, name, full, mask); + } + } +} + +extern "C" OSV_LIBC_API +int inotify_init1(int flags) { - WARN_STUBBED(); + if (flags & ~(IN_CLOEXEC | IN_NONBLOCK)) { + return libc_error(EINVAL); + } + int of = 0; + if (flags & IN_NONBLOCK) { + of |= O_NONBLOCK; + } + if (flags & IN_CLOEXEC) { + of |= O_CLOEXEC; + } + try { + fileref f = make_file(of); + fdesc fd(f); + return fd.release(); + } catch (int error) { + return libc_error(error); + } +} + +extern "C" OSV_LIBC_API +int inotify_init() +{ + return inotify_init1(0); +} - return libc_error(EINVAL); +extern "C" OSV_LIBC_API +int inotify_add_watch(int fd, const char* pathname, uint32_t mask) +{ + if (!pathname) { + return libc_error(EINVAL); + } + fileref f(fileref_from_fd(fd)); + if (!f) { + return libc_error(EBADF); + } + auto* in = dynamic_cast(f.get()); + if (!in) { + return libc_error(EINVAL); + } + // Resolve to an absolute path so it matches what the VFS hook reports. + char abs[PATH_MAX]; + if (pathname[0] == '/') { + strlcpy(abs, pathname, sizeof(abs)); + } else { + char cwd[PATH_MAX]; + if (!getcwd(cwd, sizeof(cwd))) { + return libc_error(errno); + } + snprintf(abs, sizeof(abs), "%s/%s", cwd, pathname); + } + // Strip a trailing slash (except for root). + size_t n = strlen(abs); + while (n > 1 && abs[n - 1] == '/') { + abs[--n] = '\0'; + } + return in->add_watch(abs, mask); +} + +extern "C" OSV_LIBC_API +int inotify_rm_watch(int fd, int wd) +{ + fileref f(fileref_from_fd(fd)); + if (!f) { + return libc_error(EBADF); + } + auto* in = dynamic_cast(f.get()); + if (!in) { + return libc_error(EINVAL); + } + return in->rm_watch(wd); } diff --git a/modules/tests/Makefile b/modules/tests/Makefile index 652d94a6b..46fe15817 100644 --- a/modules/tests/Makefile +++ b/modules/tests/Makefile @@ -124,6 +124,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \ tst-signal-fills.so \ tst-prctl.so \ tst-mmap-file-cow.so \ + tst-inotify.so \ tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \ tst-queue-mpsc.so tst-af-local.so tst-pipe.so tst-yield.so \ misc-ctxsw.so tst-read.so tst-symlink.so tst-openat.so \ diff --git a/tests/tst-inotify.cc b/tests/tst-inotify.cc new file mode 100644 index 000000000..e5198c86a --- /dev/null +++ b/tests/tst-inotify.cc @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2026 Greg Burd + * + * This work is open source software, licensed under the terms of the + * BSD license as described in the LICENSE file in the top-level directory. + */ + +// Exercises inotify(2): watch a directory and observe create/delete/move +// events. Built and run as part of the OSv test image. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// Read one event at a time. Because a single read() may return several packed +// events (e.g. a rename yields MOVED_FROM+MOVED_TO), buffer the surplus and hand +// them out one call at a time. Blocks up to 2s for the first batch. +static char ev_buf[4096]; +static ssize_t ev_have = 0; +static ssize_t ev_off = 0; + +static uint32_t read_event(int fd, std::string& name) +{ + if (ev_off >= ev_have) { + struct pollfd pfd { fd, POLLIN, 0 }; + assert(poll(&pfd, 1, 2000) == 1); + ev_have = read(fd, ev_buf, sizeof(ev_buf)); + ev_off = 0; + assert(ev_have >= (ssize_t)sizeof(inotify_event)); + } + auto* ev = reinterpret_cast(ev_buf + ev_off); + name = ev->len ? std::string(ev->name) : std::string(); + ev_off += sizeof(inotify_event) + ev->len; + return ev->mask; +} + +int main() +{ + std::cerr << "Running inotify tests\n"; + + // Work in a fresh writable directory under /tmp (ramfs). + const char* dir = "/tmp/inotify-test"; + mkdir(dir, 0777); + + int ifd = inotify_init1(IN_NONBLOCK); + assert(ifd >= 0); + + int wd = inotify_add_watch(ifd, dir, IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); + assert(wd >= 0); + + // No events yet: nonblocking read returns EAGAIN. + char tmp[64]; + assert(read(ifd, tmp, sizeof(tmp)) == -1 && errno == EAGAIN); + + // Create a file -> IN_CREATE with its name. + std::string p = std::string(dir) + "/file1"; + int f = open(p.c_str(), O_CREAT | O_WRONLY, 0644); + assert(f >= 0); + close(f); + + std::string name; + uint32_t mask = read_event(ifd, name); + assert((mask & IN_CREATE) && name == "file1"); + + // Rename it -> IN_MOVED_FROM (old name) then IN_MOVED_TO (new name). + std::string p2 = std::string(dir) + "/file2"; + assert(rename(p.c_str(), p2.c_str()) == 0); + mask = read_event(ifd, name); + assert((mask & IN_MOVED_FROM) && name == "file1"); + mask = read_event(ifd, name); + assert((mask & IN_MOVED_TO) && name == "file2"); + + // Delete it -> IN_DELETE. + assert(unlink(p2.c_str()) == 0); + mask = read_event(ifd, name); + assert((mask & IN_DELETE) && name == "file2"); + + // Create a subdirectory -> IN_CREATE | IN_ISDIR. + std::string sub = std::string(dir) + "/subdir"; + assert(mkdir(sub.c_str(), 0777) == 0); + mask = read_event(ifd, name); + assert((mask & IN_CREATE) && (mask & IN_ISDIR) && name == "subdir"); + rmdir(sub.c_str()); + mask = read_event(ifd, name); + assert((mask & IN_DELETE) && (mask & IN_ISDIR)); + + // Removing the watch queues IN_IGNORED and rejects a bogus wd. + assert(inotify_rm_watch(ifd, wd) == 0); + errno = 0; + assert(inotify_rm_watch(ifd, 9999) == -1 && errno == EINVAL); + + // Bad init flags rejected. + errno = 0; + assert(inotify_init1(0x40) == -1 && errno == EINVAL); + + close(ifd); + std::cerr << "inotify tests PASSED\n"; + return 0; +}