[LTP] [PATCH v3] inotify13: check rename cookie pairing after adjacent moves

Amir Goldstein amir73il@gmail.com
Thu Oct 29 19:50:22 CET 2026


On Thu, Oct 29, 2026 at 6:49 PM Hungyu Lin <dennylin0707@gmail.com> wrote:
>
> 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 <amir73il@gmail.com>
> Signed-off-by: Hungyu Lin <dennylin0707@gmail.com>
> ---
> Changes in v3:
> - Check event fields separately, starting with the mask, and report specific
>   TFAIL diagnostics with actual and expected values. Log correct events
>   with TINFO only.
> - Report missing and extra events directly instead of using a generic
>   failure flag.
> - Use delete_entry() for both cleanup paths, as suggested by Amir.
> - Use a file/directory test-case table and derive the test count from it.
> - Clarify the operation sequence and affected-kernel history, the truncated
>   payload diagnostic, and both cookie relations in the success message.
> - Handle an empty nonblocking queue as missing events rather than TBROK.
>
> Built with GCC 13.3.0 and checked with LTP checkpatch and sparse. On ARM64
> QEMU, the test reports 0 pass / 2 fail on the baseline kernel and 2 pass /
> 0 fail with either kernel fix. With -i 20, the results are 0 pass / 40
> fail before the fix and 40 pass / 0 fail afterwards.
>
>  runtest/syscalls                              |   1 +
>  testcases/kernel/syscalls/inotify/.gitignore  |   1 +
>  testcases/kernel/syscalls/inotify/inotify13.c | 217 ++++++++++++++++++++++++++
>  3 files changed, 219 insertions(+)
>  create mode 100644 testcases/kernel/syscalls/inotify/inotify13.c
>
> diff --git a/runtest/syscalls b/runtest/syscalls
> index 2bdb4a9a2b4801d03d93efea686124530f5ce2d5..e578bd92d7e7ded5c0ca2f95dc87fce814d733cc 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 f6e5c546ac4032766cee9f16b3689be5d0302d37..b597ea63f493b2da2a58017d3eb7b16a7b033fc5 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 0000000000000000000000000000000000000000..2ea5b5d4aeff9b368e5fa79ec18207035567a34b
> --- /dev/null
> +++ b/testcases/kernel/syscalls/inotify/inotify13.c
> @@ -0,0 +1,217 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2026 Hungyu Lin <dennylin0707@gmail.com>
> + */
> +
> +/*\
> + * Verify that two IN_MOVED_FROM events that differ only in their rename
> + * cookie are not coalesced.
> + *
> + * inotify(7) allows coalescing only of events with identical wd, mask,
> + * cookie and name. A regression introduced in v2.6.31 dropped the cookie
> + * comparison. On affected kernels, the second IN_MOVED_FROM is coalesced
> + * into the first, leaving the following IN_MOVED_TO without a matching
> + * IN_MOVED_FROM cookie.
> + *
> + * With source/ and target/ watched for IN_MOVE, and without reading the queue:
> + *
> + * 1. Move source/entry to unwatched/ (IN_MOVED_FROM, cookie A).
> + * 2. Recreate source/entry.
> + * 3. Move source/entry to target/ (IN_MOVED_FROM and IN_MOVED_TO, cookie B).
> + *
> + * All three events must be queued, with A != B. IN_CREATE is not watched, so
> + * the two IN_MOVED_FROM events are adjacent; the queue is not read in between,
> + * because only the last unread event can be merged with a new one. Both a
> + * regular file and a directory are tested.
> + */
> +
> +#include "config.h"
> +#include <fcntl.h>
> +#include <string.h>
> +#include "tst_test.h"
> +#include "inotify.h"
> +
> +#ifdef HAVE_SYS_INOTIFY_H
> +#include <sys/inotify.h>
> +
> +#define SOURCE "source/entry"
> +#define UNWATCHED "unwatched/entry"
> +#define TARGET "target/entry"
> +
> +struct event_t {
> +       const char *name;
> +       unsigned int mask;
> +       int wd;
> +};
> +
> +static struct tcase {
> +       const char *type;
> +       unsigned int isdir;
> +} tcases[] = {
> +       { "file", 0 },
> +       { "directory", IN_ISDIR },
> +};
> +
> +static int fd_notify = -1;
> +
> +static void create_entry(unsigned int isdir, const char *path)
> +{
> +       if (isdir)
> +               SAFE_MKDIR(path, 0700);
> +       else
> +               SAFE_TOUCH(path, 0600, NULL);
> +}
> +
> +static void delete_entry(unsigned int isdir, const char *path)
> +{
> +       if (isdir)
> +               SAFE_RMDIR(path);
> +       else
> +               SAFE_UNLINK(path);
> +}
> +
> +static void verify_inotify(unsigned int n)
> +{
> +       const char *type = tcases[n].type;
> +       unsigned int isdir = tcases[n].isdir;
> +       struct event_t event_set[] = {
> +               { "entry", IN_MOVED_FROM | isdir, 0 },
> +               { "entry", IN_MOVED_FROM | isdir, 0 },
> +               { "entry", IN_MOVED_TO | isdir, 0 },
> +       };
> +       union {
> +               struct inotify_event align;
> +               char data[4096];
> +       } buf;
> +       struct inotify_event *event;
> +       const struct event_t *expected;
> +       unsigned int cookies[ARRAY_SIZE(event_set)] = { 0 };
> +       unsigned int count = 0, i;
> +       size_t pos = 0, len;
> +       int wd_source, wd_target;
> +
> +       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);
> +
> +       event_set[0].wd = wd_source;
> +       event_set[1].wd = wd_source;
> +       event_set[2].wd = wd_target;
> +
> +       create_entry(isdir, SOURCE);
> +       SAFE_RENAME(SOURCE, UNWATCHED);
> +       create_entry(isdir, SOURCE);
> +       SAFE_RENAME(SOURCE, TARGET);
> +
> +       len = SAFE_READ(SAFE_READ_ANY_EAGAIN, fd_notify, buf.data, sizeof(buf.data));
> +       SAFE_CLOSE(fd_notify);
> +
> +       /* Reuse these paths for the next case and for -i iterations. */
> +       delete_entry(isdir, UNWATCHED);
> +       delete_entry(isdir, TARGET);
> +
> +       while (pos < len) {
> +               if (len - pos < sizeof(*event))
> +                       tst_brk(TBROK, "%s: event %u has %zu header bytes (expected at least %zu)",
> +                               type, count, len - pos, sizeof(*event));
> +
> +               event = (struct inotify_event *)(buf.data + pos);
> +               if (event->len > len - pos - sizeof(*event))
> +                       tst_brk(TBROK, "%s: event %u claims %u name bytes but only %zu are left in the buffer, wd=%d mask=%#x cookie=%u",
> +                               type, count, event->len, len - pos - sizeof(*event),
> +                               event->wd, event->mask, event->cookie);
> +
> +               if (count >= ARRAY_SIZE(event_set)) {
> +                       tst_res(TFAIL, "%s: unexpected extra event %u: got wd=%d mask=%#x cookie=%u len=%u name=\"%.*s\", expected no event after %zu events",
> +                               type, count, event->wd,
> +                               event->mask, event->cookie, event->len,
> +                               (int)event->len, event->name, ARRAY_SIZE(event_set));
> +                       return;
> +               }
> +
> +               expected = &event_set[count];
> +               if (event->mask != expected->mask) {
> +                       tst_res(TFAIL, "%s: event %u has wrong mask: got wd=%d mask=%#x cookie=%u name=\"%.*s\", expected wd=%d mask=%#x name=\"%s\"",
> +                               type, count, event->wd, event->mask, event->cookie,
> +                               (int)event->len, event->name, expected->wd,
> +                               expected->mask, expected->name);
> +                       return;
> +               }
> +
> +               if (event->wd != expected->wd) {
> +                       tst_res(TFAIL, "%s: event %u has wrong wd: got wd=%d mask=%#x cookie=%u name=\"%.*s\", expected wd=%d mask=%#x name=\"%s\"",
> +                               type, count, event->wd, event->mask, event->cookie,
> +                               (int)event->len, event->name, expected->wd,
> +                               expected->mask, expected->name);
> +                       return;
> +               }
> +
> +               if (event->len < strlen(expected->name) + 1 ||
> +                   strncmp(expected->name, event->name, event->len)) {
> +                       tst_res(TFAIL, "%s: event %u has wrong name: got wd=%d mask=%#x cookie=%u len=%u name=\"%.*s\", expected wd=%d mask=%#x name=\"%s\"",
> +                               type, count, event->wd, event->mask, event->cookie,
> +                               event->len, (int)event->len, event->name,
> +                               expected->wd, expected->mask, expected->name);
> +                       return;
> +               }

I am sorry if my review comments seem nit-picking to you but the
error messages do not follow the other tests pattern.

This is the pattern of the other tests:

 git grep expected\ \%
fanotify01.c:                           "got event: mask=%llx (expected %llx) "
fanotify01.c:                           "(expected %u) fd=%d",
fanotify02.c:                           "got event: mask=%llx (expected %llx) "
fanotify02.c:                           "(expected %u) fd=%d",
fanotify03.c:           tst_res(TFAIL, "read() got errno %d (expected
%d)", errno, exp_errno);
fanotify03.c:           tst_res(TFAIL, "execve() got errno %d
(expected %d)", errno, exp_errno);
fanotify03.c:                           "got event: mask=%llx (expected %llx) "
fanotify03.c:                           "(expected %u) fd=%d",
fanotify05.c:                           "got event: mask=%llx
(expected %llx) pid=%u fd=%d",
fanotify06.c:           tst_res(TFAIL, "group %d got event: mask %llx
(expected %llx) "
fanotify06.c:                   "(expected %u) fd=%d", group,
fanotify07.c:                           "got event: mask=%llx (expected %llx) "
fanotify09.c:           tst_res(TFAIL, "group %d got event: mask %llx
(expected %llx) pid=%u fd=%d filename=%s",
fanotify09.c:           tst_res(TFAIL, "group %d got event: mask %llx
pid=%u (expected %u) fd=%d filename=%s",
fanotify10.c:           tst_res(TFAIL, "group %d (%x) got event: mask
%llx (expected %llx) "
fanotify10.c:                   "(expected %u) fd=%u", group, fanotify_class[p],
fanotify12.c:                           "Received event: mask=%llx
(expected %llx, "
fanotify15.c:                           "Got event: mask=%llx (expected %llx) "
fanotify15.c:                           "(expected %u) fd=%d",
fanotify16.c:                           "got event: mask=%llx (expected %llx) "
fanotify16.c:                           "fsid=%x.%x (expected %x.%x)",
fanotify16.c:                           "(expected %u) fd=%d name='%s' "
fanotify16.c:                           "pid=%u fd=%d name='%s'
num_info=%d (expected %d) "
fanotify19.c:                           "mask=%llx pid=%u (expected %u) fd=%d",
fanotify19.c:                           "mask=%llx pid=%u fd=%d (expected %d)",
fanotify23.c:                           "got event: mask=%llx (expected %llx)",

It highlights the mismatch without cluttering the log with more information.

Again, not a big deal, but it gives the feeling that my review comments
are being proxied to an agent without paying attention.
I am sure this is just a miscommunication and I should have been
more specific in my request.

Thanks,
Amir.


More information about the ltp mailing list