mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file
@ 2026-10-26 15:11 Edvard Conrad
  2026-10-26 22:53 ` Amir Goldstein
  0 siblings, 1 reply; 3+ messages in thread
From: Edvard Conrad @ 2026-10-26 15:11 UTC (permalink / raw)
  To: Miklos Szeredi, Amir Goldstein, Alexander Viro, Christian Brauner
  Cc: Jan Kara, linux-fsdevel, linux-unionfs, linux-kernel,
	Edvard Conrad, stable

When fanotify reports a permission event, copy_event_to_user() creates
the event fd with dentry_open_nonotify(), so that the listener's use of
that fd does not generate further events. If the event's path is on a
stacked filesystem, the stacked filesystem's ->open() opens the real
file with backing_file_open(). Since commit ebe559609d78 ("fs: get rid
of __FMODE_NONOTIFY kludge"), FMODE_NONOTIFY lives in f_mode only and
the backing file does not inherit it: it is allocated with the default
fsnotify mode, and fsnotify_open_perm_and_set_mode() raises FS_OPEN_PERM
on the real path if the backing filesystem has permission event
watchers.

Before that commit __FMODE_NONOTIFY travelled in f_flags, and overlayfs
copies the user file's f_flags into the backing open
(ovl_open_realfile()), so the backing file was exempt implicitly. Commit
bc2473c90fca ("ovl: enable fsnotify events on underlying real files")
relied on that when it dropped FMODE_NONOTIFY from OVL_OPEN_FLAGS.

The nested event is queued to the same group, from inside the
listener's read(2), and can only be answered by another reader thread
of that group. A listener holding filesystem marks on both the stacked
and the backing filesystem therefore deadlocks as soon as all of its
reader threads are inside such a read(2) at the same time, and every
other task that touches a marked filesystem queues up behind it.

Observed with a commercial anti-malware agent (16 reader threads) on an
ostree/composefs host: / is an overlayfs whose data layer is the ostree
object store on the same btrfs that holds /etc and /home, and a
systemd-sysext overlay sits on /usr. Every on-demand scan of /usr wedged
the whole machine until the group was killed. Stack of a blocked reader
thread on 7.2.7:

  fanotify_get_response
  fanotify_handle_event
  send_to_group
  fsnotify
  __fsnotify_parent
  do_dentry_open
  vfs_open
  backing_file_open
  ovl_open_realfile [overlay]
  ovl_open [overlay]
  do_dentry_open
  vfs_open
  backing_file_open
  ovl_open_realfile [overlay]
  ovl_open [overlay]
  do_dentry_open
  vfs_open
  dentry_open_nonotify
  copy_event_to_user
  fanotify_read
  vfs_read
  ksys_read

Userspace cannot avoid this: the backing open happens while the kernel
builds the fd, before the event is reported, and fid reporting is not
available for permission event classes.

Make a backing file inherit FMODE_NONOTIFY when the user file it is
opened for must not generate events. This restores the pre-6.14
behaviour for the fds that fanotify creates (and for any other
FMODE_NONOTIFY file), while ordinary opens through a stacked filesystem
keep generating events on the real path, as intended by bc2473c90fca.

Verified with a self-contained reproducer (private tmpfs lower layers, an
overlayfs over them, FAN_OPEN_PERM filesystem marks on the overlay and on
one lower layer, a child opening a file through the overlay) in a qemu
VM on this tree built with defconfig plus OVERLAY_FS, FANOTIFY and
FANOTIFY_ACCESS_PERMISSIONS: without this patch the listener's read(2)
never returns and the child's open is released only by the listener's
death; with it read(2) returns, the event is answered and the open
completes.

backing_tmpfile_open() is left alone: its only caller is
ovl_create_tmpfile(), whose user file is a regular overlayfs file.

Fixes: ebe559609d78 ("fs: get rid of __FMODE_NONOTIFY kludge")
Cc: stable@vger.kernel.org

Assisted-by: LLM
Signed-off-by: Edvard Conrad <edvard@tuta.io>
---
Reproducer: nested-perm-repro.c (inline below). It unshares a mount
namespace, mounts two private tmpfs and an overlayfs over them, puts
FAN_OPEN_PERM filesystem marks on the overlay and on one tmpfs, and lets
a child open a file through the overlay. On an unpatched 6.14+ kernel
the listener's read(2) never returns (the kernel raises a second
permission event for the backing file while creating the event fd); with
this patch it returns and the child's open completes. Verified in a qemu
VM on a defconfig+overlayfs+fanotify build of this tree, before and
after the patch (x86_64 defconfig plus OVERLAY_FS, FANOTIFY and
FANOTIFY_ACCESS_PERMISSIONS). Also observed in production: Bitdefender
BEST with filesystem marks on the btrfs that holds the ostree object
store and on a systemd-sysext overlay over /usr, Fedora kernel 7.2.7, on
a bootc/composefs host; every scan of /usr wedged the machine until the
group was killed. Full SysRq-w dumps available on request.

Tool disclosure (Documentation/process/generated-content.rst): the stack
analysis, the git-history search for the regressing commit, the patch
text, the commit message and the reproducer were produced with an AI
coding assistant (Anthropic Claude Code) under my direction over
interactive sessions; I reviewed, built and tested the result. No static
analysis tools beyond checkpatch were used.

nested-perm-repro.c:

/*
 * nested-perm-repro.c — fanotify permission event raised by the kernel's own
 * event-fd creation on a stacked filesystem (overlayfs), deadlocking a listener
 * with a single reader thread. Self-contained: builds a private tmpfs + overlay
 * stack inside a new mount namespace, touches nothing else on the system.
 *
 *   gcc -O2 -Wall -o nested-perm-repro nested-perm-repro.c
 *   sudo ./nested-perm-repro
 *
 * What it does:
 *   1. unshare(CLONE_NEWNS); mount tmpfs A and tmpfs B; create A/file;
 *      mount overlay M with lowerdir=A:B (read-only overlay, no upper needed).
 *   2. fanotify group (FAN_CLASS_CONTENT) with FAN_OPEN_PERM *filesystem* marks
 *      on M (the stacked fs) and on A (the backing fs) — the shape of an
 *      anti-malware agent watching both the OS tree and the disk it lives on.
 *      Filesystem marks, not mount marks: overlayfs opens its layers through
 *      private clones of the lower mounts, which a mount mark never matches.
 *      Each tmpfs is its own superblock, so the marks touch nothing else.
 *   3. fork: the child opens M/file. That raises an OPEN_PERM event on M.
 *   4. The parent read()s the event. The kernel builds the event fd with
 *      dentry_open_nonotify() -> ovl_open() -> backing_file_open() on A/file.
 *      On kernels >= 6.14 without the fix, that inner open raises a SECOND
 *      OPEN_PERM event on A, queued to the same group, from inside read(2).
 *      With one reader thread nobody can read it: read() never returns.
 *   5. The parent dies on alarm(TIMEOUT) (SIGALRM, default action; the sleep is
 *      TASK_KILLABLE so the fatal signal gets through). Closing the group
 *      answers everything FAN_ALLOW; the child's open completes and reports
 *      how long it was blocked: >= TIMEOUT means REPRODUCED.
 *
 * Expected output on an affected kernel:
 *   parent: read() on the fanotify fd ...
 *   child : open took 3.0 s (unblocked only by the listener's death) -> BUG REPRODUCED
 * On a fixed kernel the read returns at once, the parent answers FAN_ALLOW,
 * and the child reports a few milliseconds -> not reproduced.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <signal.h>
#include <time.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/fanotify.h>

#define TIMEOUT 3
#define BASE "/tmp/nested-perm-repro"

static void die(const char *what)
{
    fprintf(stderr, "%s: %s\n", what, strerror(errno));
    exit(1);
}

static double now(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

int main(void)
{
    char a[128], b[128], m[128], file[160], opts[512];
    int fan, fd;
    pid_t child;

    snprintf(a, sizeof a, "%s/A", BASE);
    snprintf(b, sizeof b, "%s/B", BASE);
    snprintf(m, sizeof m, "%s/M", BASE);
    snprintf(file, sizeof file, "%s/file", m);

    /* 1. private mount namespace + the stack */
    if (unshare(CLONE_NEWNS) < 0) die("unshare(CLONE_NEWNS)");
    if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) die("make-rprivate /");
    mkdir(BASE, 0755);
    mkdir(a, 0755); mkdir(b, 0755); mkdir(m, 0755);
    if (mount("tmpfs", a, "tmpfs", 0, "size=1m") < 0) die("mount tmpfs A");
    if (mount("tmpfs", b, "tmpfs", 0, "size=1m") < 0) die("mount tmpfs B");
    {
        char p[160];
        snprintf(p, sizeof p, "%s/file", a);
        fd = open(p, O_WRONLY | O_CREAT, 0644);
        if (fd < 0) die("create A/file");
        (void)!write(fd, "hello\n", 6);
        close(fd);
    }
    snprintf(opts, sizeof opts, "lowerdir=%s:%s", a, b);
    if (mount("overlay", m, "overlay", 0, opts) < 0) die("mount overlay M");
    printf("stack : %s = overlay(lowerdir=A:B), A and B are private tmpfs\n", m);

    /* 2. the listener: one group, permission marks on the stacked AND the backing mount */
    fan = fanotify_init(FAN_CLASS_CONTENT | FAN_CLOEXEC, O_RDONLY | O_CLOEXEC);
    if (fan < 0) die("fanotify_init");
    if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_OPEN_PERM, AT_FDCWD, m) < 0)
        die("fanotify_mark M");
    if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_OPEN_PERM, AT_FDCWD, a) < 0)
        die("fanotify_mark A");
    printf("marks : FAN_OPEN_PERM filesystem marks on M (stacked) and A (backing)\n");

    /* 3. the victim */
    child = fork();
    if (child < 0) die("fork");
    if (child == 0) {
        double t0, dt;
        int rc;

        close(fan);
        usleep(200 * 1000);
        t0 = now();
        rc = open(file, O_RDONLY);
        dt = now() - t0;
        if (rc >= 0) close(rc);
        if (dt >= TIMEOUT - 0.5)
            printf("child : open took %.1f s (unblocked only by the listener's death) "
                   "-> BUG REPRODUCED: the listener's read() never returned\n", dt);
        else
            printf("child : open took %.3f s (rc=%d) -> not reproduced\n", dt, rc);
        _exit(0);
    }

    /* 4. one read(). On an affected kernel this never returns. */
    printf("parent: read() on the fanotify fd (dies on alarm after %d s if it hangs) ...\n",
           TIMEOUT);
    fflush(stdout);
    alarm(TIMEOUT);
    {
        char buf[4096];
        ssize_t n = read(fan, buf, sizeof buf);
        struct fanotify_event_metadata *md = (struct fanotify_event_metadata *)buf;
        struct fanotify_response resp;

        alarm(0);
        if (n <= 0) die("read");
        printf("parent: got event mask=%#llx fd=%d pid=%d — read() returned, answering ALLOW\n",
               (unsigned long long)md->mask, md->fd, md->pid);
        resp.fd = md->fd;
        resp.response = FAN_ALLOW;
        (void)!write(fan, &resp, sizeof resp);
        if (md->fd >= 0) close(md->fd);
    }
    close(fan);
    waitpid(child, NULL, 0);
    return 0;
}

 fs/backing-file.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/fs/backing-file.c b/fs/backing-file.c
index 5614cb7801e..24357bf9328 100644
--- a/fs/backing-file.c
+++ b/fs/backing-file.c
@@ -43,6 +43,17 @@ struct file *backing_file_open(const struct file *user_file, int flags,
 	if (IS_ERR(f))
 		return f;
 
+	/*
+	 * A user file that must not generate fsnotify events (e.g. the fd
+	 * that fanotify creates for a permission event with
+	 * dentry_open_nonotify()) must not generate them through its backing
+	 * file either. Otherwise a listener watching the backing filesystem
+	 * gets a nested permission event from its own read(2), which only
+	 * another reader of the same group can answer.
+	 */
+	if (FMODE_FSNOTIFY_NONE(user_file->f_mode))
+		file_set_fsnotify_mode(f, FMODE_NONOTIFY);
+
 	path_get(user_path);
 	backing_file_set_user_path(f, user_path);
 	error = vfs_open(real_path, f);
-- 
2.56.0


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

* Re: [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file
  2026-10-26 15:11 [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file Edvard Conrad
@ 2026-10-26 22:53 ` Amir Goldstein
  0 siblings, 0 replies; 3+ messages in thread
