Summary
inotify directory watches deliver IN_CREATE / IN_DELETE events with an empty name[] field (len == 0). On real Linux, directory-watch events for create/delete/move always name the affected child entry. Because the emulated events are nameless, any consumer that filters or dispatches on the entry name (the entire fsnotify / watchdog / chokidar / inotify-tools class of programs) silently drops them — the event is delivered but carries no usable information about which file changed.
Reproduction
Cross-compile and run the minimal program below under the unpatched and patched binaries:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <unistd.h>
int main(void) {
char dir[] = "/tmp/repro-ino-XXXXXX";
if (!mkdtemp(dir)) return 2;
int fd = inotify_init1(IN_NONBLOCK);
int wd = inotify_add_watch(fd, dir, IN_CREATE);
char path[300];
snprintf(path, sizeof(path), "%s/manifest.yaml", dir);
close(open(path, O_WRONLY | O_CREAT | O_EXCL, 0644));
for (int a = 0; a < 50; a++) {
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
for (ssize_t off = 0; n > 0 && off + (ssize_t)sizeof(struct inotify_event) <= n;) {
struct inotify_event *ev = (struct inotify_event *)(buf + off);
if (ev->mask & IN_CREATE)
printf("IN_CREATE len=%u name='%s'\n", ev->len, ev->len ? ev->name : "");
off += (ssize_t)sizeof(struct inotify_event) + ev->len;
}
usleep(20000);
}
return 0;
}
aarch64-linux-gnu-gcc -D_GNU_SOURCE -static -O2 -o /tmp/repro-inotify /tmp/repro-inotify.c
elfuse -- /tmp/repro-inotify
Expected vs Actual
Expected (matches real Linux): IN_CREATE len=16 name='manifest.yaml'
Actual (unpatched):
The event fires but name[] is empty, so the consumer cannot tell which child was created.
Root cause
src/syscall/inotify.c. kqueue's EVFILT_VNODE NOTE_WRITE reports that a watched directory changed but not which child. The emulation queues the event directly with a NULL name:
collect_events() — src/syscall/inotify.c:331: queue_event(inst, w->wd, in_mask, 0, NULL)
inotify_read() — src/syscall/inotify.c:630: queue_event(inst, w->wd, in_mask, 0, NULL)
This is by design per the comments at notes_to_in_mask (src/syscall/inotify.c:206-208) and collect_events (src/syscall/inotify.c:327-330), which note the filename is omitted because EVFILT_VNODE doesn't report the child. The fix is to recover the name out-of-band: snapshot the directory's entry names at inotify_add_watch time and, on each NOTE_WRITE, re-list and diff to emit a named IN_CREATE per added entry and named IN_DELETE per removed entry.
Affected programs
- Go fsnotify (
github.com/fsnotify/fsnotify) — Linux backend reads name[] to build the changed path; nameless dir events produce empty Event.Name and are filtered out.
- inotify-tools (
inotifywait/inotifywatch) — %f/per-name filtering relies on name[].
- Node.js
fs.watch / chokidar (webpack/vite/VS Code watchers) — map name[] to the changed file path; without it, recursive/dir watching and hot-reload don't fire for the right file.
- Python
watchdog — builds src_path from dir path + name[]; empty name breaks file-level dispatch and pattern matching.
- systemd
.path units / incron — match on the created/deleted entry name.
- Any drop-folder / spool-directory watcher that acts only on new files of a given name/extension.
Fix
Resolved by snapshot+diff of directory entry names, sharing one process_vnode_event() helper across the blocking-read and non-blocking-collect paths; snapshot allocated on add_watch, freed on rm_watch/close. Regression test added that watches a fresh dir, creates a child, and asserts a named IN_CREATE is delivered (fails before the fix).
First observed while running a Go file-watching workload (k0s manifest applier) whose fsnotify-based directory watch silently never picked up newly written files.
Summary
inotify directory watches deliver
IN_CREATE/IN_DELETEevents with an emptyname[]field (len == 0). On real Linux, directory-watch events for create/delete/move always name the affected child entry. Because the emulated events are nameless, any consumer that filters or dispatches on the entry name (the entire fsnotify / watchdog / chokidar / inotify-tools class of programs) silently drops them — the event is delivered but carries no usable information about which file changed.Reproduction
Cross-compile and run the minimal program below under the unpatched and patched binaries:
Expected vs Actual
Expected (matches real Linux):
IN_CREATE len=16 name='manifest.yaml'Actual (unpatched):
The event fires but
name[]is empty, so the consumer cannot tell which child was created.Root cause
src/syscall/inotify.c. kqueue'sEVFILT_VNODENOTE_WRITEreports that a watched directory changed but not which child. The emulation queues the event directly with aNULLname:collect_events()—src/syscall/inotify.c:331:queue_event(inst, w->wd, in_mask, 0, NULL)inotify_read()—src/syscall/inotify.c:630:queue_event(inst, w->wd, in_mask, 0, NULL)This is by design per the comments at
notes_to_in_mask(src/syscall/inotify.c:206-208) andcollect_events(src/syscall/inotify.c:327-330), which note the filename is omitted becauseEVFILT_VNODEdoesn't report the child. The fix is to recover the name out-of-band: snapshot the directory's entry names atinotify_add_watchtime and, on eachNOTE_WRITE, re-list and diff to emit a namedIN_CREATEper added entry and namedIN_DELETEper removed entry.Affected programs
github.com/fsnotify/fsnotify) — Linux backend readsname[]to build the changed path; nameless dir events produce emptyEvent.Nameand are filtered out.inotifywait/inotifywatch) —%f/per-name filtering relies onname[].fs.watch/ chokidar (webpack/vite/VS Code watchers) — mapname[]to the changed file path; without it, recursive/dir watching and hot-reload don't fire for the right file.watchdog— buildssrc_pathfrom dir path +name[]; empty name breaks file-level dispatch and pattern matching..pathunits / incron — match on the created/deleted entry name.Fix
Resolved by snapshot+diff of directory entry names, sharing one
process_vnode_event()helper across the blocking-read and non-blocking-collect paths; snapshot allocated onadd_watch, freed onrm_watch/close. Regression test added that watches a fresh dir, creates a child, and asserts a namedIN_CREATEis delivered (fails before the fix).First observed while running a Go file-watching workload (k0s manifest applier) whose fsnotify-based directory watch silently never picked up newly written files.