Re: [PATCH v3] fs: let backing files inherit FMODE_NONOTIFY from the user file
From: Amir Goldstein
Date: Mon Nov 02 2026 - 07:47:33 EST
On Mon, Nov 2, 2026 at 11:58 AM Edvard Conrad <edvard@xxxxxxx> wrote:
>
> On Fri, Oct 30, 2026 at 6:13 PM Amir Goldstein <amir73il@xxxxxxxxx> wrote:
> > Would you mind adding test cases (or variants) to fanotify26 to cover
> > this find and test the attached (sanity tested only) patch.
>
> Two things on the directory side, one settled and one not.
>
> Agreed on 130fdbc3d1f99 ("ovl: pass correct flags for opening real
> directory"). Before it ovl_dir_open() passed the user file's f_flags to
> ovl_path_open(), and __FMODE_NONOTIFY travelled with them; the commit
> reasons through every O_* flag but that one is not an O_* flag. That
> fits v6.13 failing the directory case and passing the file case.
>
> The one that is not settled: with your patch, listing a merged
> directory through the event fd still deadlocks. ovl_iterate() sends a
> directory that has OVL_WHITEOUTS to ovl_iterate_merged(), and
> ovl_dir_read() opens each layer with plain ovl_path_open(), which no
> user file reaches. A listener that calls getdents64() on the fd before
> answering blocks here:
>
> fanotify_handle_event
> fsnotify
> fsnotify_open_perm_and_set_mode
> do_dentry_open
> vfs_open
> dentry_open
> ovl_dir_read_merged
> ovl_iterate
> iterate_dir
> __x64_sys_getdents64
>
> Two controls on the same kernel: the same merged directory without the
> listing passes, and a lower-only directory with the listing passes,
> since od->realfile is nonotify with your patch and iterate_dir() uses
> it directly. That was on 6b4b543fa18 with your patch applied (with some
> fuzz); the readdir.c paths involved are the same in today's master. I
> have it as an extra case in a local copy of fanotify26 and can post it
> once there is a fix to run it against.
Good catch!
Please test the attached v2 patch and post the new test cases.
>
> Incidentally, I was thinking about your ask for something more
> generic: a task-level "creating an event fd" state in fanotify would
> cover the file and directory opens in one place, and the copy-up opens
> in copy_up.c too, which also go through plain ovl_path_open() and
> which an O_RDWR group would trigger for a lower-only file (from
> reading, not tested). It would not reach the listing case above, which
> runs at getdents() time, long after the fd exists.
Writing into fanotify provided fds already has issues with potential deadlocks,
so I am not excited about making more changes to overlayfs to avert this.
Also it is the fd which is supposed to be marked NONOTIFY and not the listener
process because the listener process. We have thought about other models as well
but for now we need to keep it this way.
Thanks,
Amir.
From afc8e63b3054d65f7b0f2c1adf893ad625514397 Mon Sep 17 00:00:00 2001
From: Amir Goldstein <amir73il@xxxxxxxxx>
Date: Fri, 30 Oct 2026 18:07:02 +0100
Subject: [PATCH v2] ovl: inherit FMODE_NONOTIFY when opening directory
realfiles
When fanotify hands a listener an fd for a permission event on an
overlay directory, ovl_dir_open_realfile() opens the real directory via
dentry_open(), not backing_file_open(), so the lower open could still
raise FS_OPEN_PERM and deadlock a listener that also marks the backing
filesystem.
Export dentry_open_nonotify() and call it from ovl_path_open_realfile()
when the overlay user file must not generate events. Ordinary
ovl_path_open() callers are unchanged.
Reported-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
Closes: https://lore.kernel.org/all/20261030-welpen-anbinden-nullen-b2432cee1d10@brauner/
Fixes: 130fdbc3d1f99 ("ovl: pass correct flags for opening real directory")
Tested-by: Edvard Conrad <edvard@xxxxxxx>
Link: https://lore.kernel.org/all/P5Iny41--F-9@xxxxxxx/
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Amir Goldstein <amir73il@xxxxxxxxx>
---
fs/open.c | 1 +
fs/overlayfs/overlayfs.h | 9 ++++++++-
fs/overlayfs/readdir.c | 20 ++++++++++++--------
fs/overlayfs/util.c | 11 ++++++++++-
4 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/fs/open.c b/fs/open.c
index e43f02ff64acb..bb484b07390ac 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1172,6 +1172,7 @@ struct file *dentry_open_nonotify(const struct path *path, int flags,
}
return f;
}
+EXPORT_SYMBOL_GPL(dentry_open_nonotify);
/**
* kernel_file_open - open a file for kernel internal use
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index e9389a10452e8..b5a672e8e4764 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -531,7 +531,14 @@ void ovl_dir_modified(struct dentry *dentry, bool impurity);
u64 ovl_inode_version_get(struct inode *inode);
bool ovl_is_whiteout(struct dentry *dentry);
bool ovl_path_is_whiteout(struct ovl_fs *ofs, const struct path *path);
-struct file *ovl_path_open(const struct path *path, int flags);
+struct file *ovl_path_open_realfile(const struct file *user_file,
+ const struct path *path, int flags);
+
+static inline struct file *ovl_path_open(const struct path *path, int flags)
+{
+ return ovl_path_open_realfile(NULL, path, flags);
+}
+
int ovl_copy_up_start(struct dentry *dentry, int flags);
void ovl_copy_up_end(struct dentry *dentry);
bool ovl_already_copied_up(struct dentry *dentry, int flags);
diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c
index b862309eba4d1..d51b24e2bbd9a 100644
--- a/fs/overlayfs/readdir.c
+++ b/fs/overlayfs/readdir.c
@@ -43,6 +43,7 @@ struct ovl_dir_cache {
struct ovl_readdir_data {
struct dir_context ctx;
+ const struct file *user_file;
struct dentry *dentry;
bool is_lowest;
struct rb_root *root;
@@ -377,7 +378,8 @@ static inline int ovl_dir_read(const struct path *realpath,
struct file *realfile;
int err;
- realfile = ovl_path_open(realpath, O_RDONLY | O_LARGEFILE);
+ realfile = ovl_path_open_realfile(rdd->user_file, realpath,
+ O_RDONLY | O_LARGEFILE);
if (IS_ERR(realfile))
return PTR_ERR(realfile);
@@ -421,13 +423,14 @@ static void ovl_dir_reset(struct file *file)
}
static int ovl_dir_read_merged(struct dentry *dentry, struct list_head *list,
- struct rb_root *root)
+ struct rb_root *root, const struct file *user_file)
{
int err;
struct path realpath;
struct ovl_readdir_data rdd = {
.ctx.actor = ovl_fill_merge,
.ctx.count = INT_MAX,
+ .user_file = user_file,
.dentry = dentry,
.list = list,
.root = root,
@@ -498,9 +501,10 @@ static struct ovl_dir_cache *ovl_dir_cache_check(struct inode *inode,
return cache;
}
-static struct ovl_dir_cache *ovl_cache_get(struct dentry *dentry)
+static struct ovl_dir_cache *ovl_dir_cache_get(struct file *file)
{
int res;
+ struct dentry *dentry = file->f_path.dentry;
struct ovl_dir_cache *cache;
struct inode *inode = d_inode(dentry);
@@ -521,7 +525,7 @@ static struct ovl_dir_cache *ovl_cache_get(struct dentry *dentry)
INIT_LIST_HEAD(&cache->entries);
cache->root = RB_ROOT;
- res = ovl_dir_read_merged(dentry, &cache->entries, &cache->root);
+ res = ovl_dir_read_merged(dentry, &cache->entries, &cache->root, file);
if (res) {
ovl_cache_free(&cache->entries);
kfree(cache);
@@ -866,14 +870,13 @@ static int ovl_iterate_real(struct file *file, struct dir_context *ctx)
static int ovl_iterate_merged(struct file *file, struct dir_context *ctx)
{
struct ovl_dir_file *od = file->private_data;
- struct dentry *dentry = file->f_path.dentry;
struct ovl_cache_entry *p;
int err;
if (!od->cache) {
struct ovl_dir_cache *cache;
- cache = ovl_cache_get(dentry);
+ cache = ovl_dir_cache_get(file);
if (IS_ERR(cache))
return PTR_ERR(cache);
@@ -994,7 +997,8 @@ static struct file *ovl_dir_open_realfile(const struct file *file,
const struct path *realpath)
{
with_ovl_creds(file_inode(file)->i_sb)
- return ovl_path_open(realpath, O_RDONLY | (file->f_flags & O_LARGEFILE));
+ return ovl_path_open_realfile(file, realpath,
+ O_RDONLY | (file->f_flags & O_LARGEFILE));
}
/*
@@ -1119,7 +1123,7 @@ int ovl_check_empty_dir(struct dentry *dentry, struct list_head *list)
struct rb_root root = RB_ROOT;
with_ovl_creds(dentry->d_sb)
- err = ovl_dir_read_merged(dentry, list, &root);
+ err = ovl_dir_read_merged(dentry, list, &root, NULL);
if (err)
return err;
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 6c3fda047f94f..788c6870d67df 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -654,7 +654,8 @@ bool ovl_path_is_whiteout(struct ovl_fs *ofs, const struct path *path)
ovl_path_check_xwhiteout_xattr(ofs, path);
}
-struct file *ovl_path_open(const struct path *path, int flags)
+struct file *ovl_path_open_realfile(const struct file *user_file,
+ const struct path *path, int flags)
{
struct inode *inode = d_inode(path->dentry);
const struct mnt_idmap *real_idmap = mnt_idmap(path->mnt);
@@ -682,6 +683,14 @@ struct file *ovl_path_open(const struct path *path, int flags)
if (inode_owner_or_capable(real_idmap, inode))
flags |= O_NOATIME;
+ /*
+ * Directory opens use dentry_open() here, not backing_file_open().
+ * Inherit FMODE_NONOTIFY from a fanotify permission fd so a listener
+ * that also watches the real filesystem does not deadlock.
+ */
+ if (user_file && FMODE_FSNOTIFY_NONE(user_file->f_mode))
+ return dentry_open_nonotify(path, flags, current_cred());
+
return dentry_open(path, flags, current_cred());
}
--
2.55.0