From: Amir Goldstein @ 2026-10-26 22:53 UTC (permalink / raw)
  To: Edvard Conrad
  Cc: Miklos Szeredi, Alexander Viro, Christian Brauner, Jan Kara,
	linux-fsdevel, linux-unionfs, linux-kernel, stable

On Mon, Oct 26, 2026 at 5:12 PM Edvard Conrad <edvard@tuta.io> wrote:
>
> When fanotify reports a permission event, copy_event_to_user() creates
> the event fd with dentry_open_nonotify(), so that the listener's use of
> that fd does not generate further events. If the event's path is on a
> stacked filesystem, the stacked filesystem's ->open() opens the real
> file with backing_file_open(). Since commit ebe559609d78 ("fs: get rid
> of __FMODE_NONOTIFY kludge"), FMODE_NONOTIFY lives in f_mode only and
> the backing file does not inherit it: it is allocated with the default
> fsnotify mode, and fsnotify_open_perm_and_set_mode() raises FS_OPEN_PERM
> on the real path if the backing filesystem has permission event
> watchers.
>
> Before that commit __FMODE_NONOTIFY travelled in f_flags, and overlayfs
> copies the user file's f_flags into the backing open
> (ovl_open_realfile()), so the backing file was exempt implicitly. Commit
> bc2473c90fca ("ovl: enable fsnotify events on underlying real files")
> relied on that when it dropped FMODE_NONOTIFY from OVL_OPEN_FLAGS.
>
> The nested event is queued to the same group, from inside the
> listener's read(2), and can only be answered by another reader thread
> of that group. A listener holding filesystem marks on both the stacked
> and the backing filesystem therefore deadlocks as soon as all of its
> reader threads are inside such a read(2) at the same time, and every
> other task that touches a marked filesystem queues up behind it.
>
> Observed with a commercial anti-malware agent (16 reader threads) on an
> ostree/composefs host: / is an overlayfs whose data layer is the ostree
> object store on the same btrfs that holds /etc and /home, and a
> systemd-sysext overlay sits on /usr. Every on-demand scan of /usr wedged
> the whole machine until the group was killed. Stack of a blocked reader
> thread on 7.2.7:
>
>   fanotify_get_response
>   fanotify_handle_event
>   send_to_group
>   fsnotify
>   __fsnotify_parent
>   do_dentry_open
>   vfs_open
>   backing_file_open
>   ovl_open_realfile [overlay]
>   ovl_open [overlay]
>   do_dentry_open
>   vfs_open
>   backing_file_open
>   ovl_open_realfile [overlay]
>   ovl_open [overlay]
>   do_dentry_open
>   vfs_open
>   dentry_open_nonotify
>   copy_event_to_user
>   fanotify_read
>   vfs_read
>   ksys_read

Thanks for the report!

>
> Userspace cannot avoid this: the backing open happens while the kernel
> builds the fd, before the event is reported, and fid reporting is not
> available for permission event classes.
>
> Make a backing file inherit FMODE_NONOTIFY when the user file it is
> opened for must not generate events. This restores the pre-6.14
> behaviour for the fds that fanotify creates (and for any other
> FMODE_NONOTIFY file), while ordinary opens through a stacked filesystem
> keep generating events on the real path, as intended by bc2473c90fca.
>
> Verified with a self-contained reproducer (private tmpfs lower layers, an
> overlayfs over them, FAN_OPEN_PERM filesystem marks on the overlay and on
> one lower layer, a child opening a file through the overlay) in a qemu
> VM on this tree built with defconfig plus OVERLAY_FS, FANOTIFY and
> FANOTIFY_ACCESS_PERMISSIONS: without this patch the listener's read(2)
> never returns and the child's open is released only by the listener's
> death; with it read(2) returns, the event is answered and the open
> completes.
>
> backing_tmpfile_open() is left alone: its only caller is
> ovl_create_tmpfile(), whose user file is a regular overlayfs file.
>
> Fixes: ebe559609d78 ("fs: get rid of __FMODE_NONOTIFY kludge")
> Cc: stable@vger.kernel.org
>
> Assisted-by: LLM
> Signed-off-by: Edvard Conrad <edvard@tuta.io>
> ---
> Reproducer: nested-perm-repro.c (inline below). It unshares a mount
> namespace, mounts two private tmpfs and an overlayfs over them, puts
> FAN_OPEN_PERM filesystem marks on the overlay and on one tmpfs, and lets
> a child open a file through the overlay. On an unpatched 6.14+ kernel
> the listener's read(2) never returns (the kernel raises a second
> permission event for the backing file while creating the event fd); with
> this patch it returns and the child's open completes. Verified in a qemu
> VM on a defconfig+overlayfs+fanotify build of this tree, before and
> after the patch (x86_64 defconfig plus OVERLAY_FS, FANOTIFY and
> FANOTIFY_ACCESS_PERMISSIONS). Also observed in production: Bitdefender
> BEST with filesystem marks on the btrfs that holds the ostree object
> store and on a systemd-sysext overlay over /usr, Fedora kernel 7.2.7, on
> a bootc/composefs host; every scan of /usr wedged the machine until the
> group was killed. Full SysRq-w dumps available on request.
>
> Tool disclosure (Documentation/process/generated-content.rst): the stack
> analysis, the git-history search for the regressing commit, the patch
> text, the commit message and the reproducer were produced with an AI
> coding assistant (Anthropic Claude Code) under my direction over
> interactive sessions; I reviewed, built and tested the result. No static
> analysis tools beyond checkpatch were used.
>
> nested-perm-repro.c:
>
> /*
>  * nested-perm-repro.c — fanotify permission event raised by the kernel's own
>  * event-fd creation on a stacked filesystem (overlayfs), deadlocking a listener
>  * with a single reader thread. Self-contained: builds a private tmpfs + overlay
>  * stack inside a new mount namespace, touches nothing else on the system.
>  *
>  *   gcc -O2 -Wall -o nested-perm-repro nested-perm-repro.c
>  *   sudo ./nested-perm-repro
>  *
>  * What it does:
>  *   1. unshare(CLONE_NEWNS); mount tmpfs A and tmpfs B; create A/file;
>  *      mount overlay M with lowerdir=A:B (read-only overlay, no upper needed).
>  *   2. fanotify group (FAN_CLASS_CONTENT) with FAN_OPEN_PERM *filesystem* marks
>  *      on M (the stacked fs) and on A (the backing fs) — the shape of an
>  *      anti-malware agent watching both the OS tree and the disk it lives on.
>  *      Filesystem marks, not mount marks: overlayfs opens its layers through
>  *      private clones of the lower mounts, which a mount mark never matches.
>  *      Each tmpfs is its own superblock, so the marks touch nothing else.
>  *   3. fork: the child opens M/file. That raises an OPEN_PERM event on M.
>  *   4. The parent read()s the event. The kernel builds the event fd with
>  *      dentry_open_nonotify() -> ovl_open() -> backing_file_open() on A/file.
>  *      On kernels >= 6.14 without the fix, that inner open raises a SECOND
>  *      OPEN_PERM event on A, queued to the same group, from inside read(2).
>  *      With one reader thread nobody can read it: read() never returns.
>  *   5. The parent dies on alarm(TIMEOUT) (SIGALRM, default action; the sleep is
>  *      TASK_KILLABLE so the fatal signal gets through). Closing the group
>  *      answers everything FAN_ALLOW; the child's open completes and reports
>  *      how long it was blocked: >= TIMEOUT means REPRODUCED.
>  *
>  * Expected output on an affected kernel:
>  *   parent: read() on the fanotify fd ...
>  *   child : open took 3.0 s (unblocked only by the listener's death) -> BUG REPRODUCED
>  * On a fixed kernel the read returns at once, the parent answers FAN_ALLOW,
>  * and the child reports a few milliseconds -> not reproduced.
>  */
> #define _GNU_SOURCE
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> #include <fcntl.h>
> #include <errno.h>
> #include <sched.h>
> #include <signal.h>
> #include <time.h>
> #include <sys/mount.h>
> #include <sys/stat.h>
> #include <sys/wait.h>
> #include <sys/fanotify.h>
>
> #define TIMEOUT 3
> #define BASE "/tmp/nested-perm-repro"
>
> static void die(const char *what)
> {
>     fprintf(stderr, "%s: %s\n", what, strerror(errno));
>     exit(1);
> }
>
> static double now(void)
> {
>     struct timespec ts;
>     clock_gettime(CLOCK_MONOTONIC, &ts);
>     return ts.tv_sec + ts.tv_nsec / 1e9;
> }
>
> int main(void)
> {
>     char a[128], b[128], m[128], file[160], opts[512];
>     int fan, fd;
>     pid_t child;
>
>     snprintf(a, sizeof a, "%s/A", BASE);
>     snprintf(b, sizeof b, "%s/B", BASE);
>     snprintf(m, sizeof m, "%s/M", BASE);
>     snprintf(file, sizeof file, "%s/file", m);
>
>     /* 1. private mount namespace + the stack */
>     if (unshare(CLONE_NEWNS) < 0) die("unshare(CLONE_NEWNS)");
>     if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) die("make-rprivate /");
>     mkdir(BASE, 0755);
>     mkdir(a, 0755); mkdir(b, 0755); mkdir(m, 0755);
>     if (mount("tmpfs", a, "tmpfs", 0, "size=1m") < 0) die("mount tmpfs A");
>     if (mount("tmpfs", b, "tmpfs", 0, "size=1m") < 0) die("mount tmpfs B");
>     {
>         char p[160];
>         snprintf(p, sizeof p, "%s/file", a);
>         fd = open(p, O_WRONLY | O_CREAT, 0644);
>         if (fd < 0) die("create A/file");
>         (void)!write(fd, "hello\n", 6);
>         close(fd);
>     }
>     snprintf(opts, sizeof opts, "lowerdir=%s:%s", a, b);
>     if (mount("overlay", m, "overlay", 0, opts) < 0) die("mount overlay M");
>     printf("stack : %s = overlay(lowerdir=A:B), A and B are private tmpfs\n", m);
>
>     /* 2. the listener: one group, permission marks on the stacked AND the backing mount */
>     fan = fanotify_init(FAN_CLASS_CONTENT | FAN_CLOEXEC, O_RDONLY | O_CLOEXEC);
>     if (fan < 0) die("fanotify_init");
>     if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_OPEN_PERM, AT_FDCWD, m) < 0)
>         die("fanotify_mark M");
>     if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_OPEN_PERM, AT_FDCWD, a) < 0)
>         die("fanotify_mark A");
>     printf("marks : FAN_OPEN_PERM filesystem marks on M (stacked) and A (backing)\n");
>
>     /* 3. the victim */
>     child = fork();
>     if (child < 0) die("fork");
>     if (child == 0) {
>         double t0, dt;
>         int rc;
>
>         close(fan);
>         usleep(200 * 1000);
>         t0 = now();
>         rc = open(file, O_RDONLY);
>         dt = now() - t0;
>         if (rc >= 0) close(rc);
>         if (dt >= TIMEOUT - 0.5)
>             printf("child : open took %.1f s (unblocked only by the listener's death) "
>                    "-> BUG REPRODUCED: the listener's read() never returned\n", dt);
>         else
>             printf("child : open took %.3f s (rc=%d) -> not reproduced\n", dt, rc);
>         _exit(0);
>     }
>
>     /* 4. one read(). On an affected kernel this never returns. */
>     printf("parent: read() on the fanotify fd (dies on alarm after %d s if it hangs) ...\n",
>            TIMEOUT);
>     fflush(stdout);
>     alarm(TIMEOUT);
>     {
>         char buf[4096];
>         ssize_t n = read(fan, buf, sizeof buf);
>         struct fanotify_event_metadata *md = (struct fanotify_event_metadata *)buf;
>         struct fanotify_response resp;
>
>         alarm(0);
>         if (n <= 0) die("read");
>         printf("parent: got event mask=%#llx fd=%d pid=%d — read() returned, answering ALLOW\n",
>                (unsigned long long)md->mask, md->fd, md->pid);
>         resp.fd = md->fd;
>         resp.response = FAN_ALLOW;
>         (void)!write(fan, &resp, sizeof resp);
>         if (md->fd >= 0) close(md->fd);
>     }
>     close(fan);
>     waitpid(child, NULL, 0);
>     return 0;
> }
>

Any chance you can post this as an LTP test?

>  fs/backing-file.c | 11 +++++++++++
>  1 file changed, 11 insertions(+)
>
> diff --git a/fs/backing-file.c b/fs/backing-file.c
> index 5614cb7801e..24357bf9328 100644
> --- a/fs/backing-file.c
> +++ b/fs/backing-file.c
> @@ -43,6 +43,17 @@ struct file *backing_file_open(const struct file *user_file, int flags,
>         if (IS_ERR(f))
>                 return f;
>
> +       /*
> +        * A user file that must not generate fsnotify events (e.g. the fd
> +        * that fanotify creates for a permission event with
> +        * dentry_open_nonotify()) must not generate them through its backing
> +        * file either. Otherwise a listener watching the backing filesystem
> +        * gets a nested permission event from its own read(2), which only
> +        * another reader of the same group can answer.
> +        */
> +       if (FMODE_FSNOTIFY_NONE(user_file->f_mode))
> +               file_set_fsnotify_mode(f, FMODE_NONOTIFY);
> +
>         path_get(user_path);
>         backing_file_set_user_path(f, user_path);
>         error = vfs_open(real_path, f);
> --
> 2.56.0
>

Urgh I hate how special case this is.

How about something like below (untested)

Thanks,
Amir.

diff --git a/fs/file_table.c b/fs/file_table.c
index e2665eaa6f8c2..ef79894e2da7a 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -176,7 +176,8 @@ static int __init init_fs_stat_sysctls(void)
 fs_initcall(init_fs_stat_sysctls);
 #endif

-static int init_file(struct file *f, int flags, const struct cred *cred)
+static int init_file(struct file *f, int flags, const struct cred *cred,
+                    fmode_t fmode)
 {
        int error;

@@ -200,7 +201,7 @@ static int init_file(struct file *f, int flags,
const struct cred *cred)
        memset(&f->f_ra, 0, sizeof(f->f_ra));

        f->f_flags      = flags;
-       f->f_mode       = OPEN_FMODE(flags);
+       f->f_mode       = OPEN_FMODE(flags) | fmode;
        /*
         * Disable permission and pre-content events for all files by default.
         * They may be enabled later by fsnotify_open_perm_and_set_mode().
@@ -263,7 +264,7 @@ struct file *alloc_empty_file(int flags, const
struct cred *cred)
        if (unlikely(!f))
                return ERR_PTR(-ENOMEM);

-       error = init_file(f, flags, cred);
+       error = init_file(f, flags, cred, 0);
        if (unlikely(error)) {
                kmem_cache_free(filp_cache, f);
                return ERR_PTR(error);
@@ -297,14 +298,12 @@ struct file *alloc_empty_file_noaccount(int
flags, const struct cred *cred)
        if (unlikely(!f))
                return ERR_PTR(-ENOMEM);

-       error = init_file(f, flags, cred);
+       error = init_file(f, flags, cred, FMODE_NOACCOUNT);
        if (unlikely(error)) {
                kmem_cache_free(filp_cache, f);
                return ERR_PTR(error);
        }

-       f->f_mode |= FMODE_NOACCOUNT;
-
        return f;
 }

@@ -316,6 +315,16 @@ static int init_backing_file(struct backing_file *ff,
        return security_backing_file_alloc(&ff->file, user_file);
 }

+/*
+ * A user file that must not generate fsnotify events (e.g. the fd
+ * that fanotify creates for a permission event with
+ * dentry_open_nonotify()) must not generate them through its backing
+ * file either. Otherwise a listener watching the backing filesystem
+ * gets a nested permission event from its own read(2), which only
+ * another reader of the same group can answer.
+ */
+#define BACKING_FILE_FMODE_MASK (FMODE_NONOTIFY)
+
 /*
  * Variant of alloc_empty_file() that allocates a backing_file container
  * and doesn't check and modify nr_files.
@@ -328,19 +337,19 @@ struct file *alloc_empty_backing_file(int flags,
const struct cred *cred,
 {
        struct backing_file *ff;
        int error;
+       fmode_t fmode = user_file->f_mode & BACKING_FILE_FMODE_MASK;

        ff = kmem_cache_alloc(bfilp_cache, GFP_KERNEL);
        if (unlikely(!ff))
                return ERR_PTR(-ENOMEM);

-       error = init_file(&ff->file, flags, cred);
+       error = init_file(&ff->file, flags, cred, fmode |
+                         FMODE_BACKING | FMODE_NOACCOUNT);
        if (unlikely(error)) {
                kmem_cache_free(bfilp_cache, ff);
                return ERR_PTR(error);
        }

-       /* The f_mode flags must be set before fput(). */
-       ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
        error = init_backing_file(ff, user_file);
        if (unlikely(error)) {
                fput(&ff->file);

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

* Re: [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file
@ 2026-10-27  8:31 Edvard Conrad
  0 siblings, 0 replies; 3+ messages in thread
From: Edvard Conrad @ 2026-10-27  8:31 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: Edvard Conrad, Miklos Szeredi, Alexander Viro, Christian Brauner,
	Jan Kara, linux-fsdevel, linux-unionfs, linux-kernel, stable

On Tue, Oct 27, 2026 at 12:53 AM Amir Goldstein <amir73il@gmail.com> wrote:
> Any chance you can post this as an LTP test?

Yes. Written and verified, I will post it to the LTP list shortly as
syscalls/fanotify26. Two cases: a mark on the overlay only, which passes
on every kernel and keeps the test from passing for the wrong reason,
and a mark on the overlay plus the backing filesystem, which is the
shape that deadlocks.

> Urgh I hate how special case this is.
>
> How about something like below (untested)

Agreed on the shape, and it covers backing_tmpfile_open() as well, which
mine did not. As posted it does not fix the deadlock though:

> -       f->f_mode       = OPEN_FMODE(flags);
> +       f->f_mode       = OPEN_FMODE(flags) | fmode;

Five lines further down init_file() calls

	file_set_fsnotify_mode(f, FMODE_NONOTIFY_PERM);

and that helper clears FMODE_FSNOTIFY_MASK before it sets its argument,
so the inherited FMODE_NONOTIFY is dropped again. The backing file ends
up at FMODE_NONOTIFY_PERM, which is not FMODE_FSNOTIFY_NONE(), so
fsnotify_open_perm_and_set_mode() does not take its early return and
still raises FS_OPEN_PERM on the real path.

Measured in a VM on a defconfig + OVERLAY_FS + FANOTIFY +
FANOTIFY_ACCESS_PERMISSIONS build of this tree, with the reproducer from
the patch and with the LTP test:

  kernel                        reproducer    fanotify26
  mainline                      hangs         1 pass 1 fail
  v1 (fs/backing-file.c)        ok            2 pass
  your diff as posted           hangs         1 pass 1 fail
  v2                            ok            2 pass

v2 keeps your structure and routes the fsnotify part through
file_set_fsnotify_mode() instead:

	fmode_t fsnotify_mode = fmode & FMODE_FSNOTIFY_MASK;
	...
	f->f_mode	= OPEN_FMODE(flags) | fmode;
	file_set_fsnotify_mode(f, fsnotify_mode ? fsnotify_mode :
						  FMODE_NONOTIFY_PERM);

One deliberate difference from your version: it inherits only the "no
events at all" mode rather than masking FMODE_NONOTIFY out of whatever
the user file carries. A user file in the HSM mode (FMODE_NONOTIFY |
FMODE_NONOTIFY_PERM) would otherwise leave its backing file fully
silent, suppressing the events on real files that bc2473c90fca
deliberately enabled.

v2 follows in this thread.

Thanks,
Edvard

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

end of thread, other threads:[~2026-10-27  8:31 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26 15:11 [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file Edvard Conrad
2026-10-26 22:53 ` Amir Goldstein
2026-10-27  8:31 Edvard Conrad

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®