Skip to content

inotify directory watches deliver IN_CREATE/IN_DELETE events with an empty name[] field #55

@chenhunghan

Description

@chenhunghan

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):

IN_CREATE len=0 name=''

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions