Queue an unpaired IN_MOVED_FROM event, followed by an IN_MOVED_FROM and IN_MOVED_TO pair with the same source watch descriptor and name as the first event. Watch only IN_MOVE so creation of the replacement entry does not separate the two IN_MOVED_FROM events in the queue. Affected kernels coalesce the second IN_MOVED_FROM into the first, leaving the IN_MOVED_TO cookie without a matching IN_MOVED_FROM cookie. Check that all three events are retained, the two moves have distinct cookies, and the second IN_MOVED_FROM matches the IN_MOVED_TO cookie. Cover both regular files and directories. Suggested-by: Amir Goldstein Signed-off-by: Hungyu Lin --- runtest/syscalls | 1 + testcases/kernel/syscalls/inotify/.gitignore | 1 + testcases/kernel/syscalls/inotify/inotify13.c | 147 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 testcases/kernel/syscalls/inotify/inotify13.c diff --git a/runtest/syscalls b/runtest/syscalls index 2bdb4a9..e578bd9 100644 --- a/runtest/syscalls +++ b/runtest/syscalls @@ -677,6 +677,7 @@ inotify09 inotify09 inotify10 inotify10 inotify11 inotify11 inotify12 inotify12 +inotify13 inotify13 fanotify01 fanotify01 fanotify02 fanotify02 diff --git a/testcases/kernel/syscalls/inotify/.gitignore b/testcases/kernel/syscalls/inotify/.gitignore index f6e5c54..b597ea6 100644 --- a/testcases/kernel/syscalls/inotify/.gitignore +++ b/testcases/kernel/syscalls/inotify/.gitignore @@ -10,3 +10,4 @@ /inotify10 /inotify11 /inotify12 +/inotify13 diff --git a/testcases/kernel/syscalls/inotify/inotify13.c b/testcases/kernel/syscalls/inotify/inotify13.c new file mode 100644 index 0000000..4b172ba --- /dev/null +++ b/testcases/kernel/syscalls/inotify/inotify13.c @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2026 Hungyu Lin + */ + +/*\ + * Verify that coalescing does not lose an IN_MOVED_FROM event needed to + * match a later IN_MOVED_TO event by its rename cookie. + * + * Move an entry from a watched directory to an unwatched directory, recreate + * it under the same name, then move it to another watched directory. Watch + * only move events so the two IN_MOVED_FROM events are adjacent in the queue. + * Their cookies must differ, and the second must match the IN_MOVED_TO cookie. + * Test both regular files and directories without reading between renames. + */ + +#include "config.h" +#include +#include +#include "tst_test.h" +#include "inotify.h" + +#ifdef HAVE_SYS_INOTIFY_H +#include + +#define SOURCE "source/entry" +#define SAVED "unwatched/entry" +#define TARGET "target/entry" + +static int fd_notify = -1; + +static void create_entry(int directory) +{ + if (directory) + SAFE_MKDIR(SOURCE, 0700); + else + SAFE_TOUCH(SOURCE, 0600, NULL); +} + +static void verify_inotify(unsigned int directory) +{ + union { + struct inotify_event align; + char data[4096]; + } buf; + struct inotify_event *event, *events[3]; + unsigned int count = 0; + size_t pos = 0, len; + unsigned int from = IN_MOVED_FROM, to = IN_MOVED_TO; + int wd_source, wd_target; + const char *type = directory ? "directory" : "file"; + + if (directory) { + from |= IN_ISDIR; + to |= IN_ISDIR; + } + + fd_notify = SAFE_MYINOTIFY_INIT1(O_NONBLOCK); + wd_source = SAFE_MYINOTIFY_ADD_WATCH(fd_notify, "source", IN_MOVE); + wd_target = SAFE_MYINOTIFY_ADD_WATCH(fd_notify, "target", IN_MOVE); + + create_entry(directory); + SAFE_RENAME(SOURCE, SAVED); + create_entry(directory); + SAFE_RENAME(SOURCE, TARGET); + + len = SAFE_READ(SAFE_READ_ANY, fd_notify, buf.data, sizeof(buf.data)); + SAFE_CLOSE(fd_notify); + fd_notify = -1; + + if (directory) { + SAFE_RMDIR(SAVED); + SAFE_RMDIR(TARGET); + } else { + SAFE_UNLINK(SAVED); + SAFE_UNLINK(TARGET); + } + + while (pos < len) { + if (len - pos < sizeof(*event)) + tst_brk(TBROK, "Incomplete inotify event"); + + event = (struct inotify_event *)(buf.data + pos); + if (event->len > len - pos - sizeof(*event)) + tst_brk(TBROK, "Incomplete inotify event name"); + + tst_res(TINFO, "%s: wd=%d mask=%#x cookie=%u name=%.*s", + type, event->wd, event->mask, event->cookie, + (int)event->len, event->name); + if (event->len < sizeof("entry") || + memcmp(event->name, "entry", sizeof("entry"))) { + tst_res(TFAIL, "%s: unexpected rename event name", type); + return; + } + if (count < ARRAY_SIZE(events)) + events[count] = event; + count++; + pos += sizeof(*event) + event->len; + } + + if (count != ARRAY_SIZE(events)) { + tst_res(TFAIL, "%s: expected two FROM events and one TO, got %u", + type, count); + return; + } + + if (events[0]->wd != wd_source || events[0]->mask != from || + events[1]->wd != wd_source || events[1]->mask != from || + events[2]->wd != wd_target || events[2]->mask != to) { + tst_res(TFAIL, "%s: unexpected rename event sequence", type); + return; + } + + if (events[0]->cookie == events[1]->cookie || + events[1]->cookie != events[2]->cookie) { + tst_res(TFAIL, "%s: rename cookies do not identify the later pair", + type); + return; + } + + tst_res(TPASS, "%s: second FROM cookie matches the TO cookie", type); +} + +static void setup(void) +{ + SAFE_MKDIR("source", 0700); + SAFE_MKDIR("unwatched", 0700); + SAFE_MKDIR("target", 0700); +} + +static void cleanup(void) +{ + if (fd_notify >= 0) + SAFE_CLOSE(fd_notify); +} + +static struct tst_test test = { + .test = verify_inotify, + .tcnt = 2, + .setup = setup, + .cleanup = cleanup, + .needs_tmpdir = 1, +}; + +#else +TST_TEST_TCONF("system doesn't have required inotify support"); +#endif -- 2.43.0