Skip to content

Commit 7fd5187

Browse files
committed
Keep inotify directory baseline when a snapshot fails
A directory watch snapshots the child-name set on each change and diffs it against the previous baseline to recover the name kqueue omits. dir_snapshot returned an empty list on any failure (opendir error, a mid-read readdir error, or an allocation failure), which the diff could not tell apart from a genuinely empty directory. On a transient failure the IN_DELETE pass then saw every known child as missing and queued a spurious IN_DELETE for each, and the baseline was overwritten with the empty list, so every later change re-reported the whole directory as IN_CREATE. The corruption was permanent. Make dir_snapshot return bool: on failure it frees any partial result and reports false, and readdir read errors are now detected by clearing errno before each call. process_vnode_event only diffs against and advances to a snapshot that succeeded; on failure it keeps the previous baseline so the next successful snapshot reconciles whatever changed in between. The watch-add path documents that a failed initial snapshot is a best-effort empty baseline. Validated with make check on Apple Silicon; test-inotify still passes 6/6.
1 parent b6e2122 commit 7fd5187

1 file changed

Lines changed: 74 additions & 43 deletions

File tree

src/syscall/inotify.c

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -303,51 +303,74 @@ static void pipe_drain(inotify_instance_t *inst)
303303
;
304304
}
305305

306-
/* Snapshot the entry names of a directory (excluding "." and ".."). On return
307-
* *out is a malloc'd array of malloc'd strings with *n_out entries (free with
308-
* free_dir_snapshot). On any failure the snapshot is left empty.
306+
static void free_dir_snapshot(char **entries, int n)
307+
{
308+
if (!entries)
309+
return;
310+
for (int i = 0; i < n; i++)
311+
free(entries[i]);
312+
free(entries);
313+
}
314+
315+
/* List a directory's child names, excluding "." and "..", into the out array
316+
* (free with free_dir_snapshot). Returns false on any failure, leaving the
317+
* result empty -- which the caller must treat as distinct from a true return
318+
* with zero entries, since a failure mistaken for "empty" would diff every
319+
* known child as deleted.
309320
*/
310-
static void dir_snapshot(const char *path, char ***out, int *n_out)
321+
static bool dir_snapshot(const char *path, char ***out, int *n_out)
311322
{
312323
*out = NULL;
313324
*n_out = 0;
314325

315326
DIR *d = opendir(path);
316327
if (!d)
317-
return;
328+
return false;
318329

319330
char **names = NULL;
320331
int n = 0, cap = 0;
321-
struct dirent *de;
322-
while ((de = readdir(d)) != NULL) {
332+
bool ok = true;
333+
for (;;) {
334+
/* readdir returns NULL both at end-of-stream and on error; reset
335+
* errno immediately before each call so a non-zero errno afterwards
336+
* unambiguously signals a read error rather than EOF.
337+
*/
338+
errno = 0;
339+
struct dirent *de = readdir(d);
340+
if (!de) {
341+
if (errno != 0)
342+
ok = false;
343+
break;
344+
}
323345
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
324346
continue;
325347
if (n == cap) {
326348
int ncap = cap ? cap * 2 : 16;
327349
char **tmp = realloc(names, (size_t) ncap * sizeof(char *));
328-
if (!tmp)
350+
if (!tmp) {
351+
ok = false;
329352
break;
353+
}
330354
names = tmp;
331355
cap = ncap;
332356
}
333357
names[n] = strdup(de->d_name);
334-
if (!names[n])
358+
if (!names[n]) {
359+
ok = false;
335360
break;
361+
}
336362
n++;
337363
}
338364
closedir(d);
339365

366+
if (!ok) {
367+
free_dir_snapshot(names, n);
368+
return false;
369+
}
370+
340371
*out = names;
341372
*n_out = n;
342-
}
343-
344-
static void free_dir_snapshot(char **entries, int n)
345-
{
346-
if (!entries)
347-
return;
348-
for (int i = 0; i < n; i++)
349-
free(entries[i]);
350-
free(entries);
373+
return true;
351374
}
352375

353376
static bool snapshot_contains(char *const *entries, int n, const char *name)
@@ -379,33 +402,38 @@ static int process_vnode_event(inotify_instance_t *inst,
379402
if (w->is_dir && (fflags & NOTE_WRITE) && w->path) {
380403
char **now = NULL;
381404
int now_n = 0;
382-
dir_snapshot(w->path, &now, &now_n);
383-
384-
for (int j = 0; j < now_n && !overflow; j++) {
385-
if ((w->mask & IN_CREATE) &&
386-
!snapshot_contains(w->entries, w->n_entries, now[j])) {
387-
if (queue_event(inst, w->wd, IN_CREATE, 0, now[j]) < 0)
388-
overflow = true;
389-
else
390-
queued++;
405+
/* Only diff against -- and advance to -- a snapshot that succeeded.
406+
* On failure keep the previous baseline; the next successful snapshot
407+
* reconciles whatever changed in between.
408+
*/
409+
if (dir_snapshot(w->path, &now, &now_n)) {
410+
for (int j = 0; j < now_n && !overflow; j++) {
411+
if ((w->mask & IN_CREATE) &&
412+
!snapshot_contains(w->entries, w->n_entries, now[j])) {
413+
if (queue_event(inst, w->wd, IN_CREATE, 0, now[j]) < 0)
414+
overflow = true;
415+
else
416+
queued++;
417+
}
391418
}
392-
}
393-
for (int j = 0; j < w->n_entries && !overflow; j++) {
394-
if ((w->mask & IN_DELETE) &&
395-
!snapshot_contains(now, now_n, w->entries[j])) {
396-
if (queue_event(inst, w->wd, IN_DELETE, 0, w->entries[j]) < 0)
397-
overflow = true;
398-
else
399-
queued++;
419+
for (int j = 0; j < w->n_entries && !overflow; j++) {
420+
if ((w->mask & IN_DELETE) &&
421+
!snapshot_contains(now, now_n, w->entries[j])) {
422+
if (queue_event(inst, w->wd, IN_DELETE, 0, w->entries[j]) <
423+
0)
424+
overflow = true;
425+
else
426+
queued++;
427+
}
400428
}
401-
}
402429

403-
/* Advance the snapshot regardless: the directory state has moved on,
404-
* and any names dropped under overflow are covered by IN_Q_OVERFLOW.
405-
*/
406-
free_dir_snapshot(w->entries, w->n_entries);
407-
w->entries = now;
408-
w->n_entries = now_n;
430+
/* Advance the snapshot: the directory state has moved on, and any
431+
* names dropped under overflow are covered by IN_Q_OVERFLOW.
432+
*/
433+
free_dir_snapshot(w->entries, w->n_entries);
434+
w->entries = now;
435+
w->n_entries = now_n;
436+
}
409437
}
410438

411439
if (!overflow) {
@@ -564,7 +592,10 @@ int64_t sys_inotify_add_watch(guest_t *g,
564592
int wn = 0;
565593
if (is_dir) {
566594
wpath = strdup(path);
567-
dir_snapshot(path, &wentries, &wn);
595+
/* Best-effort: a failed listing starts the watch with an empty
596+
* baseline, which is the only state worth recording at add time.
597+
*/
598+
(void) dir_snapshot(path, &wentries, &wn);
568599
}
569600

570601
pthread_mutex_lock(&inotify_lock);

0 commit comments

Comments
 (0)