[LTP] [PATCH] syscalls/fanotify26: Add cases that list the directory
Amir Goldstein
amir73il@gmail.com
Tue Nov 3 15:44:56 CET 2026
On Tue, Nov 3, 2026 at 1:14 PM Edvard Conrad <edvard@tuta.io> wrote:
>
> Listing a directory through the event file descriptor makes overlayfs
> open its layers again, after the event fd exists: every layer of a
> merged directory in ovl_dir_read_merged(), and the upper directory of
> an impure one in ovl_dir_read_impure(). Unless those opens inherit
> FMODE_NONOTIFY from the event fd, a listener that marks the backing
> filesystem and lists the directory before it answers is sent a nested
> permission event and blocks in getdents64().
>
> Add a case for each. The impure directory is made by renaming a lower
> file into a directory created on the overlay. The case reports TCONF
> when the overlay cannot record the file's origin and so does not mark
> the directory impure. The impure listing is cached with the inode, so
> the case moves the entry out and back first; otherwise a later
> iteration would read the cache instead of the layer.
>
> Both cases fail on a kernel without the pending overlayfs fix for
> directory opens.
>
> Signed-off-by: Edvard Conrad <edvard@tuta.io>
> ---
> Applies on top of "syscalls/fanotify26: Add directory cases for the
> nested event":
> https://lore.kernel.org/ltp/P5Iny41--F-9@tuta.io/
>
> Asked for by Amir on the kernel thread, where both paths came up:
> https://lore.kernel.org/all/CAOQ4uxh67+vs0mr-Lxmwz9-N+F8m29WUNz+8qFLLDpOVYB7=vg@mail.gmail.com/
>
> Tested in a VM on 6b4b543fa18, each kernel run once and with -i 2,
> with the backing filesystem marked:
>
> file dir merged dir impure dir
> listed listed
> no fix FAIL FAIL FAIL FAIL
> + c3d355f427ce PASS FAIL FAIL FAIL
> + ovl directory fix, v1 PASS PASS FAIL FAIL
> + ovl directory fix, v2 PASS PASS PASS FAIL
> + v2 and the follow-up on PASS PASS PASS PASS
> the kernel thread
>
> The overlay-only cases pass on all of them. Without the fix the impure
> case fails in both iterations of -i 2, which is what moving the entry
> out and back is for.
>
> .../kernel/syscalls/fanotify/fanotify26.c | 92 ++++++++++++++++---
> 1 file changed, 79 insertions(+), 13 deletions(-)
>
> diff --git a/testcases/kernel/syscalls/fanotify/fanotify26.c b/testcases/kernel/syscalls/fanotify/fanotify26.c
> index 3f6b525ef..f6a56821f 100644
> --- a/testcases/kernel/syscalls/fanotify/fanotify26.c
> +++ b/testcases/kernel/syscalls/fanotify/fanotify26.c
> @@ -23,6 +23,11 @@
> * backing filesystem as well, which is the shape an anti-malware scanner uses
> * on an overlayfs root.
> *
> + * Listing a directory through the event file descriptor can make overlayfs
> + * open its layers again: every layer of a merged directory, and the upper
> + * directory of an impure one, which holds entries moved in from below. The
> + * last two test cases have the listener do that before it answers.
> + *
> * The test needs root to create permission events and to mount overlayfs.
> */
>
> @@ -34,7 +39,9 @@
> #include <signal.h>
> #include <sys/types.h>
> #include <sys/wait.h>
> +#include <sys/xattr.h>
> #include "tst_test.h"
> +#include "lapi/syscalls.h"
>
> #ifdef HAVE_SYS_FANOTIFY_H
> #include "fanotify.h"
> @@ -42,10 +49,21 @@
> #define BACKING_MNT OVL_BASE_MNTPOINT
> #define FNAME "test_file"
> #define DNAME "test_dir"
> +#define MNAME "merged_dir"
> +#define INAME "impure_dir"
> +#define MOVED "moved_file"
> #define OVL_FILE OVL_MNT "/" FNAME
> #define OVL_DIR OVL_MNT "/" DNAME
> +#define OVL_MDIR OVL_MNT "/" MNAME
> +#define OVL_IDIR OVL_MNT "/" INAME
> +#define OVL_MOVED OVL_MNT "/" MOVED
> +#define OVL_IDIR_MOVED OVL_IDIR "/" MOVED
> #define LOWER_FILE OVL_LOWER "/" FNAME
> #define LOWER_DIR OVL_LOWER "/" DNAME
> +#define LOWER_MDIR OVL_LOWER "/" MNAME
> +#define LOWER_MOVED OVL_LOWER "/" MOVED
> +#define UPPER_MDIR OVL_UPPER "/" MNAME
> +#define UPPER_IDIR OVL_UPPER "/" INAME
>
> /*
> * The listener either answers the event at once or is blocked for good, so
> @@ -56,19 +74,41 @@
> #define WAIT_MSEC 5000
>
> static int fd_notify = -1;
> +static int idir_is_impure;
>
> static struct tcase {
> const char *tname;
> + const char *path;
> int mark_backing;
> int ondir;
> + int list;
> + int impure;
> } tcases[] = {
> - {"Overlay mark, file", 0, 0},
> - {"Overlay and backing mark, file", 1, 0},
> - {"Overlay mark, directory", 0, 1},
> - {"Overlay and backing mark, directory", 1, 1},
> + {"Overlay mark, file", OVL_FILE, 0, 0, 0, 0},
> + {"Overlay and backing mark, file", OVL_FILE, 1, 0, 0, 0},
> + {"Overlay mark, directory", OVL_DIR, 0, 1, 0, 0},
> + {"Overlay and backing mark, directory", OVL_DIR, 1, 1, 0, 0},
> + {"Overlay and backing mark, merged directory, listed",
> + OVL_MDIR, 1, 1, 1, 0},
> + {"Overlay and backing mark, impure directory, listed",
> + OVL_IDIR, 1, 1, 1, 1},
> };
>
> -static void answer_one_event(pid_t opener)
> +static void list_dir(int fd)
> +{
> + char buf[1024];
> + long ret;
> +
> + /* Blocks for good if reading a layer generates a nested event. */
> + do {
> + ret = tst_syscall(__NR_getdents64, fd, buf, sizeof(buf));
> + } while (ret > 0);
> +
> + if (ret == -1)
> + tst_brk(TBROK | TERRNO, "getdents64() on the event fd failed");
> +}
> +
> +static void answer_one_event(pid_t opener, int list)
> {
> struct fanotify_event_metadata event;
> struct fanotify_response resp;
> @@ -76,6 +116,9 @@ static void answer_one_event(pid_t opener)
> /* Blocks for good if the event fd generates a nested event. */
> SAFE_READ(SAFE_READ_ALL, fd_notify, &event, sizeof(event));
>
> + if (list && event.fd != FAN_NOFD)
> + list_dir(event.fd);
> +
> /*
> * A wrong mask or a stranger's event is a result, not harness
> * breakage, so report and still answer: the opener stays parked in
> @@ -110,6 +153,20 @@ static void run(unsigned int n)
>
> tst_res(TINFO, "Test #%d: %s", n, tc->tname);
>
> + if (tc->impure) {
> + if (!idir_is_impure) {
> + tst_res(TCONF, "Overlay did not mark the directory impure");
> + return;
> + }
> + /*
> + * The impure listing is cached with the inode and reused, so
> + * a later iteration would not open the layer at all. Moving
> + * an entry with an origin out and back invalidates it.
> + */
> + SAFE_RENAME(OVL_IDIR_MOVED, OVL_MOVED);
> + SAFE_RENAME(OVL_MOVED, OVL_IDIR_MOVED);
> + }
> +
> fd_notify = SAFE_FANOTIFY_INIT(FAN_CLASS_CONTENT, O_RDONLY);
>
> SAFE_FANOTIFY_MARK(fd_notify, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
> @@ -132,10 +189,7 @@ static void run(unsigned int n)
> * answered it goes away, so a failure here is the fallout of
> * the test tearing a blocked listener down, not a result.
> */
> - if (tc->ondir)
> - fd = open(OVL_DIR, O_RDONLY | O_DIRECTORY);
> - else
> - fd = open(OVL_FILE, O_RDONLY);
> + fd = open(tc->path, O_RDONLY | (tc->ondir ? O_DIRECTORY : 0));
> if (fd != -1)
> SAFE_CLOSE(fd);
> exit(0);
> @@ -148,7 +202,7 @@ static void run(unsigned int n)
>
> reader = SAFE_FORK();
> if (!reader) {
> - answer_one_event(opener);
> + answer_one_event(opener, tc->list);
> exit(0);
> }
>
> @@ -160,8 +214,7 @@ static void run(unsigned int n)
> */
> if (TST_PROCESS_STATE_WAIT(reader, 'Z',
> tst_multiply_timeout(WAIT_MSEC))) {
> - tst_res(TFAIL,
> - "Listener blocked in read() by a nested event");
> + tst_res(TFAIL, "Listener blocked by a nested event");
> SAFE_KILL(reader, SIGKILL);
> SAFE_WAITPID(reader, NULL, 0);
> }
> @@ -175,12 +228,25 @@ static void run(unsigned int n)
>
> static void setup(void)
> {
> - /* Both have to exist in the lower layer to be opened through it. */
> + /* These have to exist in the layers to be opened through them. */
> SAFE_UMOUNT(OVL_MNT);
> SAFE_TOUCH(LOWER_FILE, 0644, NULL);
> SAFE_MKDIR(LOWER_DIR, 0755);
> + SAFE_MKDIR(LOWER_MDIR, 0755);
> + SAFE_MKDIR(UPPER_MDIR, 0755);
> + SAFE_TOUCH(LOWER_MOVED, 0644, NULL);
> SAFE_MOUNT_OVERLAY();
>
> + /*
> + * A directory made through the overlay is upper only. Moving a lower
> + * file into it records the file's origin and marks the directory
> + * impure, unless the layers cannot carry an origin.
> + */
> + SAFE_MKDIR(OVL_IDIR, 0755);
> + SAFE_RENAME(OVL_MOVED, OVL_IDIR_MOVED);
> + idir_is_impure =
> + getxattr(UPPER_IDIR, "trusted.overlay.impure", NULL, 0) != -1;
> +
> /*
> * Probe the widest combination run() uses, not a weaker proxy for
> * it. Every case marks the overlay; the second of each pair also
> --
> 2.56.0
>
Looks good!
Only problem I got:
tst_supported_fs_types.c:67: TINFO: mkfs.ext2 does exist
tst_test.c:1305: TINFO: Formatting /dev/loop0 with ext2 opts='' extra opts=''
mke2fs 1.47.2 (1-Jan-2025)
[ 107.271105] operation not supported error, dev loop0, sector 614272
op 0x9:(WRITE_ZEROES) flags 0x20800800 phys_seg 0 prio class 2
tst_test.c:1317: TINFO: Mounting /dev/loop0 to
/tmp/LTP_fanfH7dWr/mntpoint fstyp=ext2 flags=0
[ 107.359391] ext2 filesystem being mounted at
/tmp/LTP_fanfH7dWr/mntpoint supports timestamps until 2038-01-19
(0x7fffffff)
[ 107.367943] overlayfs: upper fs does not support RENAME_WHITEOUT.
tst_test.c:1895: TINFO: Overall timeout per run is 0h 02m 00s
fanotify26.c:232: TINFO: Umounting /tmp/LTP_fanfH7dWr/mntpoint/ovl
[ 107.383261] overlayfs: upper fs does not support RENAME_WHITEOUT.
fanotify26.c:246: TBROK: rename(mntpoint/ovl/moved_file,
mntpoint/ovl/impure_dir/moved_file) failed: EINVAL (22)
So I added to the test:
--- a/testcases/kernel/syscalls/fanotify/fanotify26.c
+++ b/testcases/kernel/syscalls/fanotify/fanotify26.c
@@ -275,6 +275,11 @@ static struct tst_test test = {
.mount_device = 1,
.needs_overlay = 1,
.mntpoint = OVL_BASE_MNTPOINT,
+ /* Test requires a base filesystem that supports RENAME_WHITEOUT */
+ .filesystems = (struct tst_fs[]) {
+ {.type = "ext4"},
+ {}
+ },
};
Other than that:
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Thanks,
Amir.
More information about the ltp
mailing list