Skip to content

Commit c0a1182

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 14b690b commit c0a1182

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
@@ -302,51 +302,74 @@ static void pipe_drain(inotify_instance_t *inst)
302302
;
303303
}
304304

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

314325
DIR *d = opendir(path);
315326
if (!d)
316-
return;
327+
return false;
317328

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

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

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

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

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

569600
pthread_mutex_lock(&inotify_lock);

0 commit comments

Comments
 (0)