All of lore.kernel.org
 help / color / mirror / Atom feed
* [LTP] [PATCH v4] inotify13: check rename cookie pairing after adjacent moves
@ 2026-10-29 21:07 ` Hungyu Lin
  0 siblings, 0 replies; 8+ messages in thread
From: Hungyu Lin @ 2026-10-29 21:07 UTC (permalink / raw)
  To: ltp; +Cc: linux-fsdevel, dennylin0707, amir73il, jack

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 v4:
- Put expected values immediately after the mismatched fields, following
  the existing per-field diagnostic format.
- Use the existing "got unnecessary event" and "didn't get event" wording.
- Keep event context for extra events and mask, watch descriptor and
  filename mismatches. Omit it from the two cookie-failure messages,
  since it is already present in TINFO, as confirmed by Amir.

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 the kernel fix. Local diagnostic controls also exercise both
cookie failures and verify the preceding TINFO and concise TFAIL output.
The controls are kept outside this patch.

 runtest/syscalls                              |   1 +
 testcases/kernel/syscalls/inotify/.gitignore  |   1 +
 testcases/kernel/syscalls/inotify/inotify13.c | 211 ++++++++++++++++++++++++++
 3 files changed, 213 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..9ee660e041435d16b63097ba8e2f86515d6bac57
--- /dev/null
+++ b/testcases/kernel/syscalls/inotify/inotify13.c
@@ -0,0 +1,211 @@
+// 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: got unnecessary event %u: mask=%#x wd=%d cookie=%u len=%u name=\"%.*s\"",
+				type, count, event->mask, event->wd, event->cookie,
+				event->len, (int)event->len, event->name);
+			return;
+		}
+
+		expected = &event_set[count];
+		if (event->mask != expected->mask) {
+			tst_res(TFAIL, "%s: got event %u: mask=%#x (expected %#x) wd=%d cookie=%u name=\"%.*s\"",
+				type, count, event->mask, expected->mask, event->wd,
+				event->cookie, (int)event->len, event->name);
+			return;
+		}
+
+		if (event->wd != expected->wd) {
+			tst_res(TFAIL, "%s: got event %u: mask=%#x wd=%d (expected %d) cookie=%u name=\"%.*s\"",
+				type, count, event->mask, event->wd, expected->wd,
+				event->cookie, (int)event->len, event->name);
+			return;
+		}
+
+		if (event->len < strlen(expected->name) + 1 ||
+		    strncmp(expected->name, event->name, event->len)) {
+			tst_res(TFAIL, "%s: got event %u: mask=%#x wd=%d cookie=%u len=%u name=\"%.*s\" (expected \"%s\")",
+				type, count, event->mask, event->wd, event->cookie,
+				event->len, (int)event->len, event->name,
+				expected->name);
+			return;
+		}
+
+		cookies[count] = event->cookie;
+		tst_res(TINFO, "%s: event %u: wd=%d mask=%#x cookie=%u len=%u name=\"%.*s\"",
+			type, count, event->wd, event->mask, event->cookie,
+			event->len, (int)event->len, event->name);
+		count++;
+		pos += sizeof(*event) + event->len;
+	}
+
+	for (i = count; i < ARRAY_SIZE(event_set); i++) {
+		expected = &event_set[i];
+		tst_res(TFAIL, "%s: didn't get event %u: mask=%#x wd=%d name=\"%s\"",
+			type, i, expected->mask, expected->wd, expected->name);
+	}
+
+	if (count < ARRAY_SIZE(event_set))
+		return;
+
+	if (cookies[0] == cookies[1]) {
+		tst_res(TFAIL, "%s: got second FROM cookie=%u (expected != %u)",
+			type, cookies[1], cookies[0]);
+		return;
+	}
+
+	if (cookies[1] != cookies[2]) {
+		tst_res(TFAIL, "%s: got TO cookie=%u (expected %u from second FROM)",
+			type, cookies[2], cookies[1]);
+		return;
+	}
+
+	tst_res(TPASS, "%s: FROM cookies %u and %u differ, TO cookie %u matches the second",
+		type, cookies[0], cookies[1], cookies[2]);
+}
+
+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 = ARRAY_SIZE(tcases),
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_tmpdir = 1,
+};
+
+#else
+TST_TEST_TCONF("system doesn't have required inotify support");
+#endif
-- 
2.43.0

-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 8+ messages in thread
* [PATCH v3] inotify13: check rename cookie pairing after adjacent moves
@ 2026-10-29 17:49 Hungyu Lin
  2026-10-29 19:58 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 8+ messages in thread
From: Hungyu Lin @ 2026-10-29 17:49 UTC (permalink / raw)
  To: ltp; +Cc: amir73il, jack, linux-fsdevel, dennylin0707

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;
+		}
+
+		cookies[count] = event->cookie;
+		tst_res(TINFO, "%s: event %u: wd=%d mask=%#x cookie=%u len=%u name=\"%.*s\"",
+			type, count, event->wd, event->mask, event->cookie,
+			event->len, (int)event->len, event->name);
+		count++;
+		pos += sizeof(*event) + event->len;
+	}
+
+	for (i = count; i < ARRAY_SIZE(event_set); i++) {
+		expected = &event_set[i];
+		tst_res(TFAIL, "%s: missing event %u: got no event, expected wd=%d mask=%#x name=\"%s\"",
+			type, i, expected->wd, expected->mask, expected->name);
+	}
+
+	if (count < ARRAY_SIZE(event_set))
+		return;
+
+	if (cookies[0] == cookies[1]) {
+		tst_res(TFAIL, "%s: expected different FROM cookies for wd=%d mask=%#x name=\"%s\", got first=%u second=%u",
+			type, event_set[0].wd, event_set[0].mask,
+			event_set[0].name, cookies[0], cookies[1]);
+		return;
+	}
+
+	if (cookies[1] != cookies[2]) {
+		tst_res(TFAIL, "%s: expected matching rename cookies: FROM wd=%d mask=%#x name=\"%s\" cookie=%u, TO wd=%d mask=%#x name=\"%s\" cookie=%u",
+			type, event_set[1].wd, event_set[1].mask,
+			event_set[1].name, cookies[1], event_set[2].wd,
+			event_set[2].mask, event_set[2].name, cookies[2]);
+		return;
+	}
+
+	tst_res(TPASS, "%s: FROM cookies %u and %u differ, TO cookie %u matches the second",
+		type, cookies[0], cookies[1], cookies[2]);
+}
+
+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 = ARRAY_SIZE(tcases),
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_tmpdir = 1,
+};
+
+#else
+TST_TEST_TCONF("system doesn't have required inotify support");
+#endif

-- 
2.43.0

^ permalink raw reply related	[flat|nested] 8+ messages in thread
* [PATCH v2] inotify13: check rename cookie pairing after adjacent moves
@ 2026-10-28  5:15 Hungyu Lin
  2026-10-29 19:56 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 8+ messages in thread
From: Hungyu Lin @ 2026-10-28  5:15 UTC (permalink / raw)
  To: ltp; +Cc: amir73il, jack, linux-fsdevel, dennylin0707

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>
---
 runtest/syscalls                              |   1 +
 testcases/kernel/syscalls/inotify/.gitignore  |   1 +
 testcases/kernel/syscalls/inotify/inotify13.c | 176 ++++++++++++++++++
 3 files changed, 178 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..5b28e8d
--- /dev/null
+++ b/testcases/kernel/syscalls/inotify/inotify13.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Hungyu Lin <dennylin0707@gmail.com>
+ */
+
+/*\
+ * 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 <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 SAVED "unwatched/entry"
+#define TARGET "target/entry"
+
+struct event_t {
+	const char *name;
+	unsigned int mask;
+	int wd;
+};
+
+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)
+{
+	const char *type = directory ? "directory" : "file";
+	unsigned int isdir = directory ? IN_ISDIR : 0;
+	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, failed = 0;
+
+	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(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;
+
+	/* Reuse these paths for the next case and for -i iterations. */
+	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 at offset %zu", pos);
+
+		event = (struct inotify_event *)(buf.data + pos);
+		if (event->len > len - pos - sizeof(*event))
+			tst_brk(TBROK, "Incomplete inotify event name at offset %zu", pos);
+
+		if (count >= ARRAY_SIZE(event_set)) {
+			tst_res(TINFO, "%s: unexpected event %u: wd=%d mask=%#x cookie=%u len=%u name=\"%.*s\"",
+				type, count, event->wd, event->mask, event->cookie,
+				event->len, (int)event->len, event->name);
+			failed = 1;
+		} else {
+			expected = &event_set[count];
+			cookies[count] = event->cookie;
+			tst_res(TINFO, "%s: event %u: wd=%d (expected %d) mask=%#x (expected %#x) cookie=%u len=%u name=\"%.*s\" (expected \"%s\")",
+				type, count, event->wd, expected->wd,
+				event->mask, expected->mask, event->cookie,
+				event->len, (int)event->len, event->name,
+				expected->name);
+			if (event->wd != expected->wd ||
+			    event->mask != expected->mask ||
+			    event->len < strlen(expected->name) + 1 ||
+			    memcmp(event->name, expected->name,
+				   strlen(expected->name) + 1))
+				failed = 1;
+		}
+		count++;
+		pos += sizeof(*event) + event->len;
+	}
+
+	for (i = count; i < ARRAY_SIZE(event_set); i++) {
+		expected = &event_set[i];
+		tst_res(TINFO, "%s: missing event %u: expected wd=%d mask=%#x name=\"%s\"", type, i, expected->wd,
+			expected->mask, expected->name);
+		failed = 1;
+	}
+
+	if (failed) {
+		tst_res(TFAIL, "%s: unexpected rename event sequence (%u events, expected %zu)",
+			type, count, ARRAY_SIZE(event_set));
+		return;
+	}
+
+	if (cookies[0] == cookies[1]) {
+		tst_res(TFAIL, "%s: both FROM cookies are %u (expected different cookies)",
+			type, cookies[0]);
+		return;
+	}
+
+	if (cookies[1] != cookies[2]) {
+		tst_res(TFAIL, "%s: second FROM cookie=%u, TO cookie=%u (expected equal cookies)",
+			type, cookies[1], cookies[2]);
+		return;
+	}
+
+	tst_res(TPASS, "%s: second FROM cookie=%u matches TO cookie=%u",
+		type, cookies[1], cookies[2]);
+}
+
+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


^ permalink raw reply related	[flat|nested] 8+ messages in thread
* [PATCH] inotify13: check rename cookie pairing after adjacent moves
@ 2026-10-27 19:19 Hungyu Lin
  2026-10-29 19:57 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 8+ messages in thread
From: Hungyu Lin @ 2026-10-27 19:19 UTC (permalink / raw)
  To: ltp; +Cc: amir73il, jack, linux-fsdevel, dennylin0707

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>
---
 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 <dennylin0707@gmail.com>
+ */
+
+/*\
+ * 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 <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 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


^ permalink raw reply related	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-10-30  9:20 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29 21:07 [LTP] [PATCH v4] inotify13: check rename cookie pairing after adjacent moves Hungyu Lin
2026-10-29 21:07 ` Hungyu Lin
2026-10-30  0:31 ` [LTP] " linuxtestproject.agent
2026-10-30  9:20 ` [LTP] [PATCH v4] " Amir Goldstein
2026-10-30  9:20   ` Amir Goldstein
  -- strict thread matches above, loose matches on Subject: below --
2026-10-29 17:49 [PATCH v3] " Hungyu Lin
2026-10-29 19:58 ` [LTP] " linuxtestproject.agent
2026-10-28  5:15 [PATCH v2] " Hungyu Lin
2026-10-29 19:56 ` [LTP] " linuxtestproject.agent
2026-10-27 19:19 [PATCH] " Hungyu Lin
2026-10-29 19:57 ` [LTP] " linuxtestproject.agent

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.