From 755b5c5c9277be251a51623d6065abab948c61bc Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 09:45:28 +0000 Subject: [PATCH 01/20] ovl: do not copy up an xattr value that was never read Copy-up stores uninitialized kernel memory in an xattr of the upper file, where the user can read it. ovl_copy_xattr() may only pass bytes to ovl_do_setxattr() that ovl_do_getxattr() has read. When the read fails with -ERANGE the function asks for the size again, and if the new size fits the buffer it stores the buffer although nothing was read into it. The buffer comes from kvmalloc(), or it still holds the value of the previous xattr. A FUSE lower layer that answers every sized GETXATTR with -ERANGE made copy-up store 4096 bytes that the server never sent, with a kernel pointer in them, as an xattr of the upper file. Fix this by failing the copy-up with -EIO when the size that follows -ERANGE fits the buffer. Reported-by: Fahad Alharbi Closes: https://lore.kernel.org/linux-fsdevel/CAOQ4uxjhfpNVvoVrTayj46H2CViTOc3oxzM6xdb8+E7ikE2CSA@mail.gmail.com/ Fixes: e4ad29fa0d22 ("ovl: use a minimal buffer in ovl_copy_xattr") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index e963701b4c87..c220cb5d0b70 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -131,8 +131,12 @@ int ovl_copy_xattr(struct super_block *sb, const struct path *oldpath, struct de retry: size = ovl_do_getxattr(oldpath, name, value, value_size); - if (size == -ERANGE) + if (size == -ERANGE) { size = ovl_do_getxattr(oldpath, name, NULL, 0); + /* Nothing was read into value, so it must not be used */ + if (size >= 0 && size <= value_size) + size = -EIO; + } if (size < 0) { error = size; From e6b9117076fb4c0ff4a670efa9c876d9a50d79ab Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Fri, 16 Oct 2026 09:33:58 +0200 Subject: [PATCH 02/20] ovl: fortify ovl_copy_xattr() against underlying layer modifications Adapt ovl_getxattr_value() to be used from ovl_copy_xattr(), by allowing to pass it a pre-allocated buffer, which can be resized and by handling the ENODATA/EOPNOTSUPP return codes in the caller. This replaces the retry loop in ovl_copy_xattr() with a single retry after resizing the buffer. If the value changed again by then, the underlying layer was modified, so return -EIO instead of retrying. As a side effect, ovl_copy_up_meta_inode_data() now returns -EIO instead of -ERANGE in this case of underlying xattr size changes. Reported-by: Fahad Alharbi Tested-by: Fahad Alharbi Suggested-by: Miklos Szeredi Link: https://lore.kernel.org/linux-fsdevel/CAJfpegsVoUyqy0dYkTvBGRZvgdv_nmzmCw_uF+LP-Vq3EhEkSg@mail.gmail.com/ Fixes: e4ad29fa0d22 ("ovl: use a minimal buffer in ovl_copy_xattr") Cc: stable@vger.kernel.org Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 68 ++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index c220cb5d0b70..eee22581b14f 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -72,6 +72,9 @@ static int ovl_copy_acl(struct ovl_fs *ofs, const struct path *path, return err; } +static ssize_t ovl_getxattr_value(const struct path *path, char *name, + char **value, ssize_t *value_size); + int ovl_copy_xattr(struct super_block *sb, const struct path *oldpath, struct dentry *new) { struct dentry *old = oldpath->dentry; @@ -129,34 +132,12 @@ int ovl_copy_xattr(struct super_block *sb, const struct path *oldpath, struct de break; } -retry: - size = ovl_do_getxattr(oldpath, name, value, value_size); - if (size == -ERANGE) { - size = ovl_do_getxattr(oldpath, name, NULL, 0); - /* Nothing was read into value, so it must not be used */ - if (size >= 0 && size <= value_size) - size = -EIO; - } - + size = ovl_getxattr_value(oldpath, name, &value, &value_size); if (size < 0) { error = size; break; } - if (size > value_size) { - void *new; - - new = kvmalloc(size, GFP_KERNEL); - if (!new) { - error = -ENOMEM; - break; - } - kvfree(value); - value = new; - value_size = size; - goto retry; - } - error = ovl_do_setxattr(OVL_FS(sb), new, name, value, size, 0); if (error) { if (error != -EOPNOTSUPP || ovl_must_copy_xattr(name)) @@ -1059,26 +1040,39 @@ static bool ovl_need_meta_copy_up(struct dentry *dentry, umode_t mode, return true; } -static ssize_t ovl_getxattr_value(const struct path *path, char *name, char **value) +static ssize_t ovl_getxattr_value(const struct path *path, char *name, + char **value, ssize_t *value_size) { ssize_t res; char *buf; - res = ovl_do_getxattr(path, name, NULL, 0); - if (res == -ENODATA || res == -EOPNOTSUPP) - res = 0; + res = ovl_do_getxattr(path, name, *value, *value_size); + if (res == -ERANGE) { + res = ovl_do_getxattr(path, name, NULL, 0); + /* Xattr changed underneath us? */ + if (res >= 0 && res <= *value_size) + return -EIO; + } - if (res > 0) { - buf = kzalloc(res, GFP_KERNEL); + if (res > *value_size) { + buf = kvmalloc(res, GFP_KERNEL); if (!buf) return -ENOMEM; res = ovl_do_getxattr(path, name, buf, res); - if (res < 0) - kfree(buf); - else - *value = buf; + if (res < 0) { + kvfree(buf); + /* Xattr changed underneath us? */ + if (res == -ERANGE) + return -EIO; + return res; + } + + kvfree(*value); + *value = buf; + *value_size = res; } + return res; } @@ -1089,7 +1083,7 @@ static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c) struct path upperpath; int err; char *capability = NULL; - ssize_t cap_size; + ssize_t cap_size = 0; ovl_path_upper(c->dentry, &upperpath); if (WARN_ON(upperpath.dentry == NULL)) @@ -1097,7 +1091,9 @@ static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c) if (c->stat.size) { err = cap_size = ovl_getxattr_value(&upperpath, XATTR_NAME_CAPS, - &capability); + &capability, &cap_size); + if (cap_size == -ENODATA || cap_size == -EOPNOTSUPP) + cap_size = 0; if (cap_size < 0) goto out; } @@ -1127,7 +1123,7 @@ static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c) ovl_clear_flag(OVL_VERIFIED_DIGEST, d_inode(c->dentry)); ovl_set_upperdata(d_inode(c->dentry)); out_free: - kfree(capability); + kvfree(capability); out: return err; } From cbec8610ec5aaf35df50094ff639febc23f20773 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:01:19 +0000 Subject: [PATCH 03/20] ovl: set OVL_WHITEOUTS before publishing the upper dir Listing a directory while it is copied up causes a use-after-free of the readdir cache. A merge directory must never be iterated as a real upper directory: that mode frees the cache in OVL_I(inode)->cache without looking at its refcount, while files opened in merged mode still use it. ovl_copy_up_workdir() publishes the upper dentry before it sets OVL_WHITEOUTS, and ovl_dir_open() samples both without a lock, so an open between the two stores finds the upper directory and takes it for a real one. With a delay between the two stores, an unprivileged process that listed a directory during its copy-up saw only "." and "..", and its next getdents freed the readdir cache of another descriptor. Fix this by setting OVL_WHITEOUTS before ovl_inode_update(), so that ovl_dir_reset() agrees with ovl_dir_open(), and by testing OVL_TYPE_MERGE() of the path type in ovl_dir_open(), so that an open that observes the upper dentry never iterates a merge dir as a real dir, regardless of the order in which it observes the flag. Fixes: b79e05aaa166 ("ovl: no direct iteration for dir with origin xattr") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik [amir: test OVL_TYPE_MERGE() instead of smp_rmb() in ovl_dir_open()] Link: https://lore.kernel.org/linux-unionfs/CAOQ4uxh-1J2OBjnDVbrrrjy1puSOW0kABvghN85ih6dtKUBTyA@mail.gmail.com/ Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 2 +- fs/overlayfs/readdir.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index eee22581b14f..92b3d08a0234 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -819,9 +819,9 @@ static int ovl_copy_up_workdir(struct ovl_copy_up_ctx *c) if (!c->metacopy) ovl_set_upperdata(inode); - ovl_inode_update(inode, temp); if (S_ISDIR(inode->i_mode)) ovl_set_flag(OVL_WHITEOUTS, inode); + ovl_inode_update(inode, temp); out: ovl_end_write(c->dentry); diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index 7d6f7f6022eb..ba542a192012 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -1055,7 +1055,8 @@ static int ovl_dir_open(struct inode *inode, struct file *file) return PTR_ERR(realfile); } od->realfile = realfile; - od->is_real = ovl_dir_is_real(inode); + od->is_real = !OVL_TYPE_MERGE(type) && + !ovl_test_flag(OVL_WHITEOUTS, inode); od->is_upper = OVL_TYPE_UPPER(type); file->private_data = od; From ba227ed05b385926094741e2d566789525c36085 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Thu, 14 May 2026 22:56:46 +0200 Subject: [PATCH 04/20] ovl: add assertions in dir cache code Overlayfs may keep two different types of directory cache merged (refcounted) and impure (not refcounted) in the inode. They are mutually exclusive and an overlayfs directory is never expected to switch between merged and impure modes. Nevertheless it is hard to prove that races in the code are not possible, so add assertions to fortify the code accessing the directory cache to make sure it does not free the unexpected cache type. If an assertion fires the code may leak dir cache memory but it will not access freed cache memory. Suggested-by: Nirmoy Das Link: https://lore.kernel.org/r/20260511062057.2365769-1-nirmoyd@nvidia.com/ Signed-off-by: Amir Goldstein --- fs/overlayfs/readdir.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index ba542a192012..b875332107d3 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -36,6 +36,7 @@ struct ovl_cache_entry { struct ovl_dir_cache { long refcount; u64 version; + bool is_refcounted; struct list_head entries; struct rb_root root; }; @@ -479,13 +480,31 @@ static void ovl_seek_cursor(struct ovl_dir_file *od, loff_t pos) od->cursor = p; } +/* + * Merged (refcounted) and impure (not refcounted) caches are never + * expected to be mixed. On mismatch, detach the cache from the inode + * and leak it rather than misuse it. A merged cache is still freed by + * its last ovl_cache_put(). + */ +static struct ovl_dir_cache *ovl_dir_cache_check(struct inode *inode, + bool is_refcounted) +{ + struct ovl_dir_cache *cache = ovl_dir_cache(inode); + + if (cache && WARN_ON(cache->is_refcounted != is_refcounted)) { + ovl_set_dir_cache(inode, NULL); + return NULL; + } + return cache; +} + static struct ovl_dir_cache *ovl_cache_get(struct dentry *dentry) { int res; struct ovl_dir_cache *cache; struct inode *inode = d_inode(dentry); - cache = ovl_dir_cache(inode); + cache = ovl_dir_cache_check(inode, true); if (cache && ovl_inode_version_get(inode) == cache->version) { WARN_ON(!cache->refcount); cache->refcount++; @@ -498,6 +517,7 @@ static struct ovl_dir_cache *ovl_cache_get(struct dentry *dentry) return ERR_PTR(-ENOMEM); cache->refcount = 1; + cache->is_refcounted = true; INIT_LIST_HEAD(&cache->entries); cache->root = RB_ROOT; @@ -698,7 +718,7 @@ static struct ovl_dir_cache *ovl_cache_get_impure(const struct path *path) struct ovl_fs *ofs = OVL_FS(dentry->d_sb); struct ovl_dir_cache *cache; - cache = ovl_dir_cache(inode); + cache = ovl_dir_cache_check(inode, false); if (cache && ovl_inode_version_get(inode) == cache->version) return cache; @@ -710,6 +730,7 @@ static struct ovl_dir_cache *ovl_cache_get_impure(const struct path *path) if (!cache) return ERR_PTR(-ENOMEM); + cache->is_refcounted = false; res = ovl_dir_read_impure(path, &cache->entries, &cache->root); if (res) { ovl_cache_free(&cache->entries); From e34d2ef818e4c7676d8d8e1bf43e6a1ed39efb2a Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:15:11 +0000 Subject: [PATCH 05/20] ovl: check that the parents of volatile/dirty are dirs An unprivileged user crashes the kernel by mounting overlayfs with the volatile option. A dentry that serves as the parent of a lookup must be a directory. ovl_create_volatile_dirty() walks work/incompat/volatile/dirty by name, and ovl_lookup_or_create() returns an existing entry whatever its type. A file or a symlink that another task creates during the mount becomes the parent of the next lookup, which calls a NULL ->lookup. An unprivileged user mounted overlayfs with "volatile" in a user namespace while a second process created a symlink named work/incompat, and the mount task oopsed on a NULL ->lookup call. Fix this by returning -ENOTDIR from ovl_lookup_or_create() when the caller asked for a directory and the existing entry is a non-dir. Fixes: c86243b090bc ("ovl: provide a mount option "volatile"") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik Signed-off-by: Amir Goldstein --- fs/overlayfs/super.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index bd0a3f9039d2..18f56d463572 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -621,16 +621,21 @@ static int ovl_check_rename_whiteout(struct ovl_fs *ofs) static struct dentry *ovl_lookup_or_create(struct ovl_fs *ofs, struct dentry *parent, - const char *name, umode_t mode) + const char *name, bool is_dir) { struct dentry *child; child = ovl_start_creating_upper(ofs, parent, &QSTR(name)); if (!IS_ERR(child)) { - if (!child->d_inode) + if (!child->d_inode) { child = ovl_create_real(ofs, parent, child, &QSTR(name), - OVL_CATTR(mode)); + OVL_CATTR(is_dir ? S_IFDIR : + S_IFREG)); + } else if (is_dir && !d_can_lookup(child)) { + end_creating(child); + child = ERR_PTR(-ENOTDIR); + } end_creating_keep(child); } dput(parent); @@ -652,7 +657,7 @@ static int ovl_create_volatile_dirty(struct ovl_fs *ofs) const char *const *name = volatile_path; for (ctr = ARRAY_SIZE(volatile_path); ctr; ctr--, name++) { - d = ovl_lookup_or_create(ofs, d, *name, ctr > 1 ? S_IFDIR : S_IFREG); + d = ovl_lookup_or_create(ofs, d, *name, ctr > 1); if (IS_ERR(d)) return PTR_ERR(d); } From 65bf000962a33ec9d2488e4e1797b6cf630abcbb Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:34:08 +0000 Subject: [PATCH 06/20] ovl: take a snapshot of the name for copy up The copy up of a directory reads a freed dentry name when a lookup moves the directory at the same time. A pointer to the name of a dentry is only valid while something stops a move of the dentry. ovl_do_copy_up() keeps a plain copy of c->dentry->d_name in c->destname, and ovl_copy_up_start() only keeps ovl_rename() away. When a lookup finds the same directory under another name after a change to the lower layer, d_splice_alias() moves the dentry and frees its external name. With a delay in ovl_do_copy_up(), a chmod of a lower-only directory with a 60 byte name raced with a rename of that directory in the lower layer and a stat of the new name, and KASAN reported a use-after-free in lookup_noperm_common(). Fix this by using take_dentry_name_snapshot() for c->destname. Fixes: 829c28be9bb9 ("ovl: use d_splice_alias() in place of d_add() in lookup") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index 92b3d08a0234..12000e45fefc 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -920,6 +920,7 @@ static int ovl_do_copy_up(struct ovl_copy_up_ctx *c) struct ovl_fs *ofs = OVL_FS(c->dentry->d_sb); struct dentry *origin = c->lowerpath.dentry; struct ovl_fh *fh = NULL; + struct name_snapshot name; bool to_index = false; /* @@ -957,12 +958,15 @@ static int ovl_do_copy_up(struct ovl_copy_up_ctx *c) goto out_free_fh; } else { /* - * c->dentry->d_name is stabilzed by ovl_copy_up_start(), - * because if we got here, it means that c->dentry has no upper - * alias and changing ->d_name means going through ovl_rename() - * that will call ovl_copy_up() on source and target dentry. + * ovl_copy_up_start() keeps ovl_rename() away from c->dentry, + * because ovl_rename() calls ovl_copy_up() on source and target + * dentry. It does not stop d_splice_alias() from moving a + * directory that a lookup found under another name after a + * change to the lower layer. That move frees an external name, + * so take a snapshot. */ - c->destname = c->dentry->d_name; + take_dentry_name_snapshot(&name, c->dentry); + c->destname = name.name; /* * Mark parent "impure" because it may now contain non-pure * upper @@ -971,7 +975,7 @@ static int ovl_do_copy_up(struct ovl_copy_up_ctx *c) err = ovl_set_impure(c->parent, c->destdir); ovl_end_write(c->dentry); if (err) - goto out_free_fh; + goto out; } /* Should we copyup with O_TMPFILE or with workdir? */ @@ -1005,6 +1009,8 @@ static int ovl_do_copy_up(struct ovl_copy_up_ctx *c) out: if (to_index) kfree(c->destname.name); + else + release_dentry_name_snapshot(&name); out_free_fh: kfree(fh); return err; From 15c6bd5cf109471baca7fbe46e8f574569257a28 Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Fri, 16 Oct 2026 13:21:34 +0200 Subject: [PATCH 07/20] vfs: add try_take_dentry_name_snapshot() take_dentry_name_snapshot() requires the caller to keep the dentry alive. The external name of a dead dentry has a zero refcount that never changes, so atomic_inc_not_zero() fails on every retry and the function never returns. A caller in RCU walk holds no reference on the dentry. Add a variant that fails instead of retrying when it cannot get a reference on the external name, so such a caller can return -ECHILD. Reviewed-by: Christian Brauner (Amutable) Signed-off-by: Amir Goldstein --- fs/dcache.c | 29 ++++++++++++++++++++++++++--- include/linux/dcache.h | 1 + 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/fs/dcache.c b/fs/dcache.c index f44c7b39c3b3..21ac036b3167 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -358,7 +358,8 @@ static inline int dname_external(const struct dentry *dentry) return dentry->d_name.name != dentry->d_shortname.string; } -void take_dentry_name_snapshot(struct name_snapshot *name, struct dentry *dentry) +static bool __take_dentry_name_snapshot(struct name_snapshot *name, + struct dentry *dentry, bool retry) { unsigned seq; const unsigned char *s; @@ -375,8 +376,12 @@ void take_dentry_name_snapshot(struct name_snapshot *name, struct dentry *dentry struct external_name *p; p = container_of(s, struct external_name, name[0]); // get a valid reference - if (unlikely(!atomic_inc_not_zero(&p->count))) - goto retry; + if (unlikely(!atomic_inc_not_zero(&p->count))) { + if (retry) + goto retry; + rcu_read_unlock(); + return false; + } name->name.name = s; } if (read_seqcount_retry(&dentry->d_seq, seq)) { @@ -384,9 +389,27 @@ void take_dentry_name_snapshot(struct name_snapshot *name, struct dentry *dentry goto retry; } rcu_read_unlock(); + return true; +} + +void take_dentry_name_snapshot(struct name_snapshot *name, struct dentry *dentry) +{ + __take_dentry_name_snapshot(name, dentry, true); } EXPORT_SYMBOL(take_dentry_name_snapshot); +/* + * For a caller that holds no reference on @dentry, e.g. in RCU walk. + * The external name of a dead dentry has a zero refcount forever, so + * take_dentry_name_snapshot() would retry without end. Fail instead. + */ +bool try_take_dentry_name_snapshot(struct name_snapshot *name, + struct dentry *dentry) +{ + return __take_dentry_name_snapshot(name, dentry, false); +} +EXPORT_SYMBOL(try_take_dentry_name_snapshot); + void release_dentry_name_snapshot(struct name_snapshot *name) { if (unlikely(name->name.name != name->inline_name.string)) { diff --git a/include/linux/dcache.h b/include/linux/dcache.h index 4b1ff99608e0..a4be11ca2df1 100644 --- a/include/linux/dcache.h +++ b/include/linux/dcache.h @@ -629,6 +629,7 @@ struct name_snapshot { union shortname_store inline_name; }; void take_dentry_name_snapshot(struct name_snapshot *, struct dentry *); +bool try_take_dentry_name_snapshot(struct name_snapshot *, struct dentry *); void release_dentry_name_snapshot(struct name_snapshot *); static inline struct dentry *d_first_child(const struct dentry *dentry) From 0b691c3c59a58aa6a2fc3f87c10cc1e399f1d33f Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:38:39 +0000 Subject: [PATCH 08/20] ovl: do not take a name snapshot in RCU walk A path lookup on an overlay over FUSE loops in the kernel for ever and stalls RCU. take_dentry_name_snapshot() needs a dentry that the caller keeps alive, because for a dead dentry the refcount of an external name stays at zero and the function retries without end. In RCU walk ovl_revalidate_real() holds no reference on the real dentry, and the eviction of the overlay inode on another CPU can drop the last one. With a delay in ovl_revalidate_real(), a stat() loop on a 48 byte name in an overlay over FUSE, raced with drop_caches, spun for ever in take_dentry_name_snapshot() and stalled RCU. Fix this by using try_take_dentry_name_snapshot() in RCU walk, and by returning -ECHILD when it fails. Fixes: 5be1fa8abd7b ("Pass parent directory inode and expected name to ->d_revalidate()") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik [amir: use try_take_dentry_name_snapshot() instead of open coded snapshot] Signed-off-by: Amir Goldstein --- fs/overlayfs/super.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index 18f56d463572..cc4b0959faf6 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -100,11 +100,17 @@ static int ovl_revalidate_real(struct dentry *d, unsigned int flags, bool weak) dir = d_inode_rcu(parent); if (!dir) return -ECHILD; + /* + * Nothing pins d in RCU walk: eviction of the overlay + * inode may drop the last reference at any time. + */ + if (!try_take_dentry_name_snapshot(&n, d)) + return -ECHILD; } else { parent = dget_parent(d); dir = d_inode(parent); + take_dentry_name_snapshot(&n, d); } - take_dentry_name_snapshot(&n, d); ret = d->d_op->d_revalidate(dir, &n.name, d, flags); release_dentry_name_snapshot(&n); if (!(flags & LOOKUP_RCU)) From 4f61ff2ebfb3d2967b83b6a4d456242244d5a61f Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:47:10 +0000 Subject: [PATCH 09/20] ovl: use the pinned upper dir in ovl_fix_origin() Renames made directly on the upper filesystem during a lookup crash the kernel. A dentry that is passed to vfs_setxattr() must be pinned. ovl_fix_origin() passes upper->d_parent to ovl_set_impure() without a reference, and the lookup holds no lock of the upper directory. A rename on the upper filesystem changes d_parent, and only the child pins the new parent, so a second rename and an rmdir free it. With delays in ovl_fix_origin() and ovl_set_impure(), renaming the upper directory twice and removing the intermediate parent during the lookup of a merge directory without an origin xattr gave a KASAN use-after-free in vfs_setxattr(). Fix this by passing the upper dentry that the overlay parent holds. Fixes: 9678e6303057 ("ovl: fix inconsistent d_ino for legacy merge dir") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik [amir: add helper ovl_set_impure_dir()] Signed-off-by: Amir Goldstein --- fs/overlayfs/namei.c | 6 ++++-- fs/overlayfs/overlayfs.h | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/namei.c b/fs/overlayfs/namei.c index ca899fdfaafd..cf7908a39f61 100644 --- a/fs/overlayfs/namei.c +++ b/fs/overlayfs/namei.c @@ -942,8 +942,10 @@ static int ovl_fix_origin(struct ovl_fs *ofs, struct dentry *dentry, goto out; err = ovl_set_origin_fh(ofs, fh, upper); - if (!err) - err = ovl_set_impure(dentry->d_parent, upper->d_parent); + if (!err) { + /* upper->d_parent is not stable, use the overlay parent */ + err = ovl_set_impure_dir(dentry->d_parent); + } ovl_drop_write(dentry); out: diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index 7f3558372c59..c89739e5338b 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -572,6 +572,12 @@ int ovl_check_setxattr(struct ovl_fs *ofs, struct dentry *upperdentry, enum ovl_xattr ox, const void *value, size_t size, int xerr); int ovl_set_impure(struct dentry *dentry, struct dentry *upperdentry); + +static inline int ovl_set_impure_dir(struct dentry *dentry) +{ + return ovl_set_impure(dentry, ovl_dentry_upper(dentry)); +} + bool ovl_inuse_trylock(struct dentry *dentry); void ovl_inuse_unlock(struct dentry *dentry); bool ovl_is_inuse(struct dentry *dentry); From 16d319e12328acafeafd3eecdf09c46d7fb43452 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 10:53:10 +0000 Subject: [PATCH 10/20] ovl: hold a reference on the parent in readdir Listing a directory on an overlay uses a freed dentry and crashes the kernel when a lookup moves the directory at the same time. Code that can sleep may only use d_parent without a reference while something stops a move of the dentry. ovl_iterate_real(), ovl_need_adjust_d_ino() and ovl_cache_update() use the d_parent of the listed directory and hold only the lock of that directory. When a lookup finds the directory under another parent after a change to a layer, d_splice_alias() moves it without that lock and drops its reference on the old parent. With a delay in ovl_iterate_real(), getdents64 on an overlay directory raced a rename in the lower layer and a lookup that moved its dentry, and after an rmdir of the old parent KASAN reported a use-after-free in security_inode_getattr(). Fix this by using dget_parent() in the three functions. Fixes: b5efccbe0a12 ("ovl: constant d_ino across copy up") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik [amir: fix comments in ovl_need_adjust_d_ino()] Signed-off-by: Amir Goldstein --- fs/overlayfs/readdir.c | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index b875332107d3..45239ef63e8c 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -588,8 +588,8 @@ static int ovl_cache_update(const struct path *path, struct ovl_cache_entry *p, goto get; } if (p->len == 2) { - /* we shall not be moved */ - this = dget(dir->d_parent); + /* d_splice_alias() can move even a locked dir */ + this = dget_parent(dir); goto get; } } @@ -818,6 +818,7 @@ static int ovl_iterate_real(struct file *file, struct dir_context *ctx) struct dentry *dir = file->f_path.dentry; struct ovl_fs *ofs = OVL_FS(dir->d_sb); const struct ovl_layer *lower_layer = ovl_layer_lower(dir); + struct dentry *parent; struct ovl_readdir_translate rdt = { .ctx.actor = ovl_fill_real, .ctx.count = ctx->count, @@ -829,18 +830,26 @@ static int ovl_iterate_real(struct file *file, struct dir_context *ctx) if (rdt.xinobits && lower_layer) rdt.fsid = lower_layer->fsid; - if (OVL_TYPE_MERGE(ovl_path_type(dir->d_parent))) { + /* + * The lock on dir does not stop d_splice_alias() from moving it to + * another parent, which drops the reference dir holds on this one. + */ + parent = dget_parent(dir); + if (OVL_TYPE_MERGE(ovl_path_type(parent))) { struct kstat stat; struct path statpath = file->f_path; - statpath.dentry = dir->d_parent; + statpath.dentry = parent; err = vfs_getattr(&statpath, &stat, STATX_INO, 0); - if (err) + if (err) { + dput(parent); return err; + } WARN_ON_ONCE(dir->d_sb->s_dev != stat.dev); rdt.parent_ino = stat.ino; } + dput(parent); if (ovl_is_impure_dir(file)) { rdt.cache = ovl_cache_get_impure(&file->f_path); @@ -896,8 +905,10 @@ static bool ovl_need_adjust_d_ino(struct file *file) { struct dentry *dentry = file->f_path.dentry; struct ovl_fs *ofs = OVL_FS(dentry->d_sb); + struct dentry *parent; + enum ovl_path_type type; - /* If parent is merge, then need to adjust d_ino for '..' */ + /* With xino, need to map real d_ino to the unified overlay ino space */ if (ovl_xino_bits(ofs)) return true; @@ -906,8 +917,14 @@ static bool ovl_need_adjust_d_ino(struct file *file) return false; /* If dir is impure then need to adjust d_ino for copied up entries */ - if (ovl_is_impure_dir(file) || - OVL_TYPE_MERGE(ovl_path_type(dentry->d_parent))) + if (ovl_is_impure_dir(file)) + return true; + + /* If parent is merge, then need to adjust d_ino for '..' */ + parent = dget_parent(dentry); + type = ovl_path_type(parent); + dput(parent); + if (OVL_TYPE_MERGE(type)) return true; /* Pure: no need to adjust d_ino */ From f635192ad223d2009ee640b63adf5ce3198aa338 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 11:00:06 +0000 Subject: [PATCH 11/20] ovl: do not copy up a dentry that was removed A change to a removed file through an open descriptor replaces whatever has its old name in the upper layer. Copy up must only create a name in the upper directory, never replace one. After an unlink, an rmdir or a rename over a lower dentry, a dentry that an open file keeps alive still has its old parent and name, but in the upper directory that name is now a whiteout or a new object. ovl_copy_up_workdir() renames the copy over it, which brings back the removed object and destroys whatever took its name. An unprivileged user opened a lower FIFO and unlinked it, root created a file at that name, and fchmod on the old descriptor replaced the file of root with the FIFO. Fix this by looking up the target name of the rename in ovl_copy_up_workdir() with LOOKUP_EXCL, so that copy up fails with -EEXIST if the name is taken, as copy up with O_TMPFILE already does when it links the copy to the name. Fixes: e9be9d5e76e3 ("overlay filesystem") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik [amir: use LOOKUP_EXCL instead of relying on d_unhashed()] Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index 12000e45fefc..286978d47ef7 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -793,9 +793,10 @@ static int ovl_copy_up_workdir(struct ovl_copy_up_ctx *c) rd.old_parent = c->workdir; rd.new_parent = c->destdir; rd.flags = 0; - err = start_renaming_dentry(&rd, 0, temp, + /* The name may have been taken since lookup, never replace it */ + err = start_renaming_dentry(&rd, LOOKUP_EXCL, temp, &QSTR_LEN(c->destname.name, c->destname.len)); - if (err) { + if (err && err != -EEXIST) { /* temp or workdir moved underneath us? map to -EIO */ err = -EIO; } From 2001cd7ca5560198131e46c6ca3c3472b06145c2 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Wed, 7 Oct 2026 12:10:59 +0000 Subject: [PATCH 12/20] ovl: fix a torn path in ovl_get_redirect() A hard link of a metacopy file that races with a rename of an ancestor stores a redirect to the wrong lower file. Each component of an absolute redirect must be the name that a dentry has in the parent that the walk continues with. ovl_get_redirect() copies the name under d_lock and calls dget_parent() after the unlock, and the callers for a hard link hold no lock that stops a rename across directories. A rename between the two steps combines the old name with the new parent. With delays in ovl_get_redirect(), an unprivileged user hard-linked a metacopy file while renaming its ancestor to another directory, the stored redirect named another lower path, and after a remount the file showed the data of a root-only file. Fix this by taking the parent first and checking it again under d_lock. Fixes: e04527fefba6 ("ovl: fix dentry leak in ovl_get_redirect") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Josef Bacik Signed-off-by: Amir Goldstein --- fs/overlayfs/dir.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c index 7beb0af26498..b3f6d4b82f68 100644 --- a/fs/overlayfs/dir.c +++ b/fs/overlayfs/dir.c @@ -1027,7 +1027,19 @@ static char *ovl_get_redirect(struct dentry *dentry, bool abs_redirect) const char *name; int thislen; + /* + * A rename can move d to another directory at any time. Hold + * d_lock to copy the name that d has in this very parent, or + * else the path mixes the old name with the new parent. + */ + tmp = dget_parent(d); spin_lock(&d->d_lock); + if (unlikely(tmp != d->d_parent)) { + spin_unlock(&d->d_lock); + dput(tmp); + continue; + } + name = ovl_dentry_get_redirect(d); if (name) { thislen = strlen(name); @@ -1040,13 +1052,13 @@ static char *ovl_get_redirect(struct dentry *dentry, bool abs_redirect) if (thislen + (name[0] != '/') > buflen) { ret = ERR_PTR(-EXDEV); spin_unlock(&d->d_lock); + dput(tmp); goto out_put; } buflen -= thislen; memcpy(&buf[buflen], name, thislen); spin_unlock(&d->d_lock); - tmp = dget_parent(d); dput(d); d = tmp; From 50912a1715b8a7c614b654011a0db750e372b43e Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Sun, 18 Oct 2026 11:52:52 +0200 Subject: [PATCH 13/20] ovl: skip copy up of empty security.capability ovl_copy_xattr() gives up if it can't read an attribute that listxattr() has named. An empty security.capability, which anybody has been able to store on any file for a long time, is such a case: it is listed, and getxattr() has nothing to return for it. A file in a lower layer that carries one can't be copied up: it can't be written to, renamed or have its mode changed. There is nothing to copy if there are no data. Go on with the next one. Fixes: 8db6c34f1dbc ("Introduce v3 namespaced file capabilities") Cc: stable@vger.kernel.org Signed-off-by: Josef Bacik [amir: treat -EINVAL as no capability too, and do the same in ovl_copy_up_meta_inode_data()] Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index 286978d47ef7..0cd76ca5b8d7 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -75,6 +75,15 @@ static int ovl_copy_acl(struct ovl_fs *ofs, const struct path *path, static ssize_t ovl_getxattr_value(const struct path *path, char *name, char **value, ssize_t *value_size); +/* + * Any user can store an empty security.capability, which fails to read with + * -EINVAL. It grants no capability, so treat it as none, like no xattr. + */ +static bool ovl_no_fscap(ssize_t err) +{ + return err == -ENODATA || err == -EOPNOTSUPP || err == -EINVAL; +} + int ovl_copy_xattr(struct super_block *sb, const struct path *oldpath, struct dentry *new) { struct dentry *old = oldpath->dentry; @@ -134,6 +143,10 @@ int ovl_copy_xattr(struct super_block *sb, const struct path *oldpath, struct de size = ovl_getxattr_value(oldpath, name, &value, &value_size); if (size < 0) { + if (!strcmp(name, XATTR_NAME_CAPS) && ovl_no_fscap(size)) { + error = 0; + continue; + } error = size; break; } @@ -1099,9 +1112,9 @@ static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c) if (c->stat.size) { err = cap_size = ovl_getxattr_value(&upperpath, XATTR_NAME_CAPS, &capability, &cap_size); - if (cap_size == -ENODATA || cap_size == -EOPNOTSUPP) + if (ovl_no_fscap(cap_size)) cap_size = 0; - if (cap_size < 0) + else if (cap_size < 0) goto out; } From eb644ed5c6d9afdb1554c41cef1f39d150fbc554 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:04 +0000 Subject: [PATCH 14/20] ovl: read the nlink xattr of every indexed non-dir With nfs_export=on, data written through a file handle to a lower file, i.e. a file that exists only in a lower layer of the overlay, can be lost on the next mount. A lookup of the file by name then returns -EIO. Also, a file with one name reports st_nlink 2 once it has been copied to the upper layer. After such a file is removed, its file handle can still open it until the next mount. Before a lower file is changed for the first time, overlayfs copies it to the writable upper layer. This is called copy up. The index directory is $workdir/index, where $workdir is the directory given by the workdir= option. Each entry is named after the file handle of a lower inode, so that overlayfs can get from the handle to the copied up inode. With nfs_export=on, copy up indexes every non-directory: it copies the file to an entry in the index directory and then hard links that entry into the upper directory. The index entry is an extra link to the upper inode, so overlayfs stores the link count that users should see in the overlay.nlink xattr. A file can also be opened by handle, either by an NFS client or by a process with CAP_DAC_READ_SEARCH calling open_by_handle_at(). If the file is not in the dcache, its overlay dentry is disconnected, i.e. it has no parent yet. Copy up then creates the index entry but no hard link in the upper directory. The index entry has i_nlink 1 and holds the only copy of the data written to the file. ovl_get_nlink() reads the stored count. Its caller passes a default, which the function returns if there is no stored count. If the lower inode has i_nlink 1, the function returns the default without reading the xattr. That was correct as long as only lower files with multiple links were indexed. On the next mount, ovl_verify_index() looks for orphans, i.e. index entries of files whose last name was removed. For an index entry with i_nlink 1, it passes a default of 0 to ovl_get_nlink() and treats a result of 0 as an orphan. Here the lower inode also has i_nlink 1, so the result is the default. ovl_indexdir_cleanup() then replaces the entry with a whiteout, which in the index marks a file as deleted. The written data is lost. A lookup by name then returns -EIO, because a whiteout as the index entry of a lower file that still exists is a bad index. ovl_get_inode() gets the wrong count the same way. Its default is the i_nlink of the upper inode, which includes the index entry, so a copied up file with one name reports st_nlink 2. When that name is removed, the count drops to 1 rather than 0. ovl_nlink_end() replaces the index entry with a whiteout only at a count of 0, so the entry stays, and the handle of the removed file still opens it. Reproduced by writing through open_by_handle_at() to an uncached lower file with one link. After a remount the data was lost, and the name returned EIO. Fix this by adding an argument to ovl_get_nlink() that says whether the caller found an index entry, and by skipping the xattr only if the caller found none. Known gaps: - An index entry that a kernel without this patch left behind for a removed file now stays, and the handle of that file keeps working. The entry has i_nlink 1 and a stored count of 1, just like the entry of a file that was only written by handle. Without this patch, the next mount treated it as an orphan. Fixes: fbd2d2074bde ("ovl: index all non-dir on copy up for NFS export") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Amir Goldstein --- fs/overlayfs/inode.c | 14 +++++++++++--- fs/overlayfs/namei.c | 2 +- fs/overlayfs/overlayfs.h | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c index 401cb8c75520..c9cca33008d3 100644 --- a/fs/overlayfs/inode.c +++ b/fs/overlayfs/inode.c @@ -999,7 +999,7 @@ int ovl_set_nlink_lower(struct dentry *dentry) } unsigned int ovl_get_nlink(struct ovl_fs *ofs, struct dentry *lowerdentry, - struct dentry *upperdentry, + struct dentry *upperdentry, bool indexed, unsigned int fallback) { int nlink_diff; @@ -1007,7 +1007,15 @@ unsigned int ovl_get_nlink(struct ovl_fs *ofs, struct dentry *lowerdentry, char buf[13]; int err; - if (!lowerdentry || !upperdentry || d_inode(lowerdentry)->i_nlink == 1) + if (!lowerdentry || !upperdentry) + return fallback; + + /* + * A lower inode with one link is not indexed on copy up, unless all + * files are indexed for NFS export. If the caller found an index entry, + * then the xattr holds the union nlink, whatever the lower nlink is. + */ + if (!indexed && d_inode(lowerdentry)->i_nlink == 1) return fallback; err = ovl_getxattr_upper(ofs, upperdentry, OVL_XATTR_NLINK, @@ -1264,7 +1272,7 @@ struct inode *ovl_get_inode(struct super_block *sb, /* Recalculate nlink for non-dir due to indexing */ if (!is_dir) nlink = ovl_get_nlink(ofs, lowerdentry, upperdentry, - nlink); + oip->index, nlink); set_nlink(inode, nlink); ino = key->i_ino; } else { diff --git a/fs/overlayfs/namei.c b/fs/overlayfs/namei.c index cf7908a39f61..ad0fb3d055ed 100644 --- a/fs/overlayfs/namei.c +++ b/fs/overlayfs/namei.c @@ -712,7 +712,7 @@ int ovl_verify_index(struct ovl_fs *ofs, struct dentry *index) if (err) goto fail; - if (ovl_get_nlink(ofs, origin.dentry, index, 0) == 0) + if (ovl_get_nlink(ofs, origin.dentry, index, true, 0) == 0) goto orphan; } diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index c89739e5338b..91cdd7e15696 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -808,7 +808,7 @@ static inline bool ovl_dir_is_real(struct inode *dir) int ovl_set_nlink_upper(struct dentry *dentry); int ovl_set_nlink_lower(struct dentry *dentry); unsigned int ovl_get_nlink(struct ovl_fs *ofs, struct dentry *lowerdentry, - struct dentry *upperdentry, + struct dentry *upperdentry, bool indexed, unsigned int fallback); int ovl_permission(struct mnt_idmap *idmap, struct inode *inode, int mask); From 949f9d8a5580c77a45fa759123367682aba4d173 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:07 +0000 Subject: [PATCH 15/20] ovl: always encode the origin fh when index is enabled On an overlay with index=on,nfs_export=on, copy up of a directory crashes the kernel once an LSM has started to deny CAP_DAC_READ_SEARCH to the mounter, the task that mounted the overlay. The mount and the LSM policy change require root. After that, any user who can write to a directory that exists only in a lower layer triggers the oops by doing so. Before a file or directory that exists only in a lower layer is changed for the first time, overlayfs copies it to the writable upper layer. This is called copy up. On copy up, ovl_get_origin_fh() encodes the file handle of the lower inode, called the origin. It returns NULL if ovl_can_decode_fh() says that the lower filesystem cannot decode handles. Whether the callers can handle NULL depends on the index option: - Without an index, NULL is fine: the callers store an empty origin xattr. - With index enabled, overlayfs keeps entries for copied up inodes in an internal index directory. The name of an index entry is derived from the handle, so ovl_get_origin_fh() must return a handle or an error. The mount only enables the index if ovl_can_decode_fh() succeeds for all layers, but the result can change later. The function checks the export operations of the filesystem and capable(CAP_DAC_READ_SEARCH). Copy up runs with the mounter's credentials, and an LSM can start to deny the capability to those credentials after the mount. ovl_get_origin_fh() then returns NULL with index enabled. This goes wrong in two callers: - With nfs_export=on, copy up of a directory creates an index entry for the directory. ovl_create_index() passes the NULL handle to ovl_get_index_name_fh(), which dereferences it. - If the result changes while mount(2) is still running, ovl_get_indexdir() passes a NULL from ovl_get_origin_fh() to ovl_verify_set_fh() in the same way. Before the commit in the Fixes tag, these two callers encoded the handle themselves, without consulting ovl_can_decode_fh(). Reproduced with a BPF LSM program that started to deny the capability after the mount. An unprivileged touch in a directory that had not been copied up yet oopsed in ovl_get_index_name_fh(). With this patch the touch succeeds and the directory gets its index entry. Fix this by calling ovl_can_decode_fh() in ovl_get_origin_fh() only when index is disabled. Fixes: 5b02bfc1e7e3 ("ovl: do not encode lower fh with upper sb_writers held") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Amir Goldstein --- fs/overlayfs/copy_up.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index 0cd76ca5b8d7..c147ad155b79 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -474,8 +474,12 @@ struct ovl_fh *ovl_get_origin_fh(struct ovl_fs *ofs, struct dentry *origin) * When lower layer doesn't support export operations store a 'null' fh, * so we can use the overlay.origin xattr to distignuish between a copy * up and a pure upper inode. + * + * With index enabled, mount already found that all layers can decode + * file handles. Do not ask again, because an LSM may change its answer + * about CAP_DAC_READ_SEARCH and index users cannot cope with a 'null' fh. */ - if (!ovl_can_decode_fh(origin->d_sb)) + if (!ofs->config.index && !ovl_can_decode_fh(origin->d_sb)) return NULL; return ovl_encode_real_fh(ofs, d_inode(origin), false); From 75e89c0055c1d7f7eeb462995f11253ca5864f79 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:23 +0000 Subject: [PATCH 16/20] ovl: set the revalidate flags after d_instantiate() stat() of a file on an overlayfs mount triggers a kernel warning and fails with ECHILD if another task is creating that file at the same time. This happens on an overlay with a FUSE upper layer, which is the layer where overlayfs creates new files. Write access to a directory on such an overlay is all a user needs to run the two tasks. The revalidate flags are DCACHE_OP_REVALIDATE and DCACHE_OP_WEAK_REVALIDATE. They tell path walk to call ->d_revalidate() or ->d_weak_revalidate() on a dentry, so that the filesystem can check that the cached dentry is still valid. An overlay dentry only has one of these flags if a dentry in one of its layers has it. FUSE dentries have the first one. When a flag is set on an overlay dentry, path walk calls ovl_dentry_revalidate_common(). That function only expects a dentry without an inode in RCU walk, while the dentry is being destroyed. For such a dentry outside of RCU walk, it hits a WARN_ON() and returns -ECHILD. So a negative overlay dentry must never have the flags set. ovl_instantiate() breaks this rule. When a task creates a file, the overlay dentry for the new name is hashed but still negative. ovl_instantiate() then works in this order: - It copies the flags from the new upper dentry to the overlay dentry. - It calls ovl_get_inode(), which can sleep in a getxattr request to the FUSE server. - Only then does it call d_instantiate(). A second task that looks up the name between the first step and the last finds a negative dentry with the flags set and revalidates it. Reproduced with a FUSE server that delays its getxattr reply: stat() of a name that was being created hit the WARN_ON() and failed with ECHILD. Fix this by setting the flags after d_instantiate(). Fixes: bccece1ead36 ("ovl: allow remote upper") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Amir Goldstein --- fs/overlayfs/dir.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c index b3f6d4b82f68..f716ac13badf 100644 --- a/fs/overlayfs/dir.c +++ b/fs/overlayfs/dir.c @@ -304,7 +304,6 @@ static int ovl_instantiate(struct dentry *dentry, struct inode *inode, }; ovl_dentry_set_upper_alias(dentry); - ovl_dentry_init_reval(dentry, newdentry, NULL); if (!hardlink) { /* @@ -335,6 +334,13 @@ static int ovl_instantiate(struct dentry *dentry, struct inode *inode, d_mark_tmpfile(tmpfile, inode); d_instantiate(dentry, inode); + /* + * Unless it is a tmpfile, the dentry is hashed and a path walk can find + * it at any time. Set the revalidate flags only now, because + * ovl_dentry_revalidate_common() does not expect them on a negative + * dentry. + */ + ovl_dentry_init_reval(dentry, ovl_dentry_upper(dentry), NULL); if (inode != oip.newinode) { pr_warn_ratelimited("newly created inode found in cache (%pd2)\n", dentry); From aefaf9521c999206b7cd2df8435a270ea8b8112b Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Fri, 16 Oct 2026 17:11:13 +0200 Subject: [PATCH 17/20] selftests/ovl: fix detached layer mountinfo expectations set_layers_via_detached_mount_fds was copied from the attached-fd test and still looks for /tmp paths. Detached open_tree() layers are recorded by d_path() as upperdir=/upper, workdir=/work, and lowerdir+=/ and datadir+=/ for each cloned directory. The test has failed since it was added. Reported-by: Disha Goel Link: https://lore.kernel.org/linux-unionfs/20260924112858.51392-2-disgoel@linux.ibm.com Fixes: ccc829b15d48a ("selftests: add tests for using detached mount with overlayfs") Signed-off-by: Amir Goldstein --- .../filesystems/overlayfs/set_layers_via_fds.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c b/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c index 7a293544233d..83bbacb2550f 100644 --- a/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c +++ b/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c @@ -686,23 +686,23 @@ TEST_F(set_layers_via_fds, set_layers_via_detached_mount_fds) while (getline(&line, &len, f_mountinfo) != -1) { char *haystack = line; - if (strstr(haystack, "workdir=/tmp/w")) + if (strstr(haystack, "workdir=/work")) layers_found[0] = true; - if (strstr(haystack, "upperdir=/tmp/u")) + if (strstr(haystack, "upperdir=/upper")) layers_found[1] = true; - if (strstr(haystack, "lowerdir+=/tmp/l1")) + if (strstr(haystack, "lowerdir+=/")) layers_found[2] = true; - if (strstr(haystack, "lowerdir+=/tmp/l2")) + if (strstr(haystack, "lowerdir+=/")) layers_found[3] = true; - if (strstr(haystack, "lowerdir+=/tmp/l3")) + if (strstr(haystack, "lowerdir+=/")) layers_found[4] = true; - if (strstr(haystack, "lowerdir+=/tmp/l4")) + if (strstr(haystack, "lowerdir+=/")) layers_found[5] = true; - if (strstr(haystack, "datadir+=/tmp/d1")) + if (strstr(haystack, "datadir+=/")) layers_found[6] = true; - if (strstr(haystack, "datadir+=/tmp/d2")) + if (strstr(haystack, "datadir+=/")) layers_found[7] = true; - if (strstr(haystack, "datadir+=/tmp/d3")) + if (strstr(haystack, "datadir+=/")) layers_found[8] = true; } free(line); From 333ad304d4794d6384087ea7fc16e58ab642809b Mon Sep 17 00:00:00 2001 From: Amir Goldstein Date: Sat, 17 Oct 2026 18:43:30 +0200 Subject: [PATCH 18/20] ovl: fix mount setup failing when override_creds drops CAP_SYS_ADMIN When a process drops CAP_SYS_ADMIN and calls fsconfig(FSCONFIG_SET_FLAG, "override_creds"), overlayfs saves those restricted credentials in ofs->creator_cred. ovl_fill_super() then wraps the entire mount setup in with_ovl_creds(), switching to those restricted credentials for the duration. Mount setup calls clone_private_mount(), which checks CAP_SYS_ADMIN against current credentials. With restricted credentials active, this check fails with -EPERM even though the task issuing FSCONFIG_CMD_CREATE is fully privileged. Use the mounter's real credentials only for cloning the private layer mounts. Keep all other backing filesystem operations during mount setup under the stashed credentials. Reported-by: Disha Goel Closes: https://lore.kernel.org/linux-unionfs/20260924112858.51392-1-disgoel@linux.ibm.com Fixes: c28f922c9dcee ("clone_private_mnt(): make sure that caller has CAP_SYS_ADMIN in the right userns") Cc: stable@vger.kernel.org Reviewed-by: Christian Brauner (Amutable) Signed-off-by: Amir Goldstein --- fs/overlayfs/super.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index cc4b0959faf6..b452b8c5f641 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -503,6 +503,20 @@ static int ovl_report_in_use(struct ovl_fs *ofs, const char *name) } } +/* + * clone_private_mount() checks CAP_SYS_ADMIN in current->cred. + * ovl_fill_super_creds() runs under the stashed creator cred, which + * override_creds may have dropped. + * The task that issued FSCONFIG_CMD_CREATE is still current->real_cred, so + * use that only for the clone. creator_cred stays in effect for every other + * backing-fs operation. + */ +static struct vfsmount *ovl_clone_private_mount(const struct path *path) +{ + scoped_with_creds(current_real_cred()) + return clone_private_mount(path); +} + static int ovl_get_upper(struct super_block *sb, struct ovl_fs *ofs, struct ovl_layer *upper_layer, const struct path *upperpath) @@ -526,7 +540,7 @@ static int ovl_get_upper(struct super_block *sb, struct ovl_fs *ofs, if (err) goto out; - upper_mnt = clone_private_mount(upperpath); + upper_mnt = ovl_clone_private_mount(upperpath); err = PTR_ERR(upper_mnt); if (IS_ERR(upper_mnt)) { pr_err("failed to clone upperpath\n"); @@ -1104,7 +1118,7 @@ static int ovl_get_layers(struct super_block *sb, struct ovl_fs *ofs, } } - mnt = clone_private_mount(&l->path); + mnt = ovl_clone_private_mount(&l->path); err = PTR_ERR(mnt); if (IS_ERR(mnt)) { pr_err("failed to clone lowerpath\n"); From a65eb1808177a4381718abe563dddebf840598f9 Mon Sep 17 00:00:00 2001 From: Hungyu Lin Date: Wed, 28 Oct 2026 19:51:33 +0000 Subject: [PATCH 19/20] ovl: reserve space for the casefolded name terminator ovl_casefold() allocates NAME_MAX bytes and passes that capacity to utf8_casefold(), which also needs room for a terminating NUL. A valid name whose folded form is exactly NAME_MAX bytes therefore fails to fold, and ovl_fill_merge() falls back to comparing the original bytes. For example, when two casefolded layers contain directories named with 255 uppercase and lowercase ASCII letters respectively, readdir lists both names even though lookup treats them as the same directory. The same names merge correctly at 254 bytes. Reserve one additional byte in both the allocation and the folding capacity so that NAME_MAX-byte folded names use the canonical merge key. Fixes: ee95c5fc86dd ("ovl: Create ovl_casefold() to support casefolded strncmp()") Cc: stable@vger.kernel.org Signed-off-by: Hungyu Lin Signed-off-by: Amir Goldstein --- fs/overlayfs/readdir.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/readdir.c b/fs/overlayfs/readdir.c index 45239ef63e8c..2018e3114655 100644 --- a/fs/overlayfs/readdir.c +++ b/fs/overlayfs/readdir.c @@ -82,13 +82,13 @@ static int ovl_casefold(struct ovl_readdir_data *rdd, const char *str, int len, name_is_dot_dotdot(str, len)) return 0; - cf_name = kmalloc(NAME_MAX, GFP_KERNEL); + cf_name = kmalloc(NAME_MAX + 1, GFP_KERNEL); if (!cf_name) { rdd->err = -ENOMEM; return -ENOMEM; } - cf_len = utf8_casefold(rdd->map, &qstr, cf_name, NAME_MAX); + cf_len = utf8_casefold(rdd->map, &qstr, cf_name, NAME_MAX + 1); if (cf_len > 0) *dst = cf_name; else From 1d27da7de3f74405b1de85ad26c5ea90f826114b Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 16 Oct 2026 21:17:17 +0000 Subject: [PATCH 20/20] ovl: check the casefold flag of directories only On an overlay over casefolded (case-insensitive) directories, every lookup of a file that is not a directory fails with EREMOTE, and the first file create triggers a kernel warning. No privilege is needed: an unprivileged user can cause both in a user namespace, with the layers on a tmpfs that they have mounted with the casefold option. Some filesystems support casefolding, which is case-insensitive name lookup. It is enabled per directory, and the inode of a casefolded directory has the S_CASEFOLD flag. overlayfs accepts layers on a filesystem that supports casefolding as long as casefolding is enabled on every directory of the layers, or on none. The mount records which of the two applies in ofs->casefold. ovl_lookup_single() compares the S_CASEFOLD flag of each file that it finds against ofs->casefold and returns -EREMOTE on a mismatch. Filesystems only set S_CASEFOLD on directories, so with casefolded layers a regular file, a symlink or a special file never matches and its lookup fails. ovl_get_inode() does the same comparison in a WARN_ON_ONCE() for each new overlay inode. A failed lookup never gets that far, so it is the first file create that trips the warning. Reproduced on an overlay over casefolded tmpfs or ext4 directories, with stat() of a regular file, a symlink and a FIFO, and with a file create: each stat() failed with EREMOTE. For an unprivileged user in a user namespace, on tmpfs, stat() of a regular file failed in the same way, and the file create triggered the warning. Fix this by limiting both checks to directories. Fixes: 16754d61dc69 ("ovl: Support mounting case-insensitive enabled layers") Cc: stable@vger.kernel.org Signed-off-by: Chris Mason Signed-off-by: Amir Goldstein --- fs/overlayfs/inode.c | 2 +- fs/overlayfs/namei.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c index c9cca33008d3..7e1ac70c5d97 100644 --- a/fs/overlayfs/inode.c +++ b/fs/overlayfs/inode.c @@ -1287,7 +1287,7 @@ struct inode *ovl_get_inode(struct super_block *sb, } ovl_fill_inode(inode, realinode->i_mode, realinode->i_rdev); ovl_inode_init(inode, oip, ino, fsid); - WARN_ON_ONCE(!!IS_CASEFOLDED(inode) != ofs->casefold); + WARN_ON_ONCE(is_dir && !!IS_CASEFOLDED(inode) != ofs->casefold); if (upperdentry && ovl_is_impuredir(sb, upperdentry)) ovl_set_flag(OVL_IMPURE, inode); diff --git a/fs/overlayfs/namei.c b/fs/overlayfs/namei.c index ad0fb3d055ed..c6127edb9be9 100644 --- a/fs/overlayfs/namei.c +++ b/fs/overlayfs/namei.c @@ -266,7 +266,8 @@ static int ovl_lookup_single(struct dentry *base, struct ovl_lookup_data *d, goto out_err; } - if (ofs->casefold != ovl_dentry_casefolded(this)) { + /* Filesystems set the casefold flag only on directories */ + if (d_is_dir(this) && ofs->casefold != ovl_dentry_casefolded(this)) { warn = "child wrong casefold"; err = -EREMOTE; goto out_warn;