From c782fc8ada811569811c0fdea5c3412511be95d0 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:21 +0000 Subject: [PATCH 1/7] ovl: fail the mount if the casefold flag has changed An unprivileged user in a user namespace can repeatedly trigger a kernel warning with their own overlay mounts. They do so by changing the casefold flag of the topmost layer's empty root, the top directory of that layer, after the layers are set and before the mount is created. The casefold flag of a directory makes name lookups in that directory case-insensitive. All layers of an overlay must agree on casefolding. ovl_mount_dir_check() reads the casefold flag of a layer root when fsconfig() sets that layer, and saves the result in ofs->casefold. The mount is created later, by another fsconfig() call with FSCONFIG_CMD_CREATE. The casefold flag of an empty directory can be changed. The casefold flag of the overlay root must match ofs->casefold. ovl_get_root() copies the flag from the topmost layer root, as it is when the mount is created. On a mismatch, ovl_get_root() only does a WARN_ON(), and the mount continues. The check is not a WARN_ON_ONCE(), so each such mount warns again. An unprivileged user needs CONFIG_UNICODE, and a tmpfs mounted with the casefold option in their namespace. Reproduced by changing the casefold flag of the empty upper directory after the layers were set and before FSCONFIG_CMD_CREATE, with tmpfs and ext4 layers, and as uid 1000 in a user namespace. Fix this by replacing the WARN_ON() with a check that fails the mount with -EINVAL. Fixes: f9377faaeae0 ("ovl: Add S_CASEFOLD as part of the inode flag to be copied") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Tested-by: Hungyu Lin Signed-off-by: Amir Goldstein --- fs/overlayfs/super.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index d9174eaccce2..8bc970748b96 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -1387,7 +1387,6 @@ static struct dentry *ovl_get_root(struct super_block *sb, ovl_dentry_set_flag(OVL_E_CONNECTED, root); ovl_set_upperdata(d_inode(root)); ovl_inode_init(d_inode(root), &oip, ino, fsid); - WARN_ON(!!IS_CASEFOLDED(d_inode(root)) != ofs->casefold); ovl_dentry_init_flags(root, upperdentry, oe, DCACHE_OP_WEAK_REVALIDATE); /* root keeps a reference of upperdentry */ dget(upperdentry); @@ -1571,6 +1570,17 @@ static int ovl_fill_super_creds(struct fs_context *fc, struct super_block *sb) if (!sb->s_root) goto out_free_oe; + /* + * The casefold flag of an empty layer root may have changed since the + * mount options were parsed. The root inode owns oe by now. + */ + if (!!IS_CASEFOLDED(d_inode(sb->s_root)) != ofs->casefold) { + pr_err("casefold flag of the root directory has changed\n"); + dput(sb->s_root); + sb->s_root = NULL; + return -EINVAL; + } + return 0; out_free_oe: From 83b351c3ade542bbfa1073e8c2d919826628252a Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:17 +0000 Subject: [PATCH 2/7] ovl: do not let chattr change the casefold flag chattr +F or -F on an overlay directory can make the directory, or the whole mount, unusable. Lookups of the directory, and lookups in it, then fail. From code inspection, the caller only needs to own the directory. The upper filesystem must support casefolding: ext4 or f2fs with the casefold feature, or tmpfs with the casefold option. chattr +F and -F set and clear FS_CASEFOLD_FL, the casefold flag of a directory. The flag enables casefolding (case-insensitive name lookup) for the directory. All directories of an overlay must match the casefold state that was recorded at mount time, and ovl_lookup_single() enforces this: - A lookup in a directory that does not match fails with -ESTALE. - A lookup of the directory itself fails with -EREMOTE. ovl_fileattr_set() copies the directory up, which creates it in the upper layer if it only existed in a lower layer. It then passes FS_CASEFOLD_FL on to the upper filesystem. The upper filesystem accepts the change if the upper directory is empty, regardless of what the merged directory contains. The merged directory is the one that the user sees, with the names from the upper and the lower directories. After the change the upper directory no longer matches the mount. If the directory is the root of the overlay, the whole mount is affected. The flag can also change without chattr +F or -F. chattr reads all flags of a directory, changes one and writes all of them back. For a directory that only exists in a lower layer, overlayfs reads the flags from the lower filesystem. xfs with the ascii-ci feature reports FS_CASEFOLD_FL on every inode, but never sets S_CASEFOLD, the inode flag that overlayfs compares with the mount. So its directories count as not casefolded. With a lower layer on such an xfs and an upper layer on ext4 with the casefold feature, chattr +A on a lower-only directory therefore sets FS_CASEFOLD_FL on the new, empty upper directory. Reproduced as root on tmpfs and on ext4 with chattr +F or -F on an overlay directory whose upper directory was empty. Afterwards lookups below it failed with ESTALE, and lookups of the directory itself failed with EREMOTE. The chattr +A case was seen once, on a kernel with an earlier form of this patch that still passed the caller's flag on: lookups of the directory failed with EREMOTE. Fix this by returning -EOPNOTSUPP if FS_IOC_SETFLAGS, the only call that can change the flag, asks for a casefold flag on a directory that does not agree with the casefold state of the mount. This also refuses the chattr +A case above. Fixes: 083957f9614a ("ovl: support layers on case-folding capable filesystems") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason [amir: check the flag against the casefold state of the mount instead of reading back the flags] Tested-by: Hungyu Lin Signed-off-by: Amir Goldstein --- fs/overlayfs/inode.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c index a11564e90751..f1c6df39d0ae 100644 --- a/fs/overlayfs/inode.c +++ b/fs/overlayfs/inode.c @@ -658,6 +658,14 @@ int ovl_fileattr_set(const struct mnt_idmap *idmap, unsigned int flags; int err; + /* + * Lookup refuses a directory that does not agree with the casefold + * state of the mount. Only FS_IOC_SETFLAGS can ask for a change. + */ + if (S_ISDIR(inode->i_mode) && fa->flags_valid && + !!(fa->flags & FS_CASEFOLD_FL) != OVL_FS(inode->i_sb)->casefold) + return -EOPNOTSUPP; + err = ovl_copy_up(dentry); if (!err) { ovl_path_real(dentry, &upperpath); From e803c82dcdd15e87b48fca1ea8d2cfe6a7ed83d4 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:21 +0000 Subject: [PATCH 3/7] ovl: fix whiteout creation in casefolded directories unlink and rmdir fail with EINVAL on an overlay with casefolded (case-insensitive) layers, if the name exists in a lower layer. Casefolded layers need CONFIG_UNICODE. Root can mount such an overlay, and so can an unprivileged user in a user namespace, with tmpfs layers. Overlayfs does not change a lower layer. To remove a name that exists there, it hides the name with a marker file, a whiteout. It creates the whiteout in the work directory, a scratch directory on the upper filesystem, and renames it over the name in the upper directory: - ovl_whiteout() creates the whiteout with mknod, or as a hard link to an earlier whiteout. - ovl_cleanup_and_whiteout() then prepares the rename with start_renaming_two_dentries(), which returns -EINVAL if the source or the target dentry is unhashed, that is, not in the dentry hash table. With casefolded directories, both dentries can be unhashed: - The target. Lookup in a casefolded directory leaves a negative dentry unhashed, so the rename target is unhashed if the name does not exist in the upper directory. - The source, which is the new whiteout. tmpfs hashes a dentry when mknod or link makes it positive, but ext4 does not, so on ext4 the source is unhashed as well. The cleanup after the failure also refuses an unhashed dentry, so on ext4 the whiteout is left behind in the work directory. Reproduced with rm and rmdir on casefolded tmpfs and ext4 layers, also as uid 1000 in a user namespace. Fix this by creating the whiteout with ovl_create_temp(), which repeats the lookup if the new dentry is unhashed. Prepare the rename with start_renaming_dentry(), which looks up the target name with the directories locked. The caller looked the name up without a lock, so return -ESTALE if the name now refers to a different inode. Fixes: 833d2b3a072f ("Add start_renaming_two_dentries()") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason [amir: pass the name used for lookup instead of taking a name snapshot] Tested-by: Hungyu Lin Signed-off-by: Amir Goldstein --- fs/overlayfs/dir.c | 46 ++++++++++++++++++++++------------------ fs/overlayfs/overlayfs.h | 10 +-------- fs/overlayfs/readdir.c | 3 ++- fs/overlayfs/util.c | 2 +- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c index 1792e4114621..e0a164e22d67 100644 --- a/fs/overlayfs/dir.c +++ b/fs/overlayfs/dir.c @@ -74,39 +74,34 @@ static struct dentry *ovl_start_creating_temp(struct ovl_fs *ofs, &QSTR(name)); } +/* + * The whiteout is the source of a rename, so it has to be hashed. + * ovl_create_temp() takes care of that for a casefolded workdir. + */ static struct dentry *ovl_whiteout(struct ovl_fs *ofs) { int err; struct dentry *whiteout, *link; struct dentry *workdir = ofs->workdir; - struct inode *wdir = workdir->d_inode; - char name[OVL_TEMPNAME_SIZE]; guard(mutex)(&ofs->whiteout_lock); if (!ofs->whiteout) { - whiteout = ovl_start_creating_temp(ofs, workdir, name); + whiteout = ovl_create_temp(ofs, workdir, &(struct ovl_cattr) { + .mode = S_IFCHR | WHITEOUT_MODE, + .rdev = WHITEOUT_DEV }); if (IS_ERR(whiteout)) return whiteout; - err = ovl_do_whiteout(ofs, wdir, whiteout); - if (!err) - ofs->whiteout = dget(whiteout); - end_creating(whiteout); - if (err) - return ERR_PTR(err); + ofs->whiteout = whiteout; } if (!ofs->no_shared_whiteout) { - link = ovl_start_creating_temp(ofs, workdir, name); - if (IS_ERR(link)) + link = ovl_create_temp(ofs, workdir, &(struct ovl_cattr) { + .hardlink = ofs->whiteout }); + if (!IS_ERR(link)) return link; - err = ovl_do_link(ofs, ofs->whiteout, wdir, link); - if (!err) - whiteout = dget(link); - end_creating(link); - if (!err) - return whiteout; + err = PTR_ERR(link); if (err != -EMLINK) { pr_warn("Failed to link whiteout - disabling whiteout inode sharing(nlink=%u, err=%u)\n", ofs->whiteout->d_inode->i_nlink, @@ -120,9 +115,10 @@ static struct dentry *ovl_whiteout(struct ovl_fs *ofs) } int ovl_cleanup_and_whiteout(struct ovl_fs *ofs, struct dentry *dir, - struct dentry *dentry) + struct dentry *dentry, const struct qstr *name) { struct dentry *whiteout; + struct qstr last = QSTR_LEN(name->name, name->len); struct renamedata rd = {}; int err; int flags = 0; @@ -139,9 +135,17 @@ int ovl_cleanup_and_whiteout(struct ovl_fs *ofs, struct dentry *dir, rd.old_parent = ofs->workdir; rd.new_parent = dir; rd.flags = flags; - err = start_renaming_two_dentries(&rd, whiteout, dentry); + /* + * The caller looked up @dentry by @name without a lock on @dir, and a + * casefolded directory leaves a negative dentry unhashed. Nothing tells + * if such a dentry is still valid, so look up @name again with @dir locked. + */ + err = start_renaming_dentry(&rd, 0, whiteout, &last); if (!err) { - err = ovl_do_rename_rd(&rd); + if (d_inode(rd.new_dentry) != d_inode(dentry)) + err = -ESTALE; + else + err = ovl_do_rename_rd(&rd); end_renaming(&rd); } if (err) @@ -847,7 +851,7 @@ static int ovl_remove_and_whiteout(struct dentry *dentry, goto out_dput_upper; } - err = ovl_cleanup_and_whiteout(ofs, upperdir, upper); + err = ovl_cleanup_and_whiteout(ofs, upperdir, upper, &dentry->d_name); if (!err) ovl_dir_modified(dentry->d_parent, true); diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index 901335026a20..e9389a10452e 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -396,14 +396,6 @@ static inline int ovl_do_rename(struct ovl_fs *ofs, struct dentry *olddir, return ovl_do_rename_rd(&rd); } -static inline int ovl_do_whiteout(struct ovl_fs *ofs, - struct inode *dir, struct dentry *dentry) -{ - int err = vfs_whiteout(ovl_upper_mnt_idmap(ofs), dir, dentry); - pr_debug("whiteout(%pd2) = %i\n", dentry, err); - return err; -} - static inline struct file *ovl_do_tmpfile(struct ovl_fs *ofs, struct dentry *dentry, umode_t mode) { @@ -899,7 +891,7 @@ static inline void ovl_copyflags(struct inode *from, struct inode *to) /* dir.c */ extern const struct inode_operations ovl_dir_inode_operations; int ovl_cleanup_and_whiteout(struct ovl_fs *ofs, struct dentry *dir, - struct dentry *dentry); + struct dentry *dentry, const struct qstr *name); struct ovl_cattr { dev_t rdev; umode_t mode; diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index 2018e3114655..b862309eba4d 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -1343,7 +1343,8 @@ int ovl_indexdir_cleanup(struct ovl_fs *ofs) * Whiteout orphan index to block future open by * handle after overlay nlink dropped to zero. */ - err = ovl_cleanup_and_whiteout(ofs, indexdir, index); + err = ovl_cleanup_and_whiteout(ofs, indexdir, index, + &QSTR_LEN(p->name, p->len)); } else { /* Cleanup orphan index entries */ err = ovl_cleanup(ofs, indexdir, index); diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c index 521717209b2e..6c3fda047f94 100644 --- a/fs/overlayfs/util.c +++ b/fs/overlayfs/util.c @@ -1118,7 +1118,7 @@ static void ovl_cleanup_index(struct dentry *dentry) } else if (ovl_index_all(dentry->d_sb)) { /* Whiteout orphan index to block future open by handle */ err = ovl_cleanup_and_whiteout(OVL_FS(dentry->d_sb), - indexdir, index); + indexdir, index, &name); } else { /* Cleanup orphan index entries */ err = ovl_cleanup(ofs, indexdir, index); From 323899b4ed233d5b6e2fc160c16fbf39cf0317da Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Fri, 30 Oct 2026 18:07:02 +0100 Subject: [PATCH 4/7] 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) Closes: https://lore.kernel.org/all/20261030-welpen-anbinden-nullen-b2432cee1d10@brauner/ Fixes: 130fdbc3d1f99 ("ovl: pass correct flags for opening real directory") Cc: stable@vger.kernel.org Tested-by: Edvard Conrad Link: https://lore.kernel.org/ltp/P5Iny41--F-9@tuta.io/ Signed-off-by: Amir Goldstein --- fs/open.c | 1 + fs/overlayfs/overlayfs.h | 9 ++++++++- fs/overlayfs/readdir.c | 3 ++- fs/overlayfs/util.c | 11 ++++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/fs/open.c b/fs/open.c index e43f02ff64ac..bb484b07390a 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 e9389a10452e..b5a672e8e476 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 b862309eba4d..5d21a328fad4 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -994,7 +994,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)); } /* diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c index 6c3fda047f94..788c6870d67d 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()); } From 9ff338af0e002fe217cfa16b1289b76f6b21fc72 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Fri, 30 Oct 2026 18:07:02 +0100 Subject: [PATCH 5/7] ovl: inherit FMODE_NONOTIFY when reading directory layers Listing a merged or impure overlay directory opens the real layer directories from ovl_dir_read(), when the readdir cache is filled. Those opens use plain ovl_path_open(), so when the directory fd is one that fanotify created for a permission event, they can still raise FS_OPEN_PERM. A listener that also marks the backing filesystem and calls getdents() on the event fd before answering then deadlocks the same way as with the directory open fixed by the previous commit. Store the overlay user file in struct ovl_readdir_data and pass it to ovl_path_open_realfile() from ovl_dir_read(), for both the merged and the impure cache. Internal readers such as ovl_check_empty_dir() and the mount-time workdir cleanup still pass no user file. A note about severity and real world impact: The FAN_OPEN_PERM event is traditionally used by anti-malware protection agents and security/audit agents. The scope that is often used in those use cases is FAN_MARK_MOUNT and agents typically monitor multiple mounts in a mount namespace. FAN_OPEN_PERM from directory open requires user opt-in with FAN_ONDIR. It is often not the case that anti-malware agents monitor open of directories. Security/audit engines are more likely to opt-into FAN_OPEN_PERM on directories, but are less likely to list the content of a directory within the scope of FAN_OPEN_PERM event, so the real world impact of this bug is likely not significant. Reported-by: Edvard Conrad Closes: https://lore.kernel.org/all/P5Qu3co--N-9@tuta.io/ Cc: stable@vger.kernel.org Tested-by: Edvard Conrad Link: https://lore.kernel.org/ltp/P5WKBbq--F-9@tuta.io/ Co-developed-by: Edvard Conrad Signed-off-by: Edvard Conrad Signed-off-by: Amir Goldstein --- fs/overlayfs/readdir.c | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index 5d21a328fad4..4cd4af097e5c 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); @@ -665,8 +669,8 @@ static bool ovl_fill_plain(struct dir_context *ctx, const char *name, return true; } -static int ovl_dir_read_impure(const struct path *path, struct list_head *list, - struct rb_root *root) +static int ovl_dir_read_impure(const struct path *path, struct list_head *list, + struct rb_root *root, const struct file *user_file) { int err; struct path realpath; @@ -674,6 +678,7 @@ static int ovl_dir_read_impure(const struct path *path, struct list_head *list, struct ovl_readdir_data rdd = { .ctx.actor = ovl_fill_plain, .ctx.count = INT_MAX, + .user_file = user_file, .list = list, .root = root, }; @@ -710,9 +715,10 @@ static int ovl_dir_read_impure(const struct path *path, struct list_head *list, return 0; } -static struct ovl_dir_cache *ovl_cache_get_impure(const struct path *path) +static struct ovl_dir_cache *ovl_cache_get_impure(struct file *file) { int res; + const struct path *path = &file->f_path; struct dentry *dentry = path->dentry; struct inode *inode = d_inode(dentry); struct ovl_fs *ofs = OVL_FS(dentry->d_sb); @@ -731,7 +737,7 @@ static struct ovl_dir_cache *ovl_cache_get_impure(const struct path *path) return ERR_PTR(-ENOMEM); cache->is_refcounted = false; - res = ovl_dir_read_impure(path, &cache->entries, &cache->root); + res = ovl_dir_read_impure(path, &cache->entries, &cache->root, file); if (res) { ovl_cache_free(&cache->entries); kfree(cache); @@ -852,7 +858,7 @@ static int ovl_iterate_real(struct file *file, struct dir_context *ctx) dput(parent); if (ovl_is_impure_dir(file)) { - rdt.cache = ovl_cache_get_impure(&file->f_path); + rdt.cache = ovl_cache_get_impure(file); if (IS_ERR(rdt.cache)) return PTR_ERR(rdt.cache); } @@ -866,14 +872,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); @@ -1120,7 +1125,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; From 520ef67f47f69255695859b0008281a384d0bc20 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:22 +0000 Subject: [PATCH 6/7] ovl: look up the index by the decoded origin A file opened by file handle on an overlay with nfs_export=on can show the stale lower data of a file that was copied up to the upper layer. While the file opened this way stays in the inode cache, a lookup of the copied up file by path fails with ESTALE for all users. The open can also succeed for a file that was removed, where it has to fail with ESTALE. This happens with a handle that has one byte changed. The handle comes from an NFS client, or from a process that calls open_by_handle_at() with CAP_DAC_READ_SEARCH in the initial user namespace. When a file in a lower layer is about to be modified, overlayfs first copies it to the upper layer (copy up). With nfs_export=on, each copy up also creates an entry in the index directory, a directory that overlayfs keeps for itself on the upper filesystem. The handle of an overlay file that comes from a lower layer wraps the handle of its lower inode. Overlayfs calls this lower inode the origin. The overlay handle stays valid after a copy up. To decode such a handle, ovl_lower_fh_to_d() first decodes the wrapped handle to the origin. If no overlay inode is cached for the origin, it looks in the index directory: - An index entry there leads to the upper file. - A whiteout, the marker that overlayfs uses for a removed file, means that the overlay file was removed. The name of an index entry is the handle that copy up encoded for the origin, as a hex string. ovl_lower_fh_to_d() builds the name for this lookup from the bytes that the caller passed in. But handles with different bytes can decode to the same inode: - Overlayfs ignores some flag bits of the handle. - A lower filesystem can accept a different fid type, extra bytes, or generation 0 for any generation. Such a handle decodes to the origin but misses the index entry or the whiteout. So the decode returns an overlay inode that has only the lower file. While that inode is cached, a path lookup of a copied up file fails with ESTALE, since the upper file that the lookup finds does not match the cached inode, which has none. Reproduced on tmpfs and on ext4 with open_by_handle_at(), after a copy up and a mount cycle. A handle in which one flag bit, the fid type, the length or the generation was changed read the old lower data, and path lookups returned ESTALE. Fix this by re-encoding the decoded origin and building the index name from that handle. Fixes: f71bd9cfb692 ("ovl: decode indexed non-dir file handles") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Amir Goldstein --- Documentation/filesystems/overlayfs.rst | 3 ++- fs/overlayfs/export.c | 4 ++-- fs/overlayfs/namei.c | 13 ++++++++++--- fs/overlayfs/overlayfs.h | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Documentation/filesystems/overlayfs.rst b/Documentation/filesystems/overlayfs.rst index 16c35b491dad..4c241d220bc2 100644 --- a/Documentation/filesystems/overlayfs.rst +++ b/Documentation/filesystems/overlayfs.rst @@ -731,7 +731,8 @@ When decoding an overlay file handle, the following steps are followed: 1. Find underlying layer by UUID and path type information. 2. Decode the underlying filesystem file handle to underlying dentry. - 3. For a lower file handle, lookup the handle in index directory by name. + 3. For a lower file handle, encode the decoded underlying inode again and + lookup that handle in index directory by name. 4. If a whiteout is found in index, return ESTALE. This represents an overlay object that was deleted after its file handle was encoded. 5. For a non-directory, instantiate a disconnected overlay dentry from the diff --git a/fs/overlayfs/export.c b/fs/overlayfs/export.c index 9aae20c59608..bd998a3d8b95 100644 --- a/fs/overlayfs/export.c +++ b/fs/overlayfs/export.c @@ -713,9 +713,9 @@ static struct dentry *ovl_lower_fh_to_d(struct super_block *sb, } } - /* Then lookup indexed upper/whiteout by origin fh */ + /* Then lookup indexed upper/whiteout by origin */ if (ovl_indexdir(sb)) { - index = ovl_get_index_fh(ofs, fh); + index = ovl_get_index_origin(ofs, origin.dentry); err = PTR_ERR(index); if (IS_ERR(index)) { index = NULL; diff --git a/fs/overlayfs/namei.c b/fs/overlayfs/namei.c index c6127edb9be9..64d63b260709 100644 --- a/fs/overlayfs/namei.c +++ b/fs/overlayfs/namei.c @@ -781,14 +781,21 @@ int ovl_get_index_name(struct ovl_fs *ofs, struct dentry *origin, return err; } -/* Lookup index by file handle for NFS export */ -struct dentry *ovl_get_index_fh(struct ovl_fs *ofs, struct ovl_fh *fh) +/* + * Lookup index by decoded origin for NFS export. + * + * The name of an index is the hex of the file handle that copy up encoded. + * The bytes of the handle to decode cannot be used as the name, because a + * lower fs may decode many handles to the same inode. For example, it may + * ignore the fid type, extra fid bytes or a zero generation. + */ +struct dentry *ovl_get_index_origin(struct ovl_fs *ofs, struct dentry *origin) { struct dentry *index; struct qstr name; int err; - err = ovl_get_index_name_fh(fh, &name); + err = ovl_get_index_name(ofs, origin, &name); if (err) return ERR_PTR(err); diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index b5a672e8e476..e42543d4c108 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -750,7 +750,7 @@ int ovl_verify_index(struct ovl_fs *ofs, struct dentry *index); int ovl_get_index_name_fh(const struct ovl_fh *fh, struct qstr *name); int ovl_get_index_name(struct ovl_fs *ofs, struct dentry *origin, struct qstr *name); -struct dentry *ovl_get_index_fh(struct ovl_fs *ofs, struct ovl_fh *fh); +struct dentry *ovl_get_index_origin(struct ovl_fs *ofs, struct dentry *origin); struct dentry *ovl_lookup_index(struct ovl_fs *ofs, struct dentry *upper, struct dentry *origin, bool verify); int ovl_path_next(int idx, struct dentry *dentry, struct path *path, From db1c5da238dc80a61ab429a7b71b0e2835766c45 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Mon, 2 Nov 2026 14:21:40 +0100 Subject: [PATCH 7/7] selftests/fscontext_ns: expect EINVAL for overlay descendant userns create Commit a518e63c3775 ("ovl: return EINVAL instead of EIO in case of mismatched user_ns") changed the errno for completing an overlay fscontext from another user namespace. Update the overlay test variant to match binfmt_misc. Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-lkp/202611020115.2281e158-lkp@intel.com Fixes: a518e63c3775 ("ovl: return EINVAL instead of EIO in case of mismatched user_ns") Cc: stable@vger.kernel.org Signed-off-by: Amir Goldstein --- .../selftests/filesystems/fscontext_ns/fscontext_ns_test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/filesystems/fscontext_ns/fscontext_ns_test.c b/tools/testing/selftests/filesystems/fscontext_ns/fscontext_ns_test.c index 0f30a3e6e197..535bf1f4e1a3 100644 --- a/tools/testing/selftests/filesystems/fscontext_ns/fscontext_ns_test.c +++ b/tools/testing/selftests/filesystems/fscontext_ns/fscontext_ns_test.c @@ -193,7 +193,7 @@ FIXTURE_VARIANT_ADD(fscontext_ns, binfmt_misc) { FIXTURE_VARIANT_ADD(fscontext_ns, overlay) { .fsname = "overlay", - .expected_errno = EIO, + .expected_errno = EINVAL, }; TEST_F(fscontext_ns, create_from_descendant_userns)