[LTP] [PATCH v3][STAGING] syscalls/fanotify26: Add test for nested permission events

Amir Goldstein amir73il@gmail.com
Fri Oct 30 10:22:55 CET 2026


On Thu, Oct 29, 2026 at 10:21 PM Edvard Conrad <edvard@tuta.io> wrote:
>
> The kernel creates the file descriptor it reports with a permission
> event by an open that must not generate further events. When the
> reported path is on a stacked filesystem, that open opens the real file
> on the backing filesystem, and since commit ebe559609d78 ("fs: get rid
> of __FMODE_NONOTIFY kludge") the backing file no longer inherits
> FMODE_NONOTIFY. A listener that also watches the backing filesystem is
> then sent a second permission event from inside its own read(2), which
> only the listener itself can answer, so a single reader never returns.
>
> Verify that a listener returns from read(2) and answers an open
> permission event for a file on an overlayfs mount. The first test case
> marks the overlay only and passes on any kernel, the second one also
> marks the backing filesystem, which is the shape an anti-malware
> scanner uses on an overlayfs root.
>
> The second case fails on a kernel without the pending fix "fs: let
> backing files inherit FMODE_NONOTIFY from the user file", so the test
> is added to runtest/staging for now.
>
> Link: https://lore.kernel.org/all/P58Wrer--F-9@tuta.io/
> Signed-off-by: Edvard Conrad <edvard@tuta.io>
> ---
> v2: https://lore.kernel.org/ltp/P50G-yE--F-9@tuta.io/
>
> v3:
> - Scale both process-state waits through tst_multiply_timeout(). The
>   library scales all of its own deadlines, so on a KASAN or lockdep
>   kernel a healthy but unscheduled listener was reported as blocked.
> - Report TFAIL, not TBROK, for an unexpected event mask, and check the
>   event's pid, following fanotify03.c. Answer the event either way, or
>   the opener stays parked in the kernel.
> - Probe FAN_CLASS_CONTENT + FAN_MARK_FILESYSTEM + FAN_OPEN_PERM with
>   REQUIRE_FANOTIFY_EVENTS_SUPPORTED_ON_FS(), the combination run()
>   actually uses. The two previous probes checked neither class nor mask.
> - Use SAFE_READ_ALL rather than a literal 1, matching SAFE_WRITE_ALL.
> - Drop the explicit SAFE_WAITPID() for the opener. tst_reap_children()
>   runs after every iteration and checks the exit status this discarded.
> - Drop three unused includes.
>
> Amir's Reviewed-by from v2 is not carried over: the failure reporting
> and the support probe changed since, so it needs a fresh look.

Still looks good

Reviewed-by: Amir Goldstein <amir73il@gmail.com>

>
> Not changed: the [STAGING] keyword stays in the patch subject rather
> than the commit subject, which is the form ground_rules.rst documents.
> Answered on the v2 thread.
>
> Tested on mainline v7.3-16259-g6b4b543fa18 with and without the pending
> fix: both cases pass with it, and the second fails without it.
>
>  runtest/staging                               |   1 +
>  testcases/kernel/syscalls/fanotify/.gitignore |   1 +
>  .../kernel/syscalls/fanotify/fanotify26.c     | 199 ++++++++++++++++++
>  3 files changed, 201 insertions(+)
>  create mode 100644 testcases/kernel/syscalls/fanotify/fanotify26.c
>
> diff --git a/runtest/staging b/runtest/staging
> index ef1cdea15..2ce76b119 100644
> --- a/runtest/staging
> +++ b/runtest/staging
> @@ -1 +1,2 @@
>  # Tests for features that are not yet in the stable kernel ABI
> +fanotify26 fanotify26
> diff --git a/testcases/kernel/syscalls/fanotify/.gitignore b/testcases/kernel/syscalls/fanotify/.gitignore
> index d6d0599f1..c13d9b33c 100644
> --- a/testcases/kernel/syscalls/fanotify/.gitignore
> +++ b/testcases/kernel/syscalls/fanotify/.gitignore
> @@ -23,4 +23,5 @@
>  /fanotify23
>  /fanotify24
>  /fanotify25
> +/fanotify26
>  /fanotify_child
> diff --git a/testcases/kernel/syscalls/fanotify/fanotify26.c b/testcases/kernel/syscalls/fanotify/fanotify26.c
> new file mode 100644
> index 000000000..8741cd560
> --- /dev/null
> +++ b/testcases/kernel/syscalls/fanotify/fanotify26.c
> @@ -0,0 +1,199 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2026 Edvard Conrad <edvard@tuta.io>
> + */
> +
> +/*\
> + * Check that a permission event on a stacked filesystem does not deadlock the
> + * :manpage:`fanotify(7)` listener.
> + *
> + * When the kernel reports a permission event, it creates the event file
> + * descriptor with an open that must not generate further events. If the
> + * reported path is on a stacked filesystem, that open opens the real file on
> + * the backing filesystem. Should the backing file not inherit "generate no
> + * events", and the listener watch the backing filesystem as well, the kernel
> + * queues a second permission event to the same group while the listener is
> + * still inside :manpage:`read(2)`. Only the listener can answer it, so a
> + * listener that reads with a single thread never returns, and every task that
> + * touches a watched filesystem queues up behind it.
> + *
> + * The first test case marks the overlay only and must pass on any kernel. The
> + * second one marks the backing filesystem as well, which is the shape an
> + * anti-malware scanner uses on an overlayfs root.
> + *
> + * The test needs root to create permission events and to mount overlayfs.
> + */
> +
> +#define _GNU_SOURCE
> +#include "config.h"
> +
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <signal.h>
> +#include <sys/types.h>
> +#include <sys/wait.h>
> +#include "tst_test.h"
> +
> +#ifdef HAVE_SYS_FANOTIFY_H
> +#include "fanotify.h"
> +
> +#define BACKING_MNT    OVL_BASE_MNTPOINT
> +#define FNAME          "test_file"
> +#define OVL_FILE       OVL_MNT "/" FNAME
> +#define LOWER_FILE     OVL_LOWER "/" FNAME
> +
> +/*
> + * The listener either answers the event at once or is blocked for good, so
> + * this only has to outlast scheduling noise. Scaled through
> + * tst_multiply_timeout() at the call sites, or a slow kernel build would
> + * report a healthy but unscheduled listener as blocked.
> + */
> +#define WAIT_MSEC      5000
> +
> +static int fd_notify = -1;
> +
> +static struct tcase {
> +       const char *tname;
> +       int mark_backing;
> +} tcases[] = {
> +       {"Mark on the overlay", 0},
> +       {"Mark on the overlay and on the backing filesystem", 1},
> +};
> +
> +static void answer_one_event(pid_t opener)
> +{
> +       struct fanotify_event_metadata event;
> +       struct fanotify_response resp;
> +
> +       /* Blocks for good if the event fd generates a nested event. */
> +       SAFE_READ(SAFE_READ_ALL, fd_notify, &event, sizeof(event));
> +
> +       /*
> +        * A wrong mask or a stranger's event is a result, not harness
> +        * breakage, so report and still answer: the opener stays parked in
> +        * the kernel until something does.
> +        */
> +       if (event.mask != FAN_OPEN_PERM) {
> +               tst_res(TFAIL, "Got event mask %llx, expected %llx",
> +                       (unsigned long long)event.mask,
> +                       (unsigned long long)FAN_OPEN_PERM);
> +       } else if (event.pid != opener) {
> +               tst_res(TFAIL, "Got event from pid %u, expected %u",
> +                       (unsigned int)event.pid, (unsigned int)opener);
> +       } else {
> +               tst_res(TPASS, "Listener answered the event");
> +       }
> +
> +       resp.fd = event.fd;
> +       resp.response = FAN_ALLOW;
> +       SAFE_WRITE(SAFE_WRITE_ALL, fd_notify, &resp, sizeof(resp));
> +
> +       if (event.fd != FAN_NOFD)
> +               SAFE_CLOSE(event.fd);
> +}
> +
> +static void run(unsigned int n)
> +{
> +       struct tcase *tc = &tcases[n];
> +       pid_t opener, reader;
> +
> +       tst_res(TINFO, "Test #%d: %s", n, tc->tname);
> +
> +       fd_notify = SAFE_FANOTIFY_INIT(FAN_CLASS_CONTENT, O_RDONLY);
> +
> +       SAFE_FANOTIFY_MARK(fd_notify, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
> +                          FAN_OPEN_PERM, AT_FDCWD, OVL_MNT);
> +
> +       if (tc->mark_backing) {
> +               SAFE_FANOTIFY_MARK(fd_notify,
> +                                  FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
> +                                  FAN_OPEN_PERM, AT_FDCWD, BACKING_MNT);
> +       }
> +
> +       opener = SAFE_FORK();
> +       if (!opener) {
> +               int fd;
> +
> +               /* Must not hold the group, it has to be released on close. */
> +               SAFE_CLOSE(fd_notify);
> +               /*
> +                * The kernel denies the event when a listener that never
> +                * answered it goes away, so a failure here is the fallout of
> +                * the test tearing a blocked listener down, not a result.
> +                */
> +               fd = open(OVL_FILE, O_RDONLY);
> +               if (fd != -1)
> +                       SAFE_CLOSE(fd);
> +               exit(0);
> +       }
> +
> +       /* The opener sleeps in the kernel once its event is queued. */
> +       if (TST_PROCESS_STATE_WAIT(opener, 'D',
> +                                  tst_multiply_timeout(WAIT_MSEC)))
> +               tst_brk(TBROK, "Open generated no permission event");
> +
> +       reader = SAFE_FORK();
> +       if (!reader) {
> +               answer_one_event(opener);
> +               exit(0);
> +       }
> +
> +       /*
> +        * The listener answers and exits, or is blocked for good. Only the
> +        * blocked case is handled here: the child reports its own success,
> +        * so a child that died some other way cannot be taken for one that
> +        * answered. The library reaps it on the normal path.
> +        */
> +       if (TST_PROCESS_STATE_WAIT(reader, 'Z',
> +                                  tst_multiply_timeout(WAIT_MSEC))) {
> +               tst_res(TFAIL,
> +                       "Listener blocked in read() by a nested event");
> +               SAFE_KILL(reader, SIGKILL);
> +               SAFE_WAITPID(reader, NULL, 0);
> +       }
> +
> +       /*
> +        * Releases whatever is still queued, so the opener can finish. The
> +        * library reaps it and checks its exit status after this returns.
> +        */
> +       SAFE_CLOSE(fd_notify);
> +}
> +
> +static void setup(void)
> +{
> +       /* The file has to exist in the lower layer to be opened through it. */
> +       SAFE_UMOUNT(OVL_MNT);
> +       SAFE_TOUCH(LOWER_FILE, 0644, NULL);
> +       SAFE_MOUNT_OVERLAY();
> +
> +       /*
> +        * Probe the combination run() uses, not a weaker proxy for it. Every
> +        * case marks the overlay; the second also marks the base.
> +        */
> +       REQUIRE_FANOTIFY_EVENTS_SUPPORTED_ON_FS(FAN_CLASS_CONTENT,
> +                                               FAN_MARK_FILESYSTEM,
> +                                               FAN_OPEN_PERM, OVL_MNT);
> +       REQUIRE_MARK_TYPE_SUPPORTED_ON_FS(FAN_MARK_FILESYSTEM, BACKING_MNT);
> +}
> +
> +static void cleanup(void)
> +{
> +       if (fd_notify != -1)
> +               SAFE_CLOSE(fd_notify);
> +}
> +
> +static struct tst_test test = {
> +       .test = run,
> +       .tcnt = ARRAY_SIZE(tcases),
> +       .setup = setup,
> +       .cleanup = cleanup,
> +       .forks_child = 1,
> +       .needs_root = 1,
> +       .mount_device = 1,
> +       .needs_overlay = 1,
> +       .mntpoint = OVL_BASE_MNTPOINT,
> +};
> +
> +#else
> +       TST_TEST_TCONF("system doesn't have required fanotify support");
> +#endif
> --
> 2.56.0
>


More information about the ltp mailing list