Re: [PATCH] backing-file: inherit FMODE_NONOTIFY from the user file
From: Amir Goldstein
Date: Mon Oct 26 2026 - 18:53:44 EST
On Mon, Oct 26, 2026 at 5:12 PM Edvard Conrad <edvard@xxxxxxx> 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@xxxxxxxxxxxxxxx
>
> Assisted-by: LLM
> Signed-off-by: Edvard Conrad <edvard@xxxxxxx>
> ---
> 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);