From 0d9ff5c4219fd9e14a8c0543c5247ec4e631a70a Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Thu, 23 Apr 2026 14:18:51 -0400 Subject: [PATCH 01/34] nfsd/blocklayout: always ignore loca_time_modify RFC 8881 Section 18.42 makes it clear that the client provided timestamp is a "may" condition, and clients that want to force a specific timestamp should send a separate SETATTR in the compound. Since commit b82f92d5dd1a ("fs: have setattr_copy handle multigrain timestamps appropriately") the ia_mtime value is ignored by file systems using multi-grain timestamps like XFS, which is the only file system supporting blocklayout exports right now, so make that explicit in NFSD as well. Signed-off-by: Christoph Hellwig Reviewed-by: Jeff Layton Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260423181854.743150-2-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/blocklayout.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/fs/nfsd/blocklayout.c b/fs/nfsd/blocklayout.c index 9d829c84f374..24cc5025f649 100644 --- a/fs/nfsd/blocklayout.c +++ b/fs/nfsd/blocklayout.c @@ -179,15 +179,20 @@ static __be32 nfsd4_block_commit_blocks(struct inode *inode, struct nfsd4_layoutcommit *lcp, struct iomap *iomaps, int nr_iomaps) { - struct timespec64 mtime = inode_get_mtime(inode); struct iattr iattr = { .ia_valid = 0 }; int error; - if (lcp->lc_mtime.tv_nsec == UTIME_NOW || - timespec64_compare(&lcp->lc_mtime, &mtime) < 0) - lcp->lc_mtime = current_time(inode); + /* + * This ignores the client provided mtime in loca_time_modify, as a + * fully client specified mtime doesn't really fit into the Linux + * multi-grain timestamp architecture. + * + * RFC 8881 Section 18.42 makes it clear that the client provided + * timestamp is a "may" condition, and clients that want to force a + * specific timestamp should send a separate SETATTR in the compound. + */ iattr.ia_valid |= ATTR_ATIME | ATTR_CTIME | ATTR_MTIME; - iattr.ia_atime = iattr.ia_ctime = iattr.ia_mtime = lcp->lc_mtime; + iattr.ia_atime = iattr.ia_ctime = iattr.ia_mtime = current_time(inode); if (lcp->lc_size_chg) { iattr.ia_valid |= ATTR_SIZE; From d5758c31a81bcd9d5ac8a7456549b05df3579068 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Thu, 23 Apr 2026 14:18:52 -0400 Subject: [PATCH 02/34] exportfs: split out the ops for layout-based block device access The support to grant layouts for direct block device access works at a very different layer than the rest of exports. Split the methods for it into a separate struct, and move that into a separate header to better split things out. The pointer to the new operation vector is kept in export_operations to avoid bloating the super_block. Signed-off-by: Christoph Hellwig Reviewed-by: Jeff Layton Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260423181854.743150-3-cel@kernel.org Signed-off-by: Christian Brauner --- MAINTAINERS | 2 +- fs/nfsd/blocklayout.c | 14 ++++++------ fs/nfsd/nfs4layouts.c | 9 ++++---- fs/xfs/xfs_export.c | 4 +--- fs/xfs/xfs_pnfs.c | 12 ++++++++--- fs/xfs/xfs_pnfs.h | 11 +++++----- include/linux/exportfs.h | 25 +++++++--------------- include/linux/exportfs_block.h | 39 ++++++++++++++++++++++++++++++++++ 8 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 include/linux/exportfs_block.h diff --git a/MAINTAINERS b/MAINTAINERS index 2fb1c75afd16..bad36d48a447 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9903,7 +9903,7 @@ S: Supported F: Documentation/filesystems/nfs/exporting.rst F: fs/exportfs/ F: fs/fhandle.c -F: include/linux/exportfs.h +F: include/linux/exportfs*.h FILESYSTEMS [IDMAPPED MOUNTS] M: Christian Brauner diff --git a/fs/nfsd/blocklayout.c b/fs/nfsd/blocklayout.c index 24cc5025f649..e612fcf8666a 100644 --- a/fs/nfsd/blocklayout.c +++ b/fs/nfsd/blocklayout.c @@ -2,7 +2,7 @@ /* * Copyright (c) 2014-2016 Christoph Hellwig. */ -#include +#include #include #include #include @@ -32,8 +32,8 @@ nfsd4_block_map_extent(struct inode *inode, const struct svc_fh *fhp, u32 device_generation = 0; int error; - error = sb->s_export_op->map_blocks(inode, offset, length, &iomap, - iomode != IOMODE_READ, &device_generation); + error = sb->s_export_op->block_ops->map_blocks(inode, offset, length, + &iomap, iomode != IOMODE_READ, &device_generation); if (error) { if (error == -ENXIO) return nfserr_layoutunavailable; @@ -199,8 +199,8 @@ nfsd4_block_commit_blocks(struct inode *inode, struct nfsd4_layoutcommit *lcp, iattr.ia_size = lcp->lc_newsize; } - error = inode->i_sb->s_export_op->commit_blocks(inode, iomaps, - nr_iomaps, &iattr); + error = inode->i_sb->s_export_op->block_ops->commit_blocks(inode, + iomaps, nr_iomaps, &iattr); kfree(iomaps); return nfserrno(error); } @@ -223,8 +223,8 @@ nfsd4_block_get_device_info_simple(struct super_block *sb, b->type = PNFS_BLOCK_VOLUME_SIMPLE; b->simple.sig_len = PNFS_BLOCK_UUID_LEN; - return sb->s_export_op->get_uuid(sb, b->simple.sig, &b->simple.sig_len, - &b->simple.offset); + return sb->s_export_op->block_ops->get_uuid(sb, b->simple.sig, + &b->simple.sig_len, &b->simple.offset); } static __be32 diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c index 69e41105efdd..cf5b7eb417c5 100644 --- a/fs/nfsd/nfs4layouts.c +++ b/fs/nfsd/nfs4layouts.c @@ -3,6 +3,7 @@ * Copyright (c) 2014 Christoph Hellwig. */ #include +#include #include #include #include @@ -129,6 +130,7 @@ void nfsd4_setup_layout_type(struct svc_export *exp) { #if defined(CONFIG_NFSD_BLOCKLAYOUT) || defined(CONFIG_NFSD_SCSILAYOUT) struct super_block *sb = exp->ex_path.mnt->mnt_sb; + const struct exportfs_block_ops *bops = sb->s_export_op->block_ops; #endif if (!(exp->ex_flags & NFSEXP_PNFS)) @@ -138,14 +140,11 @@ void nfsd4_setup_layout_type(struct svc_export *exp) exp->ex_layout_types |= 1 << LAYOUT_FLEX_FILES; #endif #ifdef CONFIG_NFSD_BLOCKLAYOUT - if (sb->s_export_op->get_uuid && - sb->s_export_op->map_blocks && - sb->s_export_op->commit_blocks) + if (bops && bops->get_uuid && bops->map_blocks && bops->commit_blocks) exp->ex_layout_types |= 1 << LAYOUT_BLOCK_VOLUME; #endif #ifdef CONFIG_NFSD_SCSILAYOUT - if (sb->s_export_op->map_blocks && - sb->s_export_op->commit_blocks && + if (bops && bops->map_blocks && bops->commit_blocks && sb->s_bdev && sb->s_bdev->bd_disk->fops->pr_ops && sb->s_bdev->bd_disk->fops->get_unique_id) diff --git a/fs/xfs/xfs_export.c b/fs/xfs/xfs_export.c index e3e3c3c89840..9b2ad3786b19 100644 --- a/fs/xfs/xfs_export.c +++ b/fs/xfs/xfs_export.c @@ -244,8 +244,6 @@ const struct export_operations xfs_export_operations = { .get_parent = xfs_fs_get_parent, .commit_metadata = xfs_fs_nfs_commit_metadata, #ifdef CONFIG_EXPORTFS_BLOCK_OPS - .get_uuid = xfs_fs_get_uuid, - .map_blocks = xfs_fs_map_blocks, - .commit_blocks = xfs_fs_commit_blocks, + .block_ops = &xfs_export_block_ops, #endif }; diff --git a/fs/xfs/xfs_pnfs.c b/fs/xfs/xfs_pnfs.c index 221e55887a2a..12e083f1b9ba 100644 --- a/fs/xfs/xfs_pnfs.c +++ b/fs/xfs/xfs_pnfs.c @@ -49,7 +49,7 @@ xfs_break_leased_layouts( * Get a unique ID including its location so that the client can identify * the exported device. */ -int +static int xfs_fs_get_uuid( struct super_block *sb, u8 *buf, @@ -104,7 +104,7 @@ xfs_fs_map_update_inode( /* * Get a layout for the pNFS client. */ -int +static int xfs_fs_map_blocks( struct inode *inode, loff_t offset, @@ -252,7 +252,7 @@ xfs_pnfs_validate_isize( * to manually flush the cache here similar to what the fsync code path does * for datasyncs on files that have no dirty metadata. */ -int +static int xfs_fs_commit_blocks( struct inode *inode, struct iomap *maps, @@ -332,3 +332,9 @@ xfs_fs_commit_blocks( xfs_iunlock(ip, XFS_IOLOCK_EXCL); return error; } + +const struct exportfs_block_ops xfs_export_block_ops = { + .get_uuid = xfs_fs_get_uuid, + .map_blocks = xfs_fs_map_blocks, + .commit_blocks = xfs_fs_commit_blocks, +}; diff --git a/fs/xfs/xfs_pnfs.h b/fs/xfs/xfs_pnfs.h index 940c6c2ad88c..bf43b2009e4c 100644 --- a/fs/xfs/xfs_pnfs.h +++ b/fs/xfs/xfs_pnfs.h @@ -2,13 +2,9 @@ #ifndef _XFS_PNFS_H #define _XFS_PNFS_H 1 -#ifdef CONFIG_EXPORTFS_BLOCK_OPS -int xfs_fs_get_uuid(struct super_block *sb, u8 *buf, u32 *len, u64 *offset); -int xfs_fs_map_blocks(struct inode *inode, loff_t offset, u64 length, - struct iomap *iomap, bool write, u32 *device_generation); -int xfs_fs_commit_blocks(struct inode *inode, struct iomap *maps, int nr_maps, - struct iattr *iattr); +#include +#ifdef CONFIG_EXPORTFS_BLOCK_OPS int xfs_break_leased_layouts(struct inode *inode, uint *iolock, bool *did_unlock); #else @@ -18,4 +14,7 @@ xfs_break_leased_layouts(struct inode *inode, uint *iolock, bool *did_unlock) return 0; } #endif /* CONFIG_EXPORTFS_BLOCK_OPS */ + +extern const struct exportfs_block_ops xfs_export_block_ops; + #endif /* _XFS_PNFS_H */ diff --git a/include/linux/exportfs.h b/include/linux/exportfs.h index 8bcdba28b406..c835bc64f4fa 100644 --- a/include/linux/exportfs.h +++ b/include/linux/exportfs.h @@ -6,9 +6,8 @@ #include struct dentry; -struct iattr; +struct exportfs_block_ops; struct inode; -struct iomap; struct super_block; struct vfsmount; @@ -260,19 +259,13 @@ struct handle_to_path_ctx { * @commit_metadata: * @commit_metadata should commit metadata changes to stable storage. * - * @get_uuid: - * Get a filesystem unique signature exposed to clients. - * - * @map_blocks: - * Map and, if necessary, allocate blocks for a layout. - * - * @commit_blocks: - * Commit blocks in a layout once the client is done with them. - * * @flags: * Allows the filesystem to communicate to nfsd that it may want to do things * differently when dealing with it. * + * @block_ops: + * Operations for layout grants to block on the underlying device. + * * Locking rules: * get_parent is called with child->d_inode->i_rwsem down * get_name is not (which is possibly inconsistent) @@ -290,12 +283,6 @@ struct export_operations { struct dentry * (*get_parent)(struct dentry *child); int (*commit_metadata)(struct inode *inode); - int (*get_uuid)(struct super_block *sb, u8 *buf, u32 *len, u64 *offset); - int (*map_blocks)(struct inode *inode, loff_t offset, - u64 len, struct iomap *iomap, - bool write, u32 *device_generation); - int (*commit_blocks)(struct inode *inode, struct iomap *iomaps, - int nr_iomaps, struct iattr *iattr); int (*permission)(struct handle_to_path_ctx *ctx, unsigned int oflags); struct file * (*open)(const struct path *path, unsigned int oflags); #define EXPORT_OP_NOWCC (0x1) /* don't collect v3 wcc data */ @@ -308,6 +295,10 @@ struct export_operations { #define EXPORT_OP_FLUSH_ON_CLOSE (0x20) /* fs flushes file data on close */ #define EXPORT_OP_NOLOCKS (0x40) /* no file locking support */ unsigned long flags; + +#ifdef CONFIG_EXPORTFS_BLOCK_OPS + const struct exportfs_block_ops *block_ops; +#endif }; /** diff --git a/include/linux/exportfs_block.h b/include/linux/exportfs_block.h new file mode 100644 index 000000000000..1f52fea8e4dc --- /dev/null +++ b/include/linux/exportfs_block.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2014-2026 Christoph Hellwig. + * + * Support for exportfs-based layout grants for direct block device access. + */ +#ifndef LINUX_EXPORTFS_BLOCK_H +#define LINUX_EXPORTFS_BLOCK_H 1 + +#include + +struct iattr; +struct inode; +struct iomap; +struct super_block; + +struct exportfs_block_ops { + /* + * Get the in-band device unique signature exposed to clients. + */ + int (*get_uuid)(struct super_block *sb, u8 *buf, u32 *len, u64 *offset); + + /* + * Map blocks for direct block access. + * If @write is %true, also allocate the blocks for the range if needed. + */ + int (*map_blocks)(struct inode *inode, loff_t offset, u64 len, + struct iomap *iomap, bool write, + u32 *device_generation); + + /* + * Commit blocks previously handed out by ->map_blocks and written to by + * the client. + */ + int (*commit_blocks)(struct inode *inode, struct iomap *iomaps, + int nr_iomaps, struct iattr *iattr); +}; + +#endif /* LINUX_EXPORTFS_BLOCK_H */ From 61eb48f515853937a6237e7f64dbe9d099b54613 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Thu, 23 Apr 2026 14:18:53 -0400 Subject: [PATCH 03/34] exportfs: don't pass struct iattr to ->commit_blocks The only thing ->commit_blocks really needs is the new size, with a magic -1 placeholder 0 for "do not change the size" because it only ever extends the size. Signed-off-by: Christoph Hellwig Reviewed-by: Jeff Layton Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260423181854.743150-4-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/blocklayout.c | 12 ++---------- fs/xfs/xfs_pnfs.c | 19 ++++++++++--------- include/linux/exportfs_block.h | 3 +-- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/fs/nfsd/blocklayout.c b/fs/nfsd/blocklayout.c index e612fcf8666a..5be7721c22c2 100644 --- a/fs/nfsd/blocklayout.c +++ b/fs/nfsd/blocklayout.c @@ -179,7 +179,6 @@ static __be32 nfsd4_block_commit_blocks(struct inode *inode, struct nfsd4_layoutcommit *lcp, struct iomap *iomaps, int nr_iomaps) { - struct iattr iattr = { .ia_valid = 0 }; int error; /* @@ -191,16 +190,9 @@ nfsd4_block_commit_blocks(struct inode *inode, struct nfsd4_layoutcommit *lcp, * timestamp is a "may" condition, and clients that want to force a * specific timestamp should send a separate SETATTR in the compound. */ - iattr.ia_valid |= ATTR_ATIME | ATTR_CTIME | ATTR_MTIME; - iattr.ia_atime = iattr.ia_ctime = iattr.ia_mtime = current_time(inode); - - if (lcp->lc_size_chg) { - iattr.ia_valid |= ATTR_SIZE; - iattr.ia_size = lcp->lc_newsize; - } - error = inode->i_sb->s_export_op->block_ops->commit_blocks(inode, - iomaps, nr_iomaps, &iattr); + iomaps, nr_iomaps, + lcp->lc_size_chg ? lcp->lc_newsize : 0); kfree(iomaps); return nfserrno(error); } diff --git a/fs/xfs/xfs_pnfs.c b/fs/xfs/xfs_pnfs.c index 12e083f1b9ba..7d689bb2efd9 100644 --- a/fs/xfs/xfs_pnfs.c +++ b/fs/xfs/xfs_pnfs.c @@ -257,23 +257,22 @@ xfs_fs_commit_blocks( struct inode *inode, struct iomap *maps, int nr_maps, - struct iattr *iattr) + loff_t new_size) { struct xfs_inode *ip = XFS_I(inode); struct xfs_mount *mp = ip->i_mount; struct xfs_trans *tp; + struct timespec64 now; bool update_isize = false; int error, i; loff_t size; - ASSERT(iattr->ia_valid & (ATTR_ATIME|ATTR_CTIME|ATTR_MTIME)); - xfs_ilock(ip, XFS_IOLOCK_EXCL); size = i_size_read(inode); - if ((iattr->ia_valid & ATTR_SIZE) && iattr->ia_size > size) { + if (new_size > size) { update_isize = true; - size = iattr->ia_size; + size = new_size; } for (i = 0; i < nr_maps; i++) { @@ -318,11 +317,13 @@ xfs_fs_commit_blocks( xfs_trans_ijoin(tp, ip, XFS_ILOCK_EXCL); xfs_trans_log_inode(tp, ip, XFS_ILOG_CORE); - ASSERT(!(iattr->ia_valid & (ATTR_UID | ATTR_GID))); - setattr_copy(&nop_mnt_idmap, inode, iattr); + now = inode_set_ctime_current(inode); + inode_set_atime_to_ts(inode, now); + inode_set_mtime_to_ts(inode, now); + if (update_isize) { - i_size_write(inode, iattr->ia_size); - ip->i_disk_size = iattr->ia_size; + i_size_write(inode, new_size); + ip->i_disk_size = new_size; } xfs_trans_set_sync(tp); diff --git a/include/linux/exportfs_block.h b/include/linux/exportfs_block.h index 1f52fea8e4dc..d1dec4689b14 100644 --- a/include/linux/exportfs_block.h +++ b/include/linux/exportfs_block.h @@ -9,7 +9,6 @@ #include -struct iattr; struct inode; struct iomap; struct super_block; @@ -33,7 +32,7 @@ struct exportfs_block_ops { * the client. */ int (*commit_blocks)(struct inode *inode, struct iomap *iomaps, - int nr_iomaps, struct iattr *iattr); + int nr_iomaps, loff_t new_size); }; #endif /* LINUX_EXPORTFS_BLOCK_H */ From da9baa5470dcb077a7c9806f0925c60b530c470b Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Thu, 23 Apr 2026 14:18:54 -0400 Subject: [PATCH 04/34] exportfs,nfsd: rework checking for layout-based block device access support Currently NFSD hard codes checking support for block-style layouts. Lift the checks into a file system-helper and provide a exportfs-level helper to implement the typical checks. This prepares for supporting block layout export of multiple devices per file system. Signed-off-by: Christoph Hellwig Reviewed-by: Jeff Layton Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260423181854.743150-5-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/export.c | 3 +- fs/nfsd/nfs4layouts.c | 26 +++++------------ fs/xfs/xfs_pnfs.c | 13 +++++++++ include/linux/exportfs_block.h | 52 +++++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/fs/nfsd/export.c b/fs/nfsd/export.c index 665153f1720e..35fef3197a66 100644 --- a/fs/nfsd/export.c +++ b/fs/nfsd/export.c @@ -735,7 +735,8 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen) goto out4; err = 0; - nfsd4_setup_layout_type(&exp); + if (exp.ex_flags & NFSEXP_PNFS) + nfsd4_setup_layout_type(&exp); } expp = svc_export_lookup(&exp); diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c index cf5b7eb417c5..c3543d456702 100644 --- a/fs/nfsd/nfs4layouts.c +++ b/fs/nfsd/nfs4layouts.c @@ -2,7 +2,6 @@ /* * Copyright (c) 2014 Christoph Hellwig. */ -#include #include #include #include @@ -128,28 +127,17 @@ nfsd4_set_deviceid(struct nfsd4_deviceid *id, const struct svc_fh *fhp, void nfsd4_setup_layout_type(struct svc_export *exp) { -#if defined(CONFIG_NFSD_BLOCKLAYOUT) || defined(CONFIG_NFSD_SCSILAYOUT) struct super_block *sb = exp->ex_path.mnt->mnt_sb; - const struct exportfs_block_ops *bops = sb->s_export_op->block_ops; -#endif + expfs_block_layouts_t block_supported = exportfs_layouts_supported(sb); - if (!(exp->ex_flags & NFSEXP_PNFS)) - return; - -#ifdef CONFIG_NFSD_FLEXFILELAYOUT - exp->ex_layout_types |= 1 << LAYOUT_FLEX_FILES; -#endif -#ifdef CONFIG_NFSD_BLOCKLAYOUT - if (bops && bops->get_uuid && bops->map_blocks && bops->commit_blocks) + if (IS_ENABLED(CONFIG_NFSD_FLEXFILELAYOUT)) + exp->ex_layout_types |= 1 << LAYOUT_FLEX_FILES; + if (IS_ENABLED(CONFIG_NFSD_BLOCKLAYOUT) && + (block_supported & EXPFS_BLOCK_IN_BAND_ID)) exp->ex_layout_types |= 1 << LAYOUT_BLOCK_VOLUME; -#endif -#ifdef CONFIG_NFSD_SCSILAYOUT - if (bops && bops->map_blocks && bops->commit_blocks && - sb->s_bdev && - sb->s_bdev->bd_disk->fops->pr_ops && - sb->s_bdev->bd_disk->fops->get_unique_id) + if (IS_ENABLED(CONFIG_NFSD_SCSILAYOUT) && + (block_supported & EXPFS_BLOCK_OUT_OF_BAND_ID)) exp->ex_layout_types |= 1 << LAYOUT_SCSI; -#endif } void nfsd4_close_layout(struct nfs4_layout_stateid *ls) diff --git a/fs/xfs/xfs_pnfs.c b/fs/xfs/xfs_pnfs.c index 7d689bb2efd9..266a07601e8d 100644 --- a/fs/xfs/xfs_pnfs.c +++ b/fs/xfs/xfs_pnfs.c @@ -13,6 +13,7 @@ #include "xfs_bmap.h" #include "xfs_iomap.h" #include "xfs_pnfs.h" +#include /* * Ensure that we do not have any outstanding pNFS layouts that can be used by @@ -45,6 +46,17 @@ xfs_break_leased_layouts( return error; } +static expfs_block_layouts_t +xfs_fs_layouts_supported( + struct super_block *sb) +{ + expfs_block_layouts_t supported = EXPFS_BLOCK_IN_BAND_ID; + + if (exportfs_bdev_supports_out_of_band_id(sb->s_bdev)) + supported |= EXPFS_BLOCK_OUT_OF_BAND_ID; + return supported; +} + /* * Get a unique ID including its location so that the client can identify * the exported device. @@ -335,6 +347,7 @@ xfs_fs_commit_blocks( } const struct exportfs_block_ops xfs_export_block_ops = { + .layouts_supported = xfs_fs_layouts_supported, .get_uuid = xfs_fs_get_uuid, .map_blocks = xfs_fs_map_blocks, .commit_blocks = xfs_fs_commit_blocks, diff --git a/include/linux/exportfs_block.h b/include/linux/exportfs_block.h index d1dec4689b14..de519b7b599b 100644 --- a/include/linux/exportfs_block.h +++ b/include/linux/exportfs_block.h @@ -7,13 +7,35 @@ #ifndef LINUX_EXPORTFS_BLOCK_H #define LINUX_EXPORTFS_BLOCK_H 1 -#include +#include +#include +#include struct inode; struct iomap; struct super_block; +/* + * There are the two types of block-style layout support: + * - In-band implies a device identified by a unique cookie inside the actual + * device address space checked by the ->get_uuid method as used by the pNFS + * block layout. This is a bit dangerous and deprecated. + * - Out of band implies identification by out of band unique identifiers + * specified by the storage protocol, which is much safer and used by the + * pNFS SCSI/NVMe layouts. + */ +typedef unsigned int __bitwise expfs_block_layouts_t; +#define EXPFS_BLOCK_FLAG(__bit) \ + ((__force expfs_block_layouts_t)(1u << __bit)) +#define EXPFS_BLOCK_IN_BAND_ID EXPFS_BLOCK_FLAG(0) +#define EXPFS_BLOCK_OUT_OF_BAND_ID EXPFS_BLOCK_FLAG(1) + struct exportfs_block_ops { + /* + * Returns the EXPFS_BLOCK_* bitmap of supported layout types. + */ + expfs_block_layouts_t (*layouts_supported)(struct super_block *sb); + /* * Get the in-band device unique signature exposed to clients. */ @@ -35,4 +57,32 @@ struct exportfs_block_ops { int nr_iomaps, loff_t new_size); }; +static inline bool +exportfs_bdev_supports_out_of_band_id(struct block_device *bdev) +{ + return bdev->bd_disk->fops->pr_ops && + bdev->bd_disk->fops->get_unique_id; +} + +#ifdef CONFIG_EXPORTFS_BLOCK_OPS +static inline expfs_block_layouts_t +exportfs_layouts_supported(struct super_block *sb) +{ + const struct exportfs_block_ops *bops = sb->s_export_op->block_ops; + + if (!bops || + !bops->layouts_supported || + WARN_ON_ONCE(!bops->map_blocks) || + WARN_ON_ONCE(!bops->commit_blocks)) + return 0; + return bops->layouts_supported(sb); +} +#else +static inline expfs_block_layouts_t +exportfs_layouts_supported(struct super_block *sb) +{ + return 0; +} +#endif /* CONFIG_EXPORTFS_BLOCK_OPS */ + #endif /* LINUX_EXPORTFS_BLOCK_H */ From 14c3197ecf074549429b12b60e2a0a3fb875f75f Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:54 -0400 Subject: [PATCH 05/34] fs: Move file_kattr initialization to callers fileattr_fill_xflags() and fileattr_fill_flags() memset the entire file_kattr struct before populating select fields, so callers cannot pre-set fields in fa->fsx_xflags without having their values clobbered. Darrick Wong noted that a function named "fill_xflags" touching more than xflags forces callers to know implementation details beyond its apparent scope. Drop the memset from both fill functions and initialize at the entry points instead: ioctl_setflags(), ioctl_fssetxattr(), the file_setattr() syscall, and xfs_ioc_fsgetxattra() now declare fa with an aggregate initializer. ioctl_getflags(), ioctl_fsgetxattr(), and the file_getattr() syscall already aggregate-initialize fa to pass flags_valid/fsx_valid hints into vfs_fileattr_get(). Subsequent patches rely on this so that ->fileattr_get() handlers can set case-sensitivity flags (FS_XFLAG_CASEFOLD, FS_XFLAG_CASENONPRESERVING) in fa->fsx_xflags before the fill functions run. Suggested-by: Darrick J. Wong Reviewed-by: Jan Kara Reviewed-by: Darrick J. Wong Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-1-e62cc8200435@oracle.com Reviewed-by: "Darrick J. Wong" Signed-off-by: Christian Brauner --- fs/file_attr.c | 12 ++++-------- fs/xfs/xfs_ioctl.c | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/fs/file_attr.c b/fs/file_attr.c index da983e105d70..f429da66a317 100644 --- a/fs/file_attr.c +++ b/fs/file_attr.c @@ -15,12 +15,10 @@ * @fa: fileattr pointer * @xflags: FS_XFLAG_* flags * - * Set ->fsx_xflags, ->fsx_valid and ->flags (translated xflags). All - * other fields are zeroed. + * Set ->fsx_xflags, ->fsx_valid and ->flags (translated xflags). */ void fileattr_fill_xflags(struct file_kattr *fa, u32 xflags) { - memset(fa, 0, sizeof(*fa)); fa->fsx_valid = true; fa->fsx_xflags = xflags; if (fa->fsx_xflags & FS_XFLAG_IMMUTABLE) @@ -48,11 +46,9 @@ EXPORT_SYMBOL(fileattr_fill_xflags); * @flags: FS_*_FL flags * * Set ->flags, ->flags_valid and ->fsx_xflags (translated flags). - * All other fields are zeroed. */ void fileattr_fill_flags(struct file_kattr *fa, u32 flags) { - memset(fa, 0, sizeof(*fa)); fa->flags_valid = true; fa->flags = flags; if (fa->flags & FS_SYNC_FL) @@ -325,7 +321,7 @@ int ioctl_setflags(struct file *file, unsigned int __user *argp) { struct mnt_idmap *idmap = file_mnt_idmap(file); struct dentry *dentry = file->f_path.dentry; - struct file_kattr fa; + struct file_kattr fa = {}; unsigned int flags; int err; @@ -357,7 +353,7 @@ int ioctl_fssetxattr(struct file *file, void __user *argp) { struct mnt_idmap *idmap = file_mnt_idmap(file); struct dentry *dentry = file->f_path.dentry; - struct file_kattr fa; + struct file_kattr fa = {}; int err; err = copy_fsxattr_from_user(&fa, argp); @@ -431,7 +427,7 @@ SYSCALL_DEFINE5(file_setattr, int, dfd, const char __user *, filename, struct path filepath __free(path_put) = {}; unsigned int lookup_flags = 0; struct file_attr fattr; - struct file_kattr fa; + struct file_kattr fa = {}; int error; BUILD_BUG_ON(sizeof(struct file_attr) < FILE_ATTR_SIZE_VER0); diff --git a/fs/xfs/xfs_ioctl.c b/fs/xfs/xfs_ioctl.c index 46e234863644..ed9b4846c05f 100644 --- a/fs/xfs/xfs_ioctl.c +++ b/fs/xfs/xfs_ioctl.c @@ -517,7 +517,7 @@ xfs_ioc_fsgetxattra( xfs_inode_t *ip, void __user *arg) { - struct file_kattr fa; + struct file_kattr fa = {}; xfs_ilock(ip, XFS_ILOCK_SHARED); xfs_fill_fsxattr(ip, XFS_ATTR_FORK, &fa); From 3035e4454142327ec5faee2ff57ab7cb1e9fc712 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:55 -0400 Subject: [PATCH 06/34] fs: Add case sensitivity flags to file_kattr Enable upper layers such as NFSD to retrieve case sensitivity information from file systems by adding FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING flags. Filesystems report case-insensitive or case-nonpreserving behavior by setting these flags directly in fa->fsx_xflags. The default (flags unset) indicates POSIX semantics: case-sensitive and case-preserving. Both flags are added to FS_XFLAG_RDONLY_MASK so FS_IOC_FSSETXATTR silently strips them, keeping the new xflags strictly a reporting interface. Callers that want to toggle casefolding continue to use FS_IOC_SETFLAGS with FS_CASEFOLD_FL, the established UAPI on filesystems that support the operation (ext4 and f2fs on empty directories). Case sensitivity information is exported to userspace via the fa_xflags field in the FS_IOC_FSGETXATTR ioctl and file_getattr() system call. Reviewed-by: "Darrick J. Wong" Reviewed-by: Jan Kara Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-2-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/file_attr.c | 4 ++++ include/linux/fileattr.h | 3 ++- include/uapi/linux/fs.h | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/fs/file_attr.c b/fs/file_attr.c index f429da66a317..bfb00d256dd5 100644 --- a/fs/file_attr.c +++ b/fs/file_attr.c @@ -37,6 +37,8 @@ void fileattr_fill_xflags(struct file_kattr *fa, u32 xflags) fa->flags |= FS_PROJINHERIT_FL; if (fa->fsx_xflags & FS_XFLAG_VERITY) fa->flags |= FS_VERITY_FL; + if (fa->fsx_xflags & FS_XFLAG_CASEFOLD) + fa->flags |= FS_CASEFOLD_FL; } EXPORT_SYMBOL(fileattr_fill_xflags); @@ -67,6 +69,8 @@ void fileattr_fill_flags(struct file_kattr *fa, u32 flags) fa->fsx_xflags |= FS_XFLAG_PROJINHERIT; if (fa->flags & FS_VERITY_FL) fa->fsx_xflags |= FS_XFLAG_VERITY; + if (fa->flags & FS_CASEFOLD_FL) + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; } EXPORT_SYMBOL(fileattr_fill_flags); diff --git a/include/linux/fileattr.h b/include/linux/fileattr.h index 3780904a63a6..58044b598016 100644 --- a/include/linux/fileattr.h +++ b/include/linux/fileattr.h @@ -16,7 +16,8 @@ /* Read-only inode flags */ #define FS_XFLAG_RDONLY_MASK \ - (FS_XFLAG_PREALLOC | FS_XFLAG_HASATTR | FS_XFLAG_VERITY) + (FS_XFLAG_PREALLOC | FS_XFLAG_HASATTR | FS_XFLAG_VERITY | \ + FS_XFLAG_CASEFOLD | FS_XFLAG_CASENONPRESERVING) /* Flags to indicate valid value of fsx_ fields */ #define FS_XFLAG_VALUES_MASK \ diff --git a/include/uapi/linux/fs.h b/include/uapi/linux/fs.h index 13f71202845e..2ea4c81df08f 100644 --- a/include/uapi/linux/fs.h +++ b/include/uapi/linux/fs.h @@ -254,6 +254,13 @@ struct file_attr { #define FS_XFLAG_DAX 0x00008000 /* use DAX for IO */ #define FS_XFLAG_COWEXTSIZE 0x00010000 /* CoW extent size allocator hint */ #define FS_XFLAG_VERITY 0x00020000 /* fs-verity enabled */ +/* + * Case handling flags (read-only, cannot be set via ioctl). + * Default (neither set) indicates POSIX semantics: case-sensitive + * lookups and case-preserving storage. + */ +#define FS_XFLAG_CASEFOLD 0x00040000 /* case-insensitive lookups */ +#define FS_XFLAG_CASENONPRESERVING 0x00080000 /* case not preserved */ #define FS_XFLAG_HASATTR 0x80000000 /* no DIFLAG for this */ /* the read-only stuff doesn't really belong here, but any other place is From c92db2ca726fe61a66580d30ecff8c192a791935 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:56 -0400 Subject: [PATCH 07/34] fat: Implement fileattr_get for case sensitivity Report FAT's case sensitivity behavior via the FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING flags. FAT filesystems are case-insensitive by default. MSDOS supports a 'nocase' mount option that enables case-sensitive behavior; check this option when reporting case sensitivity. VFAT long filename entries preserve case; without VFAT, only uppercased 8.3 short names are stored. MSDOS with 'nocase' also preserves case since the name-formatting code skips upcasing when 'nocase' is set. Check both options when reporting case preservation. Reviewed-by: Jan Kara Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-3-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/fat/fat.h | 3 +++ fs/fat/file.c | 36 ++++++++++++++++++++++++++++++++++++ fs/fat/namei_msdos.c | 1 + fs/fat/namei_vfat.c | 1 + 4 files changed, 41 insertions(+) diff --git a/fs/fat/fat.h b/fs/fat/fat.h index 5a58f0bf8ce8..99ed9228a677 100644 --- a/fs/fat/fat.h +++ b/fs/fat/fat.h @@ -10,6 +10,8 @@ #include #include +struct file_kattr; + /* * vfat shortname flags */ @@ -408,6 +410,7 @@ extern void fat_truncate_blocks(struct inode *inode, loff_t offset); extern int fat_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, u32 request_mask, unsigned int flags); +int fat_fileattr_get(struct dentry *dentry, struct file_kattr *fa); extern int fat_file_fsync(struct file *file, loff_t start, loff_t end, int datasync); diff --git a/fs/fat/file.c b/fs/fat/file.c index becccdd2e501..37e7049b4c8c 100644 --- a/fs/fat/file.c +++ b/fs/fat/file.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "fat.h" static long fat_fallocate(struct file *file, int mode, @@ -398,6 +399,40 @@ void fat_truncate_blocks(struct inode *inode, loff_t offset) fat_flush_inodes(inode->i_sb, inode, NULL); } +int fat_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct msdos_sb_info *sbi = MSDOS_SB(dentry->d_sb); + bool case_sensitive; + + /* + * FAT filesystems are case-insensitive by default. VFAT + * becomes case-sensitive when mounted with 'check=strict', + * which installs vfat_dentry_ops. MSDOS has no such option; + * its 'nocase' mount option selects case-sensitive matching. + * + * VFAT long filename entries preserve case. Without VFAT, only + * uppercased 8.3 short names are stored. MSDOS with 'nocase' + * also preserves case. + */ + if (sbi->options.isvfat) + case_sensitive = sbi->options.name_check == 's'; + else + case_sensitive = sbi->options.nocase; + + if (!case_sensitive) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + if (!sbi->options.isvfat) + fa->fsx_xflags |= FS_XFLAG_CASENONPRESERVING; + } + if (d_inode(dentry)->i_flags & S_IMMUTABLE) { + fa->fsx_xflags |= FS_XFLAG_IMMUTABLE; + fa->flags |= FS_IMMUTABLE_FL; + } + return 0; +} +EXPORT_SYMBOL_GPL(fat_fileattr_get); + int fat_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, u32 request_mask, unsigned int flags) { @@ -575,5 +610,6 @@ EXPORT_SYMBOL_GPL(fat_setattr); const struct inode_operations fat_file_inode_operations = { .setattr = fat_setattr, .getattr = fat_getattr, + .fileattr_get = fat_fileattr_get, .update_time = fat_update_time, }; diff --git a/fs/fat/namei_msdos.c b/fs/fat/namei_msdos.c index 4cc65f330fb7..0fd2971ad4b1 100644 --- a/fs/fat/namei_msdos.c +++ b/fs/fat/namei_msdos.c @@ -644,6 +644,7 @@ static const struct inode_operations msdos_dir_inode_operations = { .rename = msdos_rename, .setattr = fat_setattr, .getattr = fat_getattr, + .fileattr_get = fat_fileattr_get, .update_time = fat_update_time, }; diff --git a/fs/fat/namei_vfat.c b/fs/fat/namei_vfat.c index 918b3756674c..e909447873e3 100644 --- a/fs/fat/namei_vfat.c +++ b/fs/fat/namei_vfat.c @@ -1185,6 +1185,7 @@ static const struct inode_operations vfat_dir_inode_operations = { .rename = vfat_rename2, .setattr = fat_setattr, .getattr = fat_getattr, + .fileattr_get = fat_fileattr_get, .update_time = fat_update_time, }; From 27e0b573dd4aa927670fbfd84732e569fde72078 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:57 -0400 Subject: [PATCH 08/34] exfat: Implement fileattr_get for case sensitivity Report exFAT's case sensitivity behavior via the FS_XFLAG_CASEFOLD flag. exFAT compares names through the volume's upcase table; in practice that table folds case, and case is preserved at rest. Acked-by: Namjae Jeon Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://github.com/exfatprogs/exfatprogs/issues/313 Link: https://patch.msgid.link/20260507-case-sensitivity-v14-4-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/exfat/exfat_fs.h | 2 ++ fs/exfat/file.c | 18 ++++++++++++++++-- fs/exfat/namei.c | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h index 89ef5368277f..aff4dcd4e75a 100644 --- a/fs/exfat/exfat_fs.h +++ b/fs/exfat/exfat_fs.h @@ -496,6 +496,8 @@ int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry, int exfat_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, unsigned int request_mask, unsigned int query_flags); +struct file_kattr; +int exfat_fileattr_get(struct dentry *dentry, struct file_kattr *fa); int exfat_file_fsync(struct file *file, loff_t start, loff_t end, int datasync); long exfat_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); long exfat_compat_ioctl(struct file *filp, unsigned int cmd, diff --git a/fs/exfat/file.c b/fs/exfat/file.c index 354bdcfe4abc..91e5511945d1 100644 --- a/fs/exfat/file.c +++ b/fs/exfat/file.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "exfat_raw.h" #include "exfat_fs.h" @@ -323,6 +324,18 @@ int exfat_getattr(struct mnt_idmap *idmap, const struct path *path, return 0; } +int exfat_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + /* + * exFAT compares filenames through an upcase table, so lookup + * is always case-insensitive. Long names are stored in UTF-16 + * with case intact; CASENONPRESERVING stays clear. + */ + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + return 0; +} + int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry, struct iattr *attr) { @@ -817,6 +830,7 @@ const struct file_operations exfat_file_operations = { }; const struct inode_operations exfat_file_inode_operations = { - .setattr = exfat_setattr, - .getattr = exfat_getattr, + .setattr = exfat_setattr, + .getattr = exfat_getattr, + .fileattr_get = exfat_fileattr_get, }; diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c index 2c5636634b4a..94002e43db08 100644 --- a/fs/exfat/namei.c +++ b/fs/exfat/namei.c @@ -1311,4 +1311,5 @@ const struct inode_operations exfat_dir_inode_operations = { .rename = exfat_rename, .setattr = exfat_setattr, .getattr = exfat_getattr, + .fileattr_get = exfat_fileattr_get, }; From eeb7b37b9700f0dbb3e6fe7b9e910b466ac190dd Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:58 -0400 Subject: [PATCH 09/34] ntfs3: Implement fileattr_get for case sensitivity Report NTFS case sensitivity behavior via the FS_XFLAG_CASEFOLD flag. NTFS always preserves case at rest. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-5-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/ntfs3/file.c | 29 +++++++++++++++++++++++++++++ fs/ntfs3/namei.c | 1 + fs/ntfs3/ntfs_fs.h | 1 + 3 files changed, 31 insertions(+) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index b041639ab406..ad9350d7fc3f 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -180,6 +180,34 @@ long ntfs_compat_ioctl(struct file *filp, u32 cmd, unsigned long arg) } #endif +/* + * ntfs_fileattr_get - inode_operations::fileattr_get + */ +int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct inode *inode = d_inode(dentry); + struct ntfs_sb_info *sbi = inode->i_sb->s_fs_info; + + /* Avoid any operation if inode is bad. */ + if (unlikely(is_bad_ni(ntfs_i(inode)))) + return -EINVAL; + + /* + * NTFS preserves case (the default). Case sensitivity depends on + * mount options: with "nocase", NTFS is case-insensitive; + * otherwise it is case-sensitive. + */ + if (sbi->options->nocase) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + if (inode->i_flags & S_IMMUTABLE) { + fa->fsx_xflags |= FS_XFLAG_IMMUTABLE; + fa->flags |= FS_IMMUTABLE_FL; + } + return 0; +} + /* * ntfs_getattr - inode_operations::getattr */ @@ -1547,6 +1575,7 @@ const struct inode_operations ntfs_file_inode_operations = { .get_acl = ntfs_get_acl, .set_acl = ntfs_set_acl, .fiemap = ntfs_fiemap, + .fileattr_get = ntfs_fileattr_get, }; const struct file_operations ntfs_file_operations = { diff --git a/fs/ntfs3/namei.c b/fs/ntfs3/namei.c index b2af8f695e60..e159ba66a34a 100644 --- a/fs/ntfs3/namei.c +++ b/fs/ntfs3/namei.c @@ -518,6 +518,7 @@ const struct inode_operations ntfs_dir_inode_operations = { .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, .fiemap = ntfs_fiemap, + .fileattr_get = ntfs_fileattr_get, }; const struct inode_operations ntfs_special_inode_operations = { diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index bbf3b6a1dcbe..41db22d652c4 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -529,6 +529,7 @@ bool dir_is_empty(struct inode *dir); extern const struct file_operations ntfs_dir_operations; /* Globals from file.c */ +int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, u32 request_mask, u32 flags); int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, From b6fe046c30236e37e3f8c500cf5b1297c317c5ee Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:52:59 -0400 Subject: [PATCH 10/34] hfs: Implement fileattr_get for case sensitivity Report HFS case sensitivity behavior via the FS_XFLAG_CASEFOLD flag. HFS is always case-insensitive (using Mac OS Roman case folding) and always preserves case at rest. Reviewed-by: Viacheslav Dubeyko Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-6-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/hfs/dir.c | 1 + fs/hfs/hfs_fs.h | 2 ++ fs/hfs/inode.c | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/fs/hfs/dir.c b/fs/hfs/dir.c index f5e7efe924e7..c4c6e1623f55 100644 --- a/fs/hfs/dir.c +++ b/fs/hfs/dir.c @@ -328,4 +328,5 @@ const struct inode_operations hfs_dir_inode_operations = { .rmdir = hfs_remove, .rename = hfs_rename, .setattr = hfs_inode_setattr, + .fileattr_get = hfs_fileattr_get, }; diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h index ac0e83f77a0f..1b23448c9a48 100644 --- a/fs/hfs/hfs_fs.h +++ b/fs/hfs/hfs_fs.h @@ -177,6 +177,8 @@ extern int hfs_get_block(struct inode *inode, sector_t block, extern const struct address_space_operations hfs_aops; extern const struct address_space_operations hfs_btree_aops; +struct file_kattr; +int hfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); int hfs_write_begin(const struct kiocb *iocb, struct address_space *mapping, loff_t pos, unsigned int len, struct folio **foliop, void **fsdata); diff --git a/fs/hfs/inode.c b/fs/hfs/inode.c index 89b33a9d46d5..f41cc261684d 100644 --- a/fs/hfs/inode.c +++ b/fs/hfs/inode.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "hfs_fs.h" #include "btree.h" @@ -699,6 +700,18 @@ static int hfs_file_fsync(struct file *filp, loff_t start, loff_t end, return ret; } +int hfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + /* + * HFS compares filenames using Mac OS Roman case folding, so + * lookup is always case-insensitive. Names are stored on disk + * with case intact; CASENONPRESERVING stays clear. + */ + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + return 0; +} + static const struct file_operations hfs_file_operations = { .llseek = generic_file_llseek, .read_iter = generic_file_read_iter, @@ -715,4 +728,5 @@ static const struct inode_operations hfs_file_inode_operations = { .lookup = hfs_file_lookup, .setattr = hfs_inode_setattr, .listxattr = generic_listxattr, + .fileattr_get = hfs_fileattr_get, }; From a6469a15eefe10e8c5e49eb80cc38dbe94803ea9 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:00 -0400 Subject: [PATCH 11/34] hfsplus: Report case sensitivity in fileattr_get Add case sensitivity reporting to the existing hfsplus_fileattr_get() function via the FS_XFLAG_CASEFOLD flag. HFS+ always preserves case at rest. Case sensitivity depends on how the volume was formatted: HFSX volumes may be either case-sensitive or case-insensitive, indicated by the HFSPLUS_SB_CASEFOLD superblock flag. FS_XFLAG_CASEFOLD is read-only: FS_XFLAG_RDONLY_MASK ensures FS_IOC_FSSETXATTR strips it. The legacy FS_IOC_SETFLAGS path in hfsplus_fileattr_set() also allows FS_CASEFOLD_FL through its allowlist on case-insensitive volumes so that a chattr read-modify-write cycle does not fail with EOPNOTSUPP. Reviewed-by: Viacheslav Dubeyko Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-7-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/hfsplus/inode.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c index d05891ec492e..5565c14b4bf6 100644 --- a/fs/hfsplus/inode.c +++ b/fs/hfsplus/inode.c @@ -740,6 +740,7 @@ int hfsplus_fileattr_get(struct dentry *dentry, struct file_kattr *fa) { struct inode *inode = d_inode(dentry); struct hfsplus_inode_info *hip = HFSPLUS_I(inode); + struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb); unsigned int flags = 0; if (inode->i_flags & S_IMMUTABLE) @@ -748,6 +749,8 @@ int hfsplus_fileattr_get(struct dentry *dentry, struct file_kattr *fa) flags |= FS_APPEND_FL; if (hip->userflags & HFSPLUS_FLG_NODUMP) flags |= FS_NODUMP_FL; + if (test_bit(HFSPLUS_SB_CASEFOLD, &sbi->flags)) + flags |= FS_CASEFOLD_FL; fileattr_fill_flags(fa, flags); @@ -759,13 +762,24 @@ int hfsplus_fileattr_set(struct mnt_idmap *idmap, { struct inode *inode = d_inode(dentry); struct hfsplus_inode_info *hip = HFSPLUS_I(inode); + struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb); + unsigned int allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL; unsigned int new_fl = 0; if (fileattr_has_fsx(fa)) return -EOPNOTSUPP; + /* + * FS_CASEFOLD_FL reflects HFSPLUS_SB_CASEFOLD, a mount-time + * property. Accept it as a no-op so chattr's RMW round-trip + * succeeds; reject any attempt to enable it on a volume that + * was not formatted case-insensitive. + */ + if (test_bit(HFSPLUS_SB_CASEFOLD, &sbi->flags)) + allowed |= FS_CASEFOLD_FL; + /* don't silently ignore unsupported ext2 flags */ - if (fa->flags & ~(FS_IMMUTABLE_FL|FS_APPEND_FL|FS_NODUMP_FL)) + if (fa->flags & ~allowed) return -EOPNOTSUPP; if (fa->flags & FS_IMMUTABLE_FL) From c9da43e4e5c32c2cb318e616ffa48c7148a70d49 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:01 -0400 Subject: [PATCH 12/34] xfs: Report case sensitivity in fileattr_get Upper layers such as NFSD need to query whether a filesystem is case-sensitive. Add FS_XFLAG_CASEFOLD to xfs_ip2xflags() when the filesystem is formatted with the ASCIICI feature flag. This serves both FS_IOC_FSGETXATTR (via xfs_fill_fsxattr() in xfs_fileattr_get()) and XFS_IOC_BULKSTAT (which populates bs_xflags directly from xfs_ip2xflags()), so bulkstat consumers and per-inode queries see a consistent view of the filesystem's case-folding behavior. FS_XFLAG_CASEFOLD is read-only: FS_XFLAG_RDONLY_MASK ensures FS_IOC_FSSETXATTR strips it, and xfs_flags2diflags() has no clause for CASEFOLD so the on-disk diflags are unaffected. The legacy FS_IOC_SETFLAGS path in xfs_fileattr_set() also allows FS_CASEFOLD_FL through its allowlist on ASCIICI filesystems so that a chattr read-modify-write cycle does not fail with EOPNOTSUPP. XFS always preserves case. XFS is case-sensitive by default, but supports ASCII case-insensitive lookups when formatted with the ASCIICI feature flag. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-8-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/xfs/libxfs/xfs_inode_util.c | 2 ++ fs/xfs/xfs_ioctl.c | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/fs/xfs/libxfs/xfs_inode_util.c b/fs/xfs/libxfs/xfs_inode_util.c index 551fa51befb6..82be54b6f8d3 100644 --- a/fs/xfs/libxfs/xfs_inode_util.c +++ b/fs/xfs/libxfs/xfs_inode_util.c @@ -130,6 +130,8 @@ xfs_ip2xflags( if (xfs_inode_has_attr_fork(ip)) flags |= FS_XFLAG_HASATTR; + if (xfs_has_asciici(ip->i_mount)) + flags |= FS_XFLAG_CASEFOLD; return flags; } diff --git a/fs/xfs/xfs_ioctl.c b/fs/xfs/xfs_ioctl.c index ed9b4846c05f..f8216f74679f 100644 --- a/fs/xfs/xfs_ioctl.c +++ b/fs/xfs/xfs_ioctl.c @@ -755,9 +755,23 @@ xfs_fileattr_set( trace_xfs_ioctl_setattr(ip); if (!fa->fsx_valid) { - if (fa->flags & ~(FS_IMMUTABLE_FL | FS_APPEND_FL | - FS_NOATIME_FL | FS_NODUMP_FL | - FS_SYNC_FL | FS_DAX_FL | FS_PROJINHERIT_FL)) + unsigned int allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | + FS_NOATIME_FL | FS_NODUMP_FL | + FS_SYNC_FL | FS_DAX_FL | + FS_PROJINHERIT_FL; + + /* + * FS_CASEFOLD_FL reflects the ASCIICI superblock feature, + * a read-only property. Accept it as a no-op so chattr's + * RMW round-trip succeeds; reject any attempt to enable + * it on a non-ASCIICI filesystem. xfs_flags2diflags() + * has no clause for CASEFOLD, so the bit is dropped from + * the on-disk diflags regardless. + */ + if (xfs_has_asciici(mp)) + allowed |= FS_CASEFOLD_FL; + + if (fa->flags & ~allowed) return -EOPNOTSUPP; } From e50bc12f5a3653c0ab1bbb80b427efd96eb6208d Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:02 -0400 Subject: [PATCH 13/34] cifs: Implement fileattr_get for case sensitivity Upper layers such as NFSD need a way to query whether a filesystem handles filenames in a case-sensitive manner. Report CIFS/SMB case handling behavior via FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING. The authoritative source is the server itself: at mount time CIFS issues QueryFSInfo(FS_ATTRIBUTE_INFORMATION) and caches the reply on the tcon. That reply carries FILE_CASE_SENSITIVE_SEARCH and FILE_CASE_PRESERVED_NAMES, which reflect whatever case handling the share actually implements after SMB3.1.1 POSIX extensions negotiation. Translating those two bits into the VFS flags lets cifs_fileattr_get report what the server advertises rather than what the client was asked to pretend. QueryFSInfo is best-effort; the mount completes even if the server does not answer. MaxPathNameComponentLength is zero in that case and is used as the "no reply received" sentinel. When no reply is available, fall back to the nocase mount option so that the reported behavior agrees with the dentry comparison operations installed on the superblock. The callback is registered on cifs_dir_inode_ops so that NFSD, ksmbd, and other consumers querying case handling against a directory get a definitive answer, and on cifs_file_inode_ops to preserve FS_COMPR_FL reporting on regular files. cifs_set_ops() also installs cifs_namespace_inode_operations on DFS referral directories that carry IS_AUTOMOUNT; register the same callback there so the answer does not depend on whether the directory is a referral point. Registering fileattr_get routes FS_IOC_GETFLAGS through vfs_fileattr_get() and short-circuits the syscall's fallback to cifs_ioctl(). That fallback invoked CIFSGetExtAttr() under CONFIG_CIFS_POSIX and CONFIG_CIFS_ALLOW_INSECURE_LEGACY on servers advertising CIFS_UNIX_EXTATTR_CAP, surfacing the SMB1 Unix-extension immutable, append, and nodump bits. cifs_fileattr_get carries over only FS_COMPR_FL from cached cifsAttrs; the SMB1 extattr fetch is not reproduced. SMB1 is deprecated, and acquiring a netfid from within a dentry-only callback is not worth preserving a path tied to an insecure legacy dialect. Acked-by: Steve French Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-9-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/smb/client/cifsfs.c | 53 +++++++++++++++++++++++++++++++++++++++ fs/smb/client/cifsfs.h | 3 +++ fs/smb/client/namespace.c | 1 + 3 files changed, 57 insertions(+) diff --git a/fs/smb/client/cifsfs.c b/fs/smb/client/cifsfs.c index 9f76b0347fa9..59e54f8bebb2 100644 --- a/fs/smb/client/cifsfs.c +++ b/fs/smb/client/cifsfs.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include "cifsfs.h" @@ -1162,6 +1163,56 @@ struct file_system_type smb3_fs_type = { MODULE_ALIAS_FS("smb3"); MODULE_ALIAS("smb3"); +int cifs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct cifs_sb_info *cifs_sb = CIFS_SB(dentry->d_sb); + struct cifs_tcon *tcon = cifs_sb_master_tcon(cifs_sb); + struct inode *inode = d_inode(dentry); + u32 attrs; + + /* Preserve FS_COMPR_FL previously reported by cifs_ioctl(). */ + if (CIFS_I(inode)->cifsAttrs & ATTR_COMPRESSED) + fa->flags |= FS_COMPR_FL; + + /* + * FS_CASEFOLD_FL is defined by UAPI as a folder attribute, + * and userspace tools (e.g., lsattr) display it only on + * directories. Confine the case-handling bits to directories + * to match that convention; for non-directories the share's + * case semantics are still discoverable through the parent. + */ + if (!S_ISDIR(inode->i_mode)) + return 0; + + /* + * The server's FS_ATTRIBUTE_INFORMATION response, cached on + * the tcon at mount, reflects the share's case-handling + * semantics after any POSIX extensions negotiation. Prefer + * it over the client-local nocase mount option, which only + * governs dentry comparison on this superblock. + * + * QueryFSInfo is best-effort at mount; when it did not + * populate fsAttrInfo, MaxPathNameComponentLength remains + * zero. In that case fall back to nocase so the reporting + * matches the comparison behavior installed on the sb. + */ + if (le32_to_cpu(tcon->fsAttrInfo.MaxPathNameComponentLength) == 0) { + if (tcon->nocase) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + return 0; + } + attrs = le32_to_cpu(tcon->fsAttrInfo.Attributes); + if (!(attrs & FILE_CASE_SENSITIVE_SEARCH)) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + if (!(attrs & FILE_CASE_PRESERVED_NAMES)) + fa->fsx_xflags |= FS_XFLAG_CASENONPRESERVING; + return 0; +} + const struct inode_operations cifs_dir_inode_ops = { .create = cifs_create, .atomic_open = cifs_atomic_open, @@ -1180,6 +1231,7 @@ const struct inode_operations cifs_dir_inode_ops = { .listxattr = cifs_listxattr, .get_acl = cifs_get_acl, .set_acl = cifs_set_acl, + .fileattr_get = cifs_fileattr_get, }; const struct inode_operations cifs_file_inode_ops = { @@ -1190,6 +1242,7 @@ const struct inode_operations cifs_file_inode_ops = { .fiemap = cifs_fiemap, .get_acl = cifs_get_acl, .set_acl = cifs_set_acl, + .fileattr_get = cifs_fileattr_get, }; const char *cifs_get_link(struct dentry *dentry, struct inode *inode, diff --git a/fs/smb/client/cifsfs.h b/fs/smb/client/cifsfs.h index c455b15f2778..9d85224fafab 100644 --- a/fs/smb/client/cifsfs.h +++ b/fs/smb/client/cifsfs.h @@ -89,6 +89,9 @@ extern const struct inode_operations cifs_file_inode_ops; extern const struct inode_operations cifs_symlink_inode_ops; extern const struct inode_operations cifs_namespace_inode_operations; +struct file_kattr; +int cifs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); + /* Functions related to files and directories */ extern const struct netfs_request_ops cifs_req_ops; diff --git a/fs/smb/client/namespace.c b/fs/smb/client/namespace.c index 52a520349cb7..52a51b032fae 100644 --- a/fs/smb/client/namespace.c +++ b/fs/smb/client/namespace.c @@ -294,4 +294,5 @@ struct vfsmount *cifs_d_automount(struct path *path) } const struct inode_operations cifs_namespace_inode_operations = { + .fileattr_get = cifs_fileattr_get, }; From 92d67628a1a91c0585e004ffce8975c7898f9ed1 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:03 -0400 Subject: [PATCH 14/34] nfs: Implement fileattr_get for case sensitivity An NFS server re-exporting an NFS mount point needs to report the case sensitivity behavior of the underlying filesystem to its clients. NFSD's attribute encoder obtains that information by calling vfs_fileattr_get() on the lower filesystem, so the NFS client must implement fileattr_get to surface what it learned from its own server. The NFS client already retrieves case sensitivity information from servers during mount via PATHCONF (NFSv3) or the FATTR4_CASE_INSENSITIVE/FATTR4_CASE_PRESERVING attributes (NFSv4). Expose this information through fileattr_get by reporting the FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING flags. NFSv2 lacks PATHCONF support, so mounts using that protocol version default to standard POSIX behavior: case-sensitive and case-preserving. PATHCONF is now invoked unconditionally for NFSv2 and NFSv3 mounts so the case-sensitivity capabilities are established even when the user pins server->namelen with the namlen= mount option. That option is orthogonal to case handling, and skipping PATHCONF because namelen was already known would leave the caps unset. The two capability bits carry opposite polarity because their POSIX defaults differ. Most servers are case-sensitive and case- preserving, matching "neither xflag set." NFS_CAP_CASE_INSENSITIVE is set only when the server affirms case insensitivity, so "server said no" and "server did not answer" both collapse to the case- sensitive default. NFS_CAP_CASE_NONPRESERVING follows the same pattern in the opposite direction: set only when the server affirms that it does not preserve case, so that silence or a missing attribute lands on the case-preserving default. The NFSv4 probe checks res.attr_bitmask[0] to distinguish "server said false" from "server omitted the attribute" before setting the bit. Both capability bits are cleared before each probe so a remount, an NFSv4 transparent state migration to a server with different case semantics, or a probe whose reply does not arrive does not retain stale capabilities from the prior probe. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-10-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/nfs/client.c | 21 +++++++++++++++------ fs/nfs/inode.c | 15 +++++++++++++++ fs/nfs/internal.h | 3 +++ fs/nfs/namespace.c | 2 ++ fs/nfs/nfs3proc.c | 2 ++ fs/nfs/nfs3xdr.c | 7 +++++-- fs/nfs/nfs4proc.c | 10 +++++++--- fs/nfs/proc.c | 3 +++ fs/nfs/symlink.c | 3 +++ include/linux/nfs_fs_sb.h | 2 +- include/linux/nfs_xdr.h | 2 ++ 11 files changed, 58 insertions(+), 12 deletions(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index be02bb227741..3db2f18315b8 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -914,6 +914,7 @@ static void nfs_server_set_fsinfo(struct nfs_server *server, */ static int nfs_probe_fsinfo(struct nfs_server *server, struct nfs_fh *mntfh, struct nfs_fattr *fattr) { + struct nfs_pathconf pathinfo = { }; struct nfs_fsinfo fsinfo; struct nfs_client *clp = server->nfs_client; int error; @@ -933,15 +934,23 @@ static int nfs_probe_fsinfo(struct nfs_server *server, struct nfs_fh *mntfh, str nfs_server_set_fsinfo(server, &fsinfo); - /* Get some general file system info */ - if (server->namelen == 0) { - struct nfs_pathconf pathinfo; + pathinfo.fattr = fattr; + nfs_fattr_init(fattr); - pathinfo.fattr = fattr; - nfs_fattr_init(fattr); + /* Clear before probing so a failed RPC does not retain stale bits. */ + if (clp->rpc_ops->version < 4) + server->caps &= ~(NFS_CAP_CASE_INSENSITIVE | + NFS_CAP_CASE_NONPRESERVING); - if (clp->rpc_ops->pathconf(server, mntfh, &pathinfo) >= 0) + if (clp->rpc_ops->pathconf(server, mntfh, &pathinfo) >= 0) { + if (server->namelen == 0) server->namelen = pathinfo.max_namelen; + if (clp->rpc_ops->version < 4) { + if (pathinfo.case_insensitive) + server->caps |= NFS_CAP_CASE_INSENSITIVE; + if (!pathinfo.case_preserving) + server->caps |= NFS_CAP_CASE_NONPRESERVING; + } } if (clp->rpc_ops->discover_trunking != NULL && diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c index e26030e73696..170d32c217ae 100644 --- a/fs/nfs/inode.c +++ b/fs/nfs/inode.c @@ -41,6 +41,7 @@ #include #include #include +#include #include "nfs4_fs.h" #include "callback.h" @@ -1095,6 +1096,20 @@ int nfs_getattr(struct mnt_idmap *idmap, const struct path *path, } EXPORT_SYMBOL_GPL(nfs_getattr); +int nfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct inode *inode = d_inode(dentry); + + if (nfs_server_capable(inode, NFS_CAP_CASE_INSENSITIVE)) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + if (nfs_server_capable(inode, NFS_CAP_CASE_NONPRESERVING)) + fa->fsx_xflags |= FS_XFLAG_CASENONPRESERVING; + return 0; +} +EXPORT_SYMBOL_GPL(nfs_fileattr_get); + static void nfs_init_lock_context(struct nfs_lock_context *l_ctx) { refcount_set(&l_ctx->count, 1); diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h index 18d46b0e71dd..ec2b3d984398 100644 --- a/fs/nfs/internal.h +++ b/fs/nfs/internal.h @@ -451,6 +451,9 @@ extern void nfs_set_cache_invalid(struct inode *inode, unsigned long flags); extern bool nfs_check_cache_invalid(struct inode *, unsigned long); extern int nfs_wait_bit_killable(struct wait_bit_key *key, int mode); +struct file_kattr; +int nfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); + #if IS_ENABLED(CONFIG_NFS_LOCALIO) /* localio.c */ struct nfs_local_dio { diff --git a/fs/nfs/namespace.c b/fs/nfs/namespace.c index af9be0c5f516..6d0073c24771 100644 --- a/fs/nfs/namespace.c +++ b/fs/nfs/namespace.c @@ -246,11 +246,13 @@ nfs_namespace_setattr(struct mnt_idmap *idmap, struct dentry *dentry, const struct inode_operations nfs_mountpoint_inode_operations = { .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, }; const struct inode_operations nfs_referral_inode_operations = { .getattr = nfs_namespace_getattr, .setattr = nfs_namespace_setattr, + .fileattr_get = nfs_fileattr_get, }; static void nfs_expire_automounts(struct work_struct *work) diff --git a/fs/nfs/nfs3proc.c b/fs/nfs/nfs3proc.c index 95d7cd564b74..b80d0c5efc27 100644 --- a/fs/nfs/nfs3proc.c +++ b/fs/nfs/nfs3proc.c @@ -1053,6 +1053,7 @@ static const struct inode_operations nfs3_dir_inode_operations = { .permission = nfs_permission, .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, #ifdef CONFIG_NFS_V3_ACL .listxattr = nfs3_listxattr, .get_inode_acl = nfs3_get_acl, @@ -1064,6 +1065,7 @@ static const struct inode_operations nfs3_file_inode_operations = { .permission = nfs_permission, .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, #ifdef CONFIG_NFS_V3_ACL .listxattr = nfs3_listxattr, .get_inode_acl = nfs3_get_acl, diff --git a/fs/nfs/nfs3xdr.c b/fs/nfs/nfs3xdr.c index e17d72908412..e745e78faab0 100644 --- a/fs/nfs/nfs3xdr.c +++ b/fs/nfs/nfs3xdr.c @@ -2276,8 +2276,11 @@ static int decode_pathconf3resok(struct xdr_stream *xdr, if (unlikely(!p)) return -EIO; result->max_link = be32_to_cpup(p++); - result->max_namelen = be32_to_cpup(p); - /* ignore remaining fields */ + result->max_namelen = be32_to_cpup(p++); + p++; /* ignore no_trunc */ + p++; /* ignore chown_restricted */ + result->case_insensitive = be32_to_cpup(p++) != 0; + result->case_preserving = be32_to_cpup(p) != 0; return 0; } diff --git a/fs/nfs/nfs4proc.c b/fs/nfs/nfs4proc.c index a9b8d482d289..0715a6745d1f 100644 --- a/fs/nfs/nfs4proc.c +++ b/fs/nfs/nfs4proc.c @@ -3933,7 +3933,8 @@ static int _nfs4_server_capabilities(struct nfs_server *server, struct nfs_fh *f server->caps &= ~(NFS_CAP_ACLS | NFS_CAP_HARDLINKS | NFS_CAP_SYMLINKS | NFS_CAP_SECURITY_LABEL | NFS_CAP_FS_LOCATIONS | - NFS_CAP_OPEN_XOR | NFS_CAP_DELEGTIME); + NFS_CAP_OPEN_XOR | NFS_CAP_DELEGTIME | + NFS_CAP_CASE_INSENSITIVE | NFS_CAP_CASE_NONPRESERVING); server->fattr_valid = NFS_ATTR_FATTR_V4; if (res.attr_bitmask[0] & FATTR4_WORD0_ACL && res.acl_bitmask & ACL4_SUPPORT_ALLOW_ACL) @@ -3944,8 +3945,9 @@ static int _nfs4_server_capabilities(struct nfs_server *server, struct nfs_fh *f server->caps |= NFS_CAP_SYMLINKS; if (res.case_insensitive) server->caps |= NFS_CAP_CASE_INSENSITIVE; - if (res.case_preserving) - server->caps |= NFS_CAP_CASE_PRESERVING; + if ((res.attr_bitmask[0] & FATTR4_WORD0_CASE_PRESERVING) && + !res.case_preserving) + server->caps |= NFS_CAP_CASE_NONPRESERVING; #ifdef CONFIG_NFS_V4_SECURITY_LABEL if (res.attr_bitmask[2] & FATTR4_WORD2_SECURITY_LABEL) server->caps |= NFS_CAP_SECURITY_LABEL; @@ -10617,6 +10619,7 @@ static const struct inode_operations nfs4_dir_inode_operations = { .getattr = nfs_getattr, .setattr = nfs_setattr, .listxattr = nfs4_listxattr, + .fileattr_get = nfs_fileattr_get, }; static const struct inode_operations nfs4_file_inode_operations = { @@ -10624,6 +10627,7 @@ static const struct inode_operations nfs4_file_inode_operations = { .getattr = nfs_getattr, .setattr = nfs_setattr, .listxattr = nfs4_listxattr, + .fileattr_get = nfs_fileattr_get, }; static struct nfs_server *nfs4_clone_server(struct nfs_server *source, diff --git a/fs/nfs/proc.c b/fs/nfs/proc.c index 70795684b8e8..03c2c1f31be9 100644 --- a/fs/nfs/proc.c +++ b/fs/nfs/proc.c @@ -598,6 +598,7 @@ nfs_proc_pathconf(struct nfs_server *server, struct nfs_fh *fhandle, { info->max_link = 0; info->max_namelen = NFS2_MAXNAMLEN; + info->case_preserving = true; return 0; } @@ -718,12 +719,14 @@ static const struct inode_operations nfs_dir_inode_operations = { .permission = nfs_permission, .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, }; static const struct inode_operations nfs_file_inode_operations = { .permission = nfs_permission, .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, }; const struct nfs_rpc_ops nfs_v2_clientops = { diff --git a/fs/nfs/symlink.c b/fs/nfs/symlink.c index 58146e935402..74a072896f8d 100644 --- a/fs/nfs/symlink.c +++ b/fs/nfs/symlink.c @@ -22,6 +22,8 @@ #include #include +#include "internal.h" + /* Symlink caching in the page cache is even more simplistic * and straight-forward than readdir caching. */ @@ -74,4 +76,5 @@ const struct inode_operations nfs_symlink_inode_operations = { .get_link = nfs_get_link, .getattr = nfs_getattr, .setattr = nfs_setattr, + .fileattr_get = nfs_fileattr_get, }; diff --git a/include/linux/nfs_fs_sb.h b/include/linux/nfs_fs_sb.h index 4daee27fa5eb..34d294774f8c 100644 --- a/include/linux/nfs_fs_sb.h +++ b/include/linux/nfs_fs_sb.h @@ -306,7 +306,7 @@ struct nfs_server { #define NFS_CAP_ATOMIC_OPEN (1U << 4) #define NFS_CAP_LGOPEN (1U << 5) #define NFS_CAP_CASE_INSENSITIVE (1U << 6) -#define NFS_CAP_CASE_PRESERVING (1U << 7) +#define NFS_CAP_CASE_NONPRESERVING (1U << 7) #define NFS_CAP_REBOOT_LAYOUTRETURN (1U << 8) #define NFS_CAP_OFFLOAD_STATUS (1U << 9) #define NFS_CAP_ZERO_RANGE (1U << 10) diff --git a/include/linux/nfs_xdr.h b/include/linux/nfs_xdr.h index fcbd21b5685f..83ee991cde2b 100644 --- a/include/linux/nfs_xdr.h +++ b/include/linux/nfs_xdr.h @@ -182,6 +182,8 @@ struct nfs_pathconf { struct nfs_fattr *fattr; /* Post-op attributes */ __u32 max_link; /* max # of hard links */ __u32 max_namelen; /* max name length */ + bool case_insensitive; + bool case_preserving; }; struct nfs4_change_info { From ef14aa143f1dd8adcba6c9277c3bbed2fe0969b4 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:04 -0400 Subject: [PATCH 15/34] vboxsf: Implement fileattr_get for case sensitivity Upper layers such as NFSD need a way to query whether a filesystem handles filenames in a case-sensitive manner. Report VirtualBox shared folder case handling behavior via the FS_XFLAG_CASEFOLD flag. The case sensitivity property is queried from the VirtualBox host service at mount time and cached in struct vboxsf_sbi. The host determines case sensitivity based on the underlying host filesystem (for example, Windows NTFS is case-insensitive while Linux ext4 is case-sensitive). VirtualBox shared folders always preserve filename case exactly as provided by the guest. The host interface does not expose a separate case-preserving property; leaving FS_XFLAG_CASENONPRESERVING unset reports the POSIX-default case-preserving behavior, which matches vboxsf semantics. The callback is registered in all three inode_operations structures (directory, file, and symlink) to ensure consistent reporting across all inode types. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-11-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/vboxsf/dir.c | 1 + fs/vboxsf/file.c | 6 ++++-- fs/vboxsf/super.c | 7 +++++++ fs/vboxsf/utils.c | 30 ++++++++++++++++++++++++++++++ fs/vboxsf/vfsmod.h | 6 ++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/fs/vboxsf/dir.c b/fs/vboxsf/dir.c index 42bedc4ec7af..c5bd3271aa96 100644 --- a/fs/vboxsf/dir.c +++ b/fs/vboxsf/dir.c @@ -477,4 +477,5 @@ const struct inode_operations vboxsf_dir_iops = { .symlink = vboxsf_dir_symlink, .getattr = vboxsf_getattr, .setattr = vboxsf_setattr, + .fileattr_get = vboxsf_fileattr_get, }; diff --git a/fs/vboxsf/file.c b/fs/vboxsf/file.c index 7a7a3fbb2651..943953867e18 100644 --- a/fs/vboxsf/file.c +++ b/fs/vboxsf/file.c @@ -222,7 +222,8 @@ const struct file_operations vboxsf_reg_fops = { const struct inode_operations vboxsf_reg_iops = { .getattr = vboxsf_getattr, - .setattr = vboxsf_setattr + .setattr = vboxsf_setattr, + .fileattr_get = vboxsf_fileattr_get, }; static int vboxsf_read_folio(struct file *file, struct folio *folio) @@ -389,5 +390,6 @@ static const char *vboxsf_get_link(struct dentry *dentry, struct inode *inode, } const struct inode_operations vboxsf_lnk_iops = { - .get_link = vboxsf_get_link + .get_link = vboxsf_get_link, + .fileattr_get = vboxsf_fileattr_get, }; diff --git a/fs/vboxsf/super.c b/fs/vboxsf/super.c index a618cb093e00..a61fbab51d37 100644 --- a/fs/vboxsf/super.c +++ b/fs/vboxsf/super.c @@ -185,6 +185,13 @@ static int vboxsf_fill_super(struct super_block *sb, struct fs_context *fc) if (err) goto fail_unmap; + /* + * A failed query leaves sbi->case_insensitive false, so the + * mount defaults to reporting case-sensitive behavior. Do not + * fail the mount over an advisory attribute. + */ + vboxsf_query_case_sensitive(sbi); + sb->s_magic = VBOXSF_SUPER_MAGIC; sb->s_blocksize = 1024; sb->s_maxbytes = MAX_LFS_FILESIZE; diff --git a/fs/vboxsf/utils.c b/fs/vboxsf/utils.c index 440e8c50629d..298bfc93255c 100644 --- a/fs/vboxsf/utils.c +++ b/fs/vboxsf/utils.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "vfsmod.h" struct inode *vboxsf_new_inode(struct super_block *sb) @@ -567,3 +568,32 @@ int vboxsf_dir_read_all(struct vboxsf_sbi *sbi, struct vboxsf_dir_info *sf_d, return err; } + +int vboxsf_query_case_sensitive(struct vboxsf_sbi *sbi) +{ + struct shfl_volinfo volinfo = {}; + u32 buf_len; + int err; + + buf_len = sizeof(volinfo); + err = vboxsf_fsinfo(sbi->root, 0, SHFL_INFO_GET | SHFL_INFO_VOLUME, + &buf_len, &volinfo); + if (err) + return err; + if (buf_len < sizeof(volinfo)) + return 0; + + sbi->case_insensitive = !volinfo.properties.case_sensitive; + return 0; +} + +int vboxsf_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct vboxsf_sbi *sbi = VBOXSF_SBI(dentry->d_sb); + + if (sbi->case_insensitive) { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + return 0; +} diff --git a/fs/vboxsf/vfsmod.h b/fs/vboxsf/vfsmod.h index 05973eb89d52..b61afd0ce842 100644 --- a/fs/vboxsf/vfsmod.h +++ b/fs/vboxsf/vfsmod.h @@ -47,6 +47,7 @@ struct vboxsf_sbi { u32 next_generation; u32 root; int bdi_id; + bool case_insensitive; }; /* per-inode information */ @@ -111,6 +112,11 @@ void vboxsf_dir_info_free(struct vboxsf_dir_info *p); int vboxsf_dir_read_all(struct vboxsf_sbi *sbi, struct vboxsf_dir_info *sf_d, u64 handle); +int vboxsf_query_case_sensitive(struct vboxsf_sbi *sbi); + +struct file_kattr; +int vboxsf_fileattr_get(struct dentry *dentry, struct file_kattr *fa); + /* from vboxsf_wrappers.c */ int vboxsf_connect(void); void vboxsf_disconnect(void); From 7bbd51b1d7488fb4586ee7d67dc19f103313a8ba Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:05 -0400 Subject: [PATCH 16/34] isofs: Implement fileattr_get for case sensitivity Upper layers such as NFSD need a way to query whether a filesystem handles filenames in a case-sensitive manner so they can provide correct semantics to remote clients. Without this information, NFS exports of ISO 9660 filesystems cannot advertise their filename case behavior. Implement isofs_fileattr_get() to report ISO 9660 case handling behavior. The 'check=r' (relaxed) mount option enables case-insensitive lookups and is reported via FS_XFLAG_CASEFOLD. By default, Joliet extensions operate in relaxed mode while plain ISO 9660 uses strict (case-sensitive) mode. Plain ISO 9660 names on the medium are uppercase. When neither Rock Ridge nor Joliet is in effect, the default 'map=n' option (and 'map=a') routes lookup and readdir through isofs_name_translate(), which forces A-Z to a-z. The names visible to userspace then differ in case from the on-disc form, so report FS_XFLAG_CASENONPRESERVING in that configuration. Rock Ridge and Joliet both deliver names as authored, and 'map=o' emits the raw on-disc name unchanged, so those configurations remain case-preserving. Casefolding is a directory property, and the in-tree consumers (NFSD, ksmbd) issue the query against a directory: NFSD walks to the parent for non-directory dentries before calling vfs_fileattr_get(), and ksmbd reports per-share attributes from the share root. Wire .fileattr_get only on isofs_dir_inode_operations. The CASEFOLD flag is set in both fa->fsx_xflags and fa->flags so FS_IOC_FSGETXATTR and FS_IOC_GETFLAGS agree. Reviewed-by: Jan Kara Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-12-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/isofs/dir.c | 16 ++++++++++++++++ fs/isofs/isofs.h | 3 +++ 2 files changed, 19 insertions(+) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index 2fd9948d606e..55385a72a4ce 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -14,6 +14,7 @@ #include #include #include "isofs.h" +#include int isofs_name_translate(struct iso_directory_record *de, char *new, struct inode *inode) { @@ -267,6 +268,20 @@ static int isofs_readdir(struct file *file, struct dir_context *ctx) return result; } +int isofs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) +{ + struct isofs_sb_info *sbi = ISOFS_SB(dentry->d_sb); + + if (sbi->s_check == 'r') { + fa->fsx_xflags |= FS_XFLAG_CASEFOLD; + fa->flags |= FS_CASEFOLD_FL; + } + if (!sbi->s_joliet_level && !sbi->s_rock && + (sbi->s_mapping == 'n' || sbi->s_mapping == 'a')) + fa->fsx_xflags |= FS_XFLAG_CASENONPRESERVING; + return 0; +} + const struct file_operations isofs_dir_operations = { .llseek = generic_file_llseek, @@ -281,6 +296,7 @@ const struct file_operations isofs_dir_operations = const struct inode_operations isofs_dir_inode_operations = { .lookup = isofs_lookup, + .fileattr_get = isofs_fileattr_get, }; diff --git a/fs/isofs/isofs.h b/fs/isofs/isofs.h index 506555837533..0ec8b24a42ed 100644 --- a/fs/isofs/isofs.h +++ b/fs/isofs/isofs.h @@ -197,6 +197,9 @@ isofs_normalize_block_and_offset(struct iso_directory_record* de, } } +struct file_kattr; +int isofs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); + extern const struct inode_operations isofs_dir_inode_operations; extern const struct file_operations isofs_dir_operations; extern const struct address_space_operations isofs_symlink_aops; From 211cb2ba487706a55c1bb4e572a89d7e7835930a Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:06 -0400 Subject: [PATCH 17/34] nfsd: Report export case-folding via NFSv3 PATHCONF The hard-coded MSDOS_SUPER_MAGIC check in nfsd3_proc_pathconf() only recognizes FAT filesystems as case-insensitive. Modern filesystems like F2FS, exFAT, and CIFS support case-insensitive directories, but NFSv3 clients cannot discover this capability. Query the export's actual case behavior through ->fileattr_get instead. This allows NFSv3 clients to correctly handle case sensitivity for any filesystem that implements the fileattr interface. Filesystems without ->fileattr_get continue to report the default POSIX behavior (case-sensitive, case-preserving). This change depends on the earlier "fat: Implement fileattr_get for case sensitivity" patch in this series, which ensures FAT filesystems report their case behavior correctly via the fileattr interface. Case-folding is a per-directory property, so nfsd_get_case_info() queries the parent dentry for non-directory filehandles. Three inherent corner cases follow: a single-file export's parent lies outside the exported subtree, so the LSM hook evaluates against an unexported directory; a disconnected dentry from fh_verify() has d_parent == itself, so the file's own attributes are reported until the dentry connects; and a hardlinked file resolves through the alias the dcache currently holds, so when the inode is linked into both case-folded and case-sensitive directories the reported value tracks whichever parent is active. These limitations are not addressable without redefining the protocol attribute as per-parent rather than per-object. RFC 1813 restricts PATHCONF errors to NFS3ERR_STALE, NFS3ERR_BADHANDLE, and NFS3ERR_SERVERFAULT. When an LSM hook denies the case-folding query on the parent, NFS3ERR_STALE is the only correct mapping: NFS3ERR_SERVERFAULT misrepresents a working server as broken, and NFS3ERR_BADHANDLE implies a decoding failure that did not occur. A client purging the filehandle on receipt is the desired outcome, since the server has refused to read attributes through it. Substituting POSIX defaults instead would let the same handle report casefold=false now and casefold=true once policy permits, opening a silent name-collision window on case-insensitive exports. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-13-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/nfsd/nfs3proc.c | 36 ++++++++++++++----- fs/nfsd/vfs.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++ fs/nfsd/vfs.h | 3 ++ fs/nfsd/xdr3.h | 4 +-- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/fs/nfsd/nfs3proc.c b/fs/nfsd/nfs3proc.c index 42adc5461db0..12b9172c6be1 100644 --- a/fs/nfsd/nfs3proc.c +++ b/fs/nfsd/nfs3proc.c @@ -710,23 +710,43 @@ nfsd3_proc_pathconf(struct svc_rqst *rqstp) resp->p_name_max = 255; /* at least */ resp->p_no_trunc = 0; resp->p_chown_restricted = 1; - resp->p_case_insensitive = 0; - resp->p_case_preserving = 1; + resp->p_case_insensitive = false; + resp->p_case_preserving = true; resp->status = fh_verify(rqstp, &argp->fh, 0, NFSD_MAY_NOP); if (resp->status == nfs_ok) { struct super_block *sb = argp->fh.fh_dentry->d_sb; + int err; - /* Note that we don't care for remote fs's here */ - switch (sb->s_magic) { - case EXT2_SUPER_MAGIC: + if (sb->s_magic == EXT2_SUPER_MAGIC) { resp->p_link_max = EXT2_LINK_MAX; resp->p_name_max = EXT2_NAME_LEN; + } + + err = nfsd_get_case_info(argp->fh.fh_dentry, + &resp->p_case_insensitive, + &resp->p_case_preserving); + /* + * RFC 1813 lists NFS3ERR_STALE, NFS3ERR_BADHANDLE, and + * NFS3ERR_SERVERFAULT as the only PATHCONF errors. + */ + switch (err) { + case 0: + case -EOPNOTSUPP: + /* Both arms leave the output booleans valid. */ break; - case MSDOS_SUPER_MAGIC: - resp->p_case_insensitive = 1; - resp->p_case_preserving = 0; + case -EACCES: + case -EPERM: + /* + * Policy denied the query. Report STALE so the + * handle is unusable without implying a server + * malfunction. + */ + resp->status = nfserr_stale; + break; + default: + resp->status = nfserr_serverfault; break; } } diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c index eafdf7b7890f..85ff418127c7 100644 --- a/fs/nfsd/vfs.c +++ b/fs/nfsd/vfs.c @@ -32,6 +32,7 @@ #include #include #include +#include #include "xdr3.h" @@ -2891,3 +2892,90 @@ nfsd_permission(struct svc_cred *cred, struct svc_export *exp, return err? nfserrno(err) : 0; } + +/** + * nfsd_get_case_info - get case sensitivity info for a dentry + * @dentry: dentry to query + * @case_insensitive: set to true if name comparison ignores case + * @case_preserving: set to true if case is preserved on disk + * + * On casefold-capable filesystems the flag lives on the directory, + * not on its entries, so for a non-directory @dentry the parent is + * queried instead. A directory (including an export root, whose + * parent lies outside the export) is queried as-is so its own + * contents' lookup behavior is reported. NFSD advertises + * fattr4_homogeneous as FALSE, so per-directory answers may differ + * within an export. + * + * The probe runs with kernel credentials. case_insensitive and + * case_preserving describe the directory's structural lookup + * behavior, not the caller's identity; running under the calling + * client's mapped credentials would let per-client MAC policy on + * the parent directory turn this query into NFS4ERR_ACCESS even + * though the underlying property is the same for every client. + * + * When the filesystem does not expose case-folding state (no + * ->fileattr_get, or the callback returns -EOPNOTSUPP / + * -ENOIOCTLCMD / -ENOTTY / -EINVAL), the outputs are filled with + * POSIX defaults (case-sensitive, case-preserving) on the premise + * that a filesystem with case-folding support wires up + * fileattr_get. + * + * Return: 0 with outputs filled, -EOPNOTSUPP with outputs filled + * to POSIX defaults, or a negative errno (e.g., -EIO, + * -ESTALE, -ENOMEM) with outputs unmodified. + */ +int +nfsd_get_case_info(struct dentry *dentry, bool *case_insensitive, + bool *case_preserving) +{ + struct file_kattr fa = {}; + const struct cred *saved; + struct cred *probe; + struct dentry *cd; + bool put = false; + int err; + + if (d_is_dir(dentry)) { + cd = dentry; + } else { + cd = dget_parent(dentry); + put = true; + } + + probe = prepare_creds(); + if (!probe) { + err = -ENOMEM; + goto out; + } + probe->fsuid = GLOBAL_ROOT_UID; + probe->fsgid = GLOBAL_ROOT_GID; + saved = override_creds(probe); + + err = vfs_fileattr_get(cd, &fa); + + put_cred(revert_creds(saved)); +out: + if (put) + dput(cd); + switch (err) { + case 0: + *case_insensitive = fa.fsx_xflags & FS_XFLAG_CASEFOLD; + *case_preserving = + !(fa.fsx_xflags & FS_XFLAG_CASENONPRESERVING); + return 0; + case -EINVAL: + case -ENOTTY: + case -ENOIOCTLCMD: + case -EOPNOTSUPP: + /* + * Filesystem does not expose case state. + * Report POSIX defaults. + */ + *case_insensitive = false; + *case_preserving = true; + return -EOPNOTSUPP; + default: + return err; + } +} diff --git a/fs/nfsd/vfs.h b/fs/nfsd/vfs.h index 702a844f2106..e09ea04a51b9 100644 --- a/fs/nfsd/vfs.h +++ b/fs/nfsd/vfs.h @@ -156,6 +156,9 @@ __be32 nfsd_readdir(struct svc_rqst *, struct svc_fh *, loff_t *, struct readdir_cd *, nfsd_filldir_t); __be32 nfsd_statfs(struct svc_rqst *, struct svc_fh *, struct kstatfs *, int access); +int nfsd_get_case_info(struct dentry *dentry, + bool *case_insensitive, + bool *case_preserving); __be32 nfsd_permission(struct svc_cred *cred, struct svc_export *exp, struct dentry *dentry, int acc); diff --git a/fs/nfsd/xdr3.h b/fs/nfsd/xdr3.h index 522067b7fd75..a7c9714b0b0e 100644 --- a/fs/nfsd/xdr3.h +++ b/fs/nfsd/xdr3.h @@ -209,8 +209,8 @@ struct nfsd3_pathconfres { __u32 p_name_max; __u32 p_no_trunc; __u32 p_chown_restricted; - __u32 p_case_insensitive; - __u32 p_case_preserving; + bool p_case_insensitive; + bool p_case_preserving; }; struct nfsd3_commitres { From 01ee7c3d2e23b41cc3f285e69b474f4e0890cce9 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:07 -0400 Subject: [PATCH 18/34] nfsd: Implement NFSv4 FATTR4_CASE_INSENSITIVE and FATTR4_CASE_PRESERVING NFSD currently provides NFSv4 clients with hard-coded responses indicating all exported filesystems are case-sensitive and case-preserving. This is incorrect for case-insensitive filesystems and ext4 directories with casefold enabled. Query the underlying filesystem's actual case sensitivity via nfsd_get_case_info() and return accurate values to clients. This supports per-directory settings for filesystems that allow mixing case-sensitive and case-insensitive directories within an export. The helper queries the parent dentry for non-directory filehandles because case-folding is a per-directory property. That resolution has the same corner cases here as for NFSv3 PATHCONF: single-file exports query an unexported parent, disconnected dentries report defaults until reconnected, and hardlinked files track whichever alias the dcache currently holds. Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-14-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/nfsd/nfs4xdr.c | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/fs/nfsd/nfs4xdr.c b/fs/nfsd/nfs4xdr.c index 2a0946c630e1..319007b79d49 100644 --- a/fs/nfsd/nfs4xdr.c +++ b/fs/nfsd/nfs4xdr.c @@ -3158,6 +3158,8 @@ struct nfsd4_fattr_args { u32 rdattr_err; bool contextsupport; bool ignore_crossmnt; + bool case_insensitive; + bool case_preserving; }; typedef __be32(*nfsd4_enc_attr)(struct xdr_stream *xdr, @@ -3356,6 +3358,33 @@ static __be32 nfsd4_encode_fattr4_acl(struct xdr_stream *xdr, return nfs_ok; } +static __be32 nfsd4_encode_fattr4_case_insensitive(struct xdr_stream *xdr, + const struct nfsd4_fattr_args *args) +{ + return nfsd4_encode_bool(xdr, args->case_insensitive); +} + +static __be32 nfsd4_encode_fattr4_case_preserving(struct xdr_stream *xdr, + const struct nfsd4_fattr_args *args) +{ + return nfsd4_encode_bool(xdr, args->case_preserving); +} + +static __be32 nfsd4_encode_fattr4_homogeneous(struct xdr_stream *xdr, + const struct nfsd4_fattr_args *args) +{ + /* + * Casefold-capable filesystems (e.g. ext4 or f2fs with the + * casefold feature) attach a Unicode encoding at mount time + * but apply case folding per directory. The per-file-system + * case_insensitive and case_preserving values can therefore + * legitimately differ across objects that share the same fsid. + * Report FATTR4_HOMOGENEOUS = FALSE on such filesystems to + * keep that variation consistent with RFC 8881 Section 5.8.2.16. + */ + return nfsd4_encode_bool(xdr, !sb_has_encoding(args->dentry->d_sb)); +} + static __be32 nfsd4_encode_fattr4_filehandle(struct xdr_stream *xdr, const struct nfsd4_fattr_args *args) { @@ -3748,8 +3777,8 @@ static const nfsd4_enc_attr nfsd4_enc_fattr4_encode_ops[] = { [FATTR4_ACLSUPPORT] = nfsd4_encode_fattr4_aclsupport, [FATTR4_ARCHIVE] = nfsd4_encode_fattr4__noop, [FATTR4_CANSETTIME] = nfsd4_encode_fattr4__true, - [FATTR4_CASE_INSENSITIVE] = nfsd4_encode_fattr4__false, - [FATTR4_CASE_PRESERVING] = nfsd4_encode_fattr4__true, + [FATTR4_CASE_INSENSITIVE] = nfsd4_encode_fattr4_case_insensitive, + [FATTR4_CASE_PRESERVING] = nfsd4_encode_fattr4_case_preserving, [FATTR4_CHOWN_RESTRICTED] = nfsd4_encode_fattr4__true, [FATTR4_FILEHANDLE] = nfsd4_encode_fattr4_filehandle, [FATTR4_FILEID] = nfsd4_encode_fattr4_fileid, @@ -3758,7 +3787,7 @@ static const nfsd4_enc_attr nfsd4_enc_fattr4_encode_ops[] = { [FATTR4_FILES_TOTAL] = nfsd4_encode_fattr4_files_total, [FATTR4_FS_LOCATIONS] = nfsd4_encode_fattr4_fs_locations, [FATTR4_HIDDEN] = nfsd4_encode_fattr4__noop, - [FATTR4_HOMOGENEOUS] = nfsd4_encode_fattr4__true, + [FATTR4_HOMOGENEOUS] = nfsd4_encode_fattr4_homogeneous, [FATTR4_MAXFILESIZE] = nfsd4_encode_fattr4_maxfilesize, [FATTR4_MAXLINK] = nfsd4_encode_fattr4_maxlink, [FATTR4_MAXNAME] = nfsd4_encode_fattr4_maxname, @@ -3968,6 +3997,23 @@ nfsd4_encode_fattr4(struct svc_rqst *rqstp, struct xdr_stream *xdr, args.fhp = tempfh; } else args.fhp = fhp; + if (attrmask[0] & (FATTR4_WORD0_CASE_INSENSITIVE | + FATTR4_WORD0_CASE_PRESERVING)) { + err = nfsd_get_case_info(dentry, &args.case_insensitive, + &args.case_preserving); + /* + * Per RFC 8881 Section 18.7.3, an attribute advertised + * in SUPPORTED_ATTRS must come back with a value or the + * GETATTR must fail. nfsd_get_case_info() fills POSIX + * defaults and returns -EOPNOTSUPP when the underlying + * filesystem does not expose case state; encode those + * defaults so the reply agrees with what SUPPORTED_ATTRS + * advertises. Other errors fail the operation as the + * spec requires. + */ + if (err && err != -EOPNOTSUPP) + goto out_nfserr; + } if (attrmask[0] & FATTR4_WORD0_ACL) { err = nfsd4_get_nfs4_acl(rqstp, dentry, &args.acl); From 0164df1d1de7602f27d359031a780e9caae80d3d Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Thu, 7 May 2026 04:53:08 -0400 Subject: [PATCH 19/34] ksmbd: Report filesystem case sensitivity via FS_ATTRIBUTE_INFORMATION FS_ATTRIBUTE_INFORMATION responses have always reported FILE_CASE_SENSITIVE_SEARCH and FILE_CASE_PRESERVED_NAMES unconditionally. Case-insensitive filesystems like exFAT, and casefolded directories on ext4 or f2fs, have no way to signal their actual semantics to SMB clients. Now that filesystems expose case behavior through ->fileattr_get, query it via vfs_fileattr_get() and translate the FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING flags into the corresponding SMB attributes. Filesystems without ->fileattr_get continue reporting default POSIX behavior (case-sensitive, case-preserving). SMB's FS_ATTRIBUTE_INFORMATION reports per-share attributes from the share root, not per-file. Shares mixing casefold and non-casefold directories report the root directory's behavior. Acked-by: Namjae Jeon Reviewed-by: Roland Mainz Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260507-case-sensitivity-v14-15-e62cc8200435@oracle.com Signed-off-by: Christian Brauner --- fs/smb/server/smb2pdu.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 21825a69c29a..0f96eb878f29 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "glob.h" #include "smbfsctl.h" @@ -5542,16 +5543,33 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, case FS_ATTRIBUTE_INFORMATION: { FILE_SYSTEM_ATTRIBUTE_INFO *info; + struct file_kattr fa = {}; size_t sz; + u32 attrs; + int err; info = (FILE_SYSTEM_ATTRIBUTE_INFO *)rsp->Buffer; - info->Attributes = cpu_to_le32(FILE_SUPPORTS_OBJECT_IDS | - FILE_PERSISTENT_ACLS | - FILE_UNICODE_ON_DISK | - FILE_CASE_PRESERVED_NAMES | - FILE_CASE_SENSITIVE_SEARCH | - FILE_SUPPORTS_BLOCK_REFCOUNTING); + attrs = FILE_SUPPORTS_OBJECT_IDS | + FILE_PERSISTENT_ACLS | + FILE_UNICODE_ON_DISK | + FILE_SUPPORTS_BLOCK_REFCOUNTING; + err = vfs_fileattr_get(path.dentry, &fa); + /* + * -EINVAL, -EOPNOTSUPP: ntfs-3g and other FUSE + * filesystems that lack FS_IOC_FSGETXATTR support. + */ + if (err && err != -ENOIOCTLCMD && err != -ENOTTY && + err != -EINVAL && err != -EOPNOTSUPP) { + path_put(&path); + return err; + } + if (!(fa.fsx_xflags & FS_XFLAG_CASEFOLD)) + attrs |= FILE_CASE_SENSITIVE_SEARCH; + if (!(fa.fsx_xflags & FS_XFLAG_CASENONPRESERVING)) + attrs |= FILE_CASE_PRESERVED_NAMES; + + info->Attributes = cpu_to_le32(attrs); info->Attributes |= cpu_to_le32(server_conf.share_fake_fscaps); if (test_share_config_flag(work->tcon->share_conf, From 2c718fccf37469e30dd8a5c992fc0b63628128fd Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:09 -0400 Subject: [PATCH 20/34] tools headers UAPI: Sync case-sensitivity flags from linux/fs.h The case-sensitivity series adds FS_XFLAG_CASEFOLD and FS_XFLAG_CASENONPRESERVING to include/uapi/linux/fs.h, and tools/perf/check-headers.sh would warn about the resulting drift in the perf beauty copy. Pick up only those two flags (and the surrounding comment block) so the series does not introduce new drift of its own. This is not a full sync. The perf copy is also missing the FS_IOC_SHUTDOWN block added by commit 1f662195dbc0 ("fs: add generic FS_IOC_SHUTDOWN definitions"). Because tools/perf/check-headers.sh emits a single warning per file, that warning will remain active until the older drift is picked up too; closing it is left to a separate sync outside this series. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=2 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-2-cel@kernel.org Signed-off-by: Christian Brauner --- tools/perf/trace/beauty/include/uapi/linux/fs.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/perf/trace/beauty/include/uapi/linux/fs.h b/tools/perf/trace/beauty/include/uapi/linux/fs.h index 70b2b661f42c..2fa003575e8b 100644 --- a/tools/perf/trace/beauty/include/uapi/linux/fs.h +++ b/tools/perf/trace/beauty/include/uapi/linux/fs.h @@ -254,6 +254,13 @@ struct file_attr { #define FS_XFLAG_DAX 0x00008000 /* use DAX for IO */ #define FS_XFLAG_COWEXTSIZE 0x00010000 /* CoW extent size allocator hint */ #define FS_XFLAG_VERITY 0x00020000 /* fs-verity enabled */ +/* + * Case handling flags (read-only, cannot be set via ioctl). + * Default (neither set) indicates POSIX semantics: case-sensitive + * lookups and case-preserving storage. + */ +#define FS_XFLAG_CASEFOLD 0x00040000 /* case-insensitive lookups */ +#define FS_XFLAG_CASENONPRESERVING 0x00080000 /* case not preserved */ #define FS_XFLAG_HASATTR 0x80000000 /* no DIFLAG for this */ /* the read-only stuff doesn't really belong here, but any other place is From c6eb07d3da833c351993ab8582c27073afe295e6 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:10 -0400 Subject: [PATCH 21/34] nfs: Avoid transient zeroed case capability bits during probe nfs_probe_fsinfo() clears NFS_CAP_CASE_INSENSITIVE and NFS_CAP_CASE_NONPRESERVING ahead of the synchronous pathconf RPC and sets them again only after the reply arrives. The code path is gated by clp->rpc_ops->version < 4 and is therefore reached on NFSv2/v3 remount via nfs_reconfigure(), which calls nfs_probe_server() against a live mount. Concurrent readers walking server->caps can observe the cleared state for the duration of the round-trip and report the wrong case-sensitivity attributes. Compute the post-probe capability mask on the stack and assign it to server->caps in a single store so readers see either the stale value or the freshly computed one, never an intermediate zero. Preserve the original behaviour of dropping the bits when the pathconf RPC itself fails. The analogous transient zero on the NFSv4 path lives in nfs4_server_capabilities() and is left for a separate fix. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=10 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-3-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfs/client.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index 3db2f18315b8..28b66bb0dd33 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -937,20 +937,23 @@ static int nfs_probe_fsinfo(struct nfs_server *server, struct nfs_fh *mntfh, str pathinfo.fattr = fattr; nfs_fattr_init(fattr); - /* Clear before probing so a failed RPC does not retain stale bits. */ - if (clp->rpc_ops->version < 4) - server->caps &= ~(NFS_CAP_CASE_INSENSITIVE | - NFS_CAP_CASE_NONPRESERVING); - if (clp->rpc_ops->pathconf(server, mntfh, &pathinfo) >= 0) { if (server->namelen == 0) server->namelen = pathinfo.max_namelen; if (clp->rpc_ops->version < 4) { + unsigned int caps = server->caps; + + caps &= ~(NFS_CAP_CASE_INSENSITIVE | + NFS_CAP_CASE_NONPRESERVING); if (pathinfo.case_insensitive) - server->caps |= NFS_CAP_CASE_INSENSITIVE; + caps |= NFS_CAP_CASE_INSENSITIVE; if (!pathinfo.case_preserving) - server->caps |= NFS_CAP_CASE_NONPRESERVING; + caps |= NFS_CAP_CASE_NONPRESERVING; + server->caps = caps; } + } else if (clp->rpc_ops->version < 4) { + server->caps &= ~(NFS_CAP_CASE_INSENSITIVE | + NFS_CAP_CASE_NONPRESERVING); } if (clp->rpc_ops->discover_trunking != NULL && From 425294293cd11a05e46278c21cc674780e02fef1 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:11 -0400 Subject: [PATCH 22/34] nfs: Skip pathconf probe when neither field is consumed The PATHCONF RPC issued from nfs_probe_fsinfo() supplies two pieces of information: max_namelen, used only when server->namelen has not been pinned by mount options, and the case_insensitive / case_preserving fields, used only by the NFSv2/NFSv3 path. NFSv4 receives its case sensitivity caps from the FATTR4_CASE_* attributes during the set_capabilities probe, and a non-zero server->namelen short-circuits the only other field of interest. When both conditions hold (NFSv4 with namelen pinned), the pathconf reply is discarded in full but the round-trip is still on the mount critical path. Gate the call on version < 4 || namelen == 0 so that mounts which cannot benefit from the reply do not pay for it. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=10 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-4-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfs/client.c | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index 28b66bb0dd33..73b95318ba48 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -937,23 +937,25 @@ static int nfs_probe_fsinfo(struct nfs_server *server, struct nfs_fh *mntfh, str pathinfo.fattr = fattr; nfs_fattr_init(fattr); - if (clp->rpc_ops->pathconf(server, mntfh, &pathinfo) >= 0) { - if (server->namelen == 0) - server->namelen = pathinfo.max_namelen; - if (clp->rpc_ops->version < 4) { - unsigned int caps = server->caps; + if (clp->rpc_ops->version < 4 || server->namelen == 0) { + if (clp->rpc_ops->pathconf(server, mntfh, &pathinfo) >= 0) { + if (server->namelen == 0) + server->namelen = pathinfo.max_namelen; + if (clp->rpc_ops->version < 4) { + unsigned int caps = server->caps; - caps &= ~(NFS_CAP_CASE_INSENSITIVE | - NFS_CAP_CASE_NONPRESERVING); - if (pathinfo.case_insensitive) - caps |= NFS_CAP_CASE_INSENSITIVE; - if (!pathinfo.case_preserving) - caps |= NFS_CAP_CASE_NONPRESERVING; - server->caps = caps; + caps &= ~(NFS_CAP_CASE_INSENSITIVE | + NFS_CAP_CASE_NONPRESERVING); + if (pathinfo.case_insensitive) + caps |= NFS_CAP_CASE_INSENSITIVE; + if (!pathinfo.case_preserving) + caps |= NFS_CAP_CASE_NONPRESERVING; + server->caps = caps; + } + } else if (clp->rpc_ops->version < 4) { + server->caps &= ~(NFS_CAP_CASE_INSENSITIVE | + NFS_CAP_CASE_NONPRESERVING); } - } else if (clp->rpc_ops->version < 4) { - server->caps &= ~(NFS_CAP_CASE_INSENSITIVE | - NFS_CAP_CASE_NONPRESERVING); } if (clp->rpc_ops->discover_trunking != NULL && From 45e57cfb7b10b64fb0d38d671d26ec935fc7efce Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:12 -0400 Subject: [PATCH 23/34] fs: Clarify FS_CASEFOLD_FL semantics in UAPI header The existing one-liner "Folder is case insensitive" leaves the impression that FS_CASEFOLD_FL is reserved for directories. That impression is wrong: filesystems that derive case-insensitivity from mount or volume state report the bit on non-directory inodes via i_op->fileattr_get, so userspace inspecting FS_IOC_GETFLAGS can see it on any inode type. Replace the one-liner with a block comment that names directories as the typical case, records that non-directory inodes may also report the bit, and notes FS_XFLAG_CASEFOLD as the read-only companion exposed through FS_IOC_FSGETXATTR. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=3 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-5-cel@kernel.org Signed-off-by: Christian Brauner --- include/uapi/linux/fs.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/uapi/linux/fs.h b/include/uapi/linux/fs.h index 2ea4c81df08f..bd87262f2e34 100644 --- a/include/uapi/linux/fs.h +++ b/include/uapi/linux/fs.h @@ -395,7 +395,16 @@ struct file_attr { #define FS_DAX_FL 0x02000000 /* Inode is DAX */ #define FS_INLINE_DATA_FL 0x10000000 /* Reserved for ext4 */ #define FS_PROJINHERIT_FL 0x20000000 /* Create with parents projid */ -#define FS_CASEFOLD_FL 0x40000000 /* Folder is case insensitive */ +/* + * FS_CASEFOLD_FL indicates case-insensitive name lookup. The + * bit is most often reported on directories, where it controls + * lookups of entries within. Filesystems that derive + * case-insensitivity from mount or volume state may also report + * it on non-directory inodes; userspace must not assume the bit + * is directory-only. FS_XFLAG_CASEFOLD reports the same + * information read-only via FS_IOC_FSGETXATTR. + */ +#define FS_CASEFOLD_FL 0x40000000 #define FS_RESERVED_FL 0x80000000 /* reserved for ext2 lib */ #define FS_FL_USER_VISIBLE 0x0003DFFF /* User visible flags */ From 3aa75b92e862490b8744b659a5a906f1dc6cb951 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:13 -0400 Subject: [PATCH 24/34] nfsd: Use kernel credentials for case-info probe nfsd_get_case_info() takes prepare_creds() and overrides fsuid/fsgid to GLOBAL_ROOT, intending to escape per-client policy on the parent directory. prepare_creds() copies the calling task's full credential, including the LSM security label, so only the DAC identity is neutralized. With labeled NFS, where the active LSM context has been mapped to the client, security_inode_file_getattr() can still deny the probe with -EACCES even though the case-folding property the caller wants is structural and identical for every client. The docblock already states the intent ("the probe runs with kernel credentials"), which the implementation does not deliver. prepare_kernel_cred(&init_task) constructs a credential from init_task's identity and security label, the kernel's own unconfined context. Use it instead and drop the redundant fsuid/fsgid overrides that init_task already supplies. The probe now matches the docblock, LSM denials on the parent disappear, and the call sites that map an unexpected error to NFS3ERR_SERVERFAULT or fail an NFSv4 GETATTR outright stop seeing -EACCES from this path. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=14 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-6-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/vfs.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c index 85ff418127c7..ba97e287c007 100644 --- a/fs/nfsd/vfs.c +++ b/fs/nfsd/vfs.c @@ -2943,13 +2943,11 @@ nfsd_get_case_info(struct dentry *dentry, bool *case_insensitive, put = true; } - probe = prepare_creds(); + probe = prepare_kernel_cred(&init_task); if (!probe) { err = -ENOMEM; goto out; } - probe->fsuid = GLOBAL_ROOT_UID; - probe->fsgid = GLOBAL_ROOT_GID; saved = override_creds(probe); err = vfs_fileattr_get(cd, &fa); From fbaee2a5c406f274d72ed8f98bf9140ae43c5972 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:14 -0400 Subject: [PATCH 25/34] nfsd: Map -ESTALE from case probe to NFS3ERR_STALE The PATHCONF switch in nfsd3_proc_pathconf() recognizes -EOPNOTSUPP (filesystem does not expose case state) and maps -EACCES / -EPERM to nfserr_stale, but lets every other errno fall through to nfserr_serverfault. -ESTALE escapes the same way even though RFC 1813 lists NFS3ERR_STALE as a permitted PATHCONF status, so a probe of an NFS-backed re-export whose parent dentry has been invalidated returns SERVERFAULT and tells the client the server is broken when the handle itself simply went stale. Add an explicit -ESTALE arm that maps to nfserr_stale. Fixes: a8de9c3b40e4 ("nfsd: Report export case-folding via NFSv3 PATHCONF") Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=13 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-7-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/nfs3proc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/nfsd/nfs3proc.c b/fs/nfsd/nfs3proc.c index 12b9172c6be1..aeda7a802bdf 100644 --- a/fs/nfsd/nfs3proc.c +++ b/fs/nfsd/nfs3proc.c @@ -745,6 +745,9 @@ nfsd3_proc_pathconf(struct svc_rqst *rqstp) */ resp->status = nfserr_stale; break; + case -ESTALE: + resp->status = nfserr_stale; + break; default: resp->status = nfserr_serverfault; break; From cfff672ffcf9a74b560c2002739729f91812e398 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Fri, 15 May 2026 11:35:15 -0400 Subject: [PATCH 26/34] nfsd: Cap case-folding probe cost across READDIR entries NFSv4 READDIR carries a per-entry attrmask. When the attrmask includes FATTR4_CASE_INSENSITIVE or FATTR4_CASE_PRESERVING, nfsd4_encode_fattr4() resolves each non-directory child's case attributes by calling nfsd_get_case_info(), which dget_parent()s back to the directory being read and re-runs the cred swap and LSM probe per child. The encoder amplifies a single answer into one prepare_kernel_cred() allocation, two LSM hooks, and one put_cred() RCU callback for every non-directory entry. No mainstream NFSv4 client has been observed to populate a READDIR attrmask with these attributes; the Linux client queries them only via SERVER_CAPS at mount time. The exposure is therefore to test clients exploring corner cases and to hostile clients that submit an attrmask designed to multiply server work by rd_dircount. Probe the directory being read once and cache the result on struct nfsd4_readdir for use by every non-directory child. The probe targets the readdir filehandle's dentry, which is held for the duration of the request, rather than dget_parent() of a child's locklessly-acquired dentry; the latter could be moved out of the directory by a concurrent rename and report attributes from an unrelated parent. Directory entries continue to be queried individually, because casefold-capable filesystems (ext4, f2fs) report case state per directory. The other callers of nfsd4_encode_fattr4() (single GETATTR, the buffer wrapper) pass NULL for the cache pointer and behave as before. Reported-by: sashiko-bot Closes: https://sashiko.dev/#/patchset/20260507-case-sensitivity-v14-0-e62cc8200435@oracle.com?part=14 Signed-off-by: Chuck Lever Link: https://patch.msgid.link/20260515153515.362266-8-cel@kernel.org Signed-off-by: Christian Brauner --- fs/nfsd/nfs4xdr.c | 55 +++++++++++++++++++++++++++++++++++++++-------- fs/nfsd/xdr4.h | 14 ++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/fs/nfsd/nfs4xdr.c b/fs/nfsd/nfs4xdr.c index 319007b79d49..20355dc3f1d1 100644 --- a/fs/nfsd/nfs4xdr.c +++ b/fs/nfsd/nfs4xdr.c @@ -3883,13 +3883,16 @@ static const nfsd4_enc_attr nfsd4_enc_fattr4_encode_ops[] = { /* * Note: @fhp can be NULL; in this case, we might have to compose the filehandle - * ourselves. + * ourselves. @case_cache is NULL for callers that encode a single dentry + * (GETATTR, the buffer wrapper); READDIR passes a per-request cache so + * non-directory children share the parent's case-folding probe result. */ static __be32 nfsd4_encode_fattr4(struct svc_rqst *rqstp, struct xdr_stream *xdr, struct svc_fh *fhp, struct svc_export *exp, struct dentry *dentry, const u32 *bmval, - int ignore_crossmnt) + int ignore_crossmnt, + struct nfsd_case_attrs_cache *case_cache) { DECLARE_BITMAP(attr_bitmap, ARRAY_SIZE(nfsd4_enc_fattr4_encode_ops)); struct nfs4_delegation *dp = NULL; @@ -3999,9 +4002,17 @@ nfsd4_encode_fattr4(struct svc_rqst *rqstp, struct xdr_stream *xdr, args.fhp = fhp; if (attrmask[0] & (FATTR4_WORD0_CASE_INSENSITIVE | FATTR4_WORD0_CASE_PRESERVING)) { - err = nfsd_get_case_info(dentry, &args.case_insensitive, - &args.case_preserving); /* + * In a batched encoder (READDIR) every non-directory + * child shares the same case-folding answer, so the + * directory being read is probed once and the result is + * cached. The probe targets case_cache->dir, the held + * readdir filehandle's dentry, instead of the child's + * locklessly-acquired dentry, which a concurrent rename + * could move under an unrelated parent. Directory + * entries are queried directly because casefold-capable + * filesystems answer per directory. + * * Per RFC 8881 Section 18.7.3, an attribute advertised * in SUPPORTED_ATTRS must come back with a value or the * GETATTR must fail. nfsd_get_case_info() fills POSIX @@ -4011,8 +4022,24 @@ nfsd4_encode_fattr4(struct svc_rqst *rqstp, struct xdr_stream *xdr, * advertises. Other errors fail the operation as the * spec requires. */ - if (err && err != -EOPNOTSUPP) - goto out_nfserr; + if (case_cache && !d_is_dir(dentry)) { + if (!case_cache->valid) { + err = nfsd_get_case_info(case_cache->dir, + &case_cache->insensitive, + &case_cache->preserving); + if (err && err != -EOPNOTSUPP) + goto out_nfserr; + case_cache->valid = true; + } + args.case_insensitive = case_cache->insensitive; + args.case_preserving = case_cache->preserving; + } else { + err = nfsd_get_case_info(dentry, + &args.case_insensitive, + &args.case_preserving); + if (err && err != -EOPNOTSUPP) + goto out_nfserr; + } } if (attrmask[0] & FATTR4_WORD0_ACL) { @@ -4170,7 +4197,7 @@ __be32 nfsd4_encode_fattr_to_buf(__be32 **p, int words, svcxdr_init_encode_from_buffer(&xdr, &dummy, *p, words << 2); ret = nfsd4_encode_fattr4(rqstp, &xdr, fhp, exp, dentry, bmval, - ignore_crossmnt); + ignore_crossmnt, NULL); *p = xdr.p; return ret; } @@ -4208,6 +4235,7 @@ nfsd4_encode_entry4_fattr(struct nfsd4_readdir *cd, const char *name, struct dentry *dentry; __be32 nfserr; int ignore_crossmnt = 0; + bool crossed = false; dentry = lookup_one_positive_unlocked(&nop_mnt_idmap, &QSTR_LEN(name, namlen), @@ -4244,11 +4272,18 @@ nfsd4_encode_entry4_fattr(struct nfsd4_readdir *cd, const char *name, nfserr = check_nfsd_access(exp, cd->rd_rqstp, false); if (nfserr) goto out_put; + crossed = true; } out_encode: + /* + * A crossed entry no longer shares a parent with the directory + * being read, so it must neither consume nor populate the + * per-readdir case-folding cache. + */ nfserr = nfsd4_encode_fattr4(cd->rd_rqstp, cd->xdr, NULL, exp, dentry, - cd->rd_bmval, ignore_crossmnt); + cd->rd_bmval, ignore_crossmnt, + crossed ? NULL : &cd->rd_case_cache); out_put: dput(dentry); exp_put(exp); @@ -4495,7 +4530,7 @@ nfsd4_encode_getattr(struct nfsd4_compoundres *resp, __be32 nfserr, /* obj_attributes */ return nfsd4_encode_fattr4(resp->rqstp, xdr, fhp, fhp->fh_export, - fhp->fh_dentry, getattr->ga_bmval, 0); + fhp->fh_dentry, getattr->ga_bmval, 0, NULL); } static __be32 @@ -5022,6 +5057,8 @@ static __be32 nfsd4_encode_dirlist4(struct xdr_stream *xdr, readdir->rd_maxcount = maxcount; readdir->common.err = 0; readdir->cookie_offset = 0; + readdir->rd_case_cache.dir = readdir->rd_fhp->fh_dentry; + readdir->rd_case_cache.valid = false; offset = readdir->rd_cookie; status = nfsd_readdir(readdir->rd_rqstp, readdir->rd_fhp, &offset, &readdir->common, nfsd4_encode_entry4); diff --git a/fs/nfsd/xdr4.h b/fs/nfsd/xdr4.h index 417e9ad9fbb3..615797df218f 100644 --- a/fs/nfsd/xdr4.h +++ b/fs/nfsd/xdr4.h @@ -432,6 +432,19 @@ struct nfsd4_read { u32 rd_eof; /* response */ }; +/* + * Cache the case-folding properties of @dir so a batched encoder + * (e.g., READDIR) does not re-probe per child. @dir is the + * directory being read, held by the request, so it is stable + * against rename for the duration of the cache's lifetime. + */ +struct nfsd_case_attrs_cache { + struct dentry *dir; + bool valid; + bool insensitive; + bool preserving; +}; + struct nfsd4_readdir { u64 rd_cookie; /* request */ nfs4_verifier rd_verf; /* request */ @@ -444,6 +457,7 @@ struct nfsd4_readdir { struct readdir_cd common; struct xdr_stream *xdr; int cookie_offset; + struct nfsd_case_attrs_cache rd_case_cache; }; struct nfsd4_release_lockowner { From 89330d3a60f7ddb9cf42873bb5249d6a736a0eab Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:45 +0100 Subject: [PATCH 27/34] filelock: pass current blocking lease to trace_break_lease_block() rather than "new_fl" The break_lease_block tracepoint currently just shows the type of "new_fl", which we can predict from the "flags" value. Switch it to display info about "fl" instead, as that's the file_lease on which the code is blocking. For trace_break_lease_unblock(), pass it a NULL pointer. "fl" may have been freed by that point, and passing it the info in new_fl is deceptive. Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-1-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/locks.c b/fs/locks.c index fead53474c30..3306892900c4 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1691,7 +1691,7 @@ int __break_lease(struct inode *inode, unsigned int flags) } else break_time++; locks_insert_block(&fl->c, &new_fl->c, leases_conflict); - trace_break_lease_block(inode, new_fl); + trace_break_lease_block(inode, fl); spin_unlock(&ctx->flc_lock); percpu_up_read(&file_rwsem); @@ -1702,7 +1702,7 @@ int __break_lease(struct inode *inode, unsigned int flags) percpu_down_read(&file_rwsem); spin_lock(&ctx->flc_lock); - trace_break_lease_unblock(inode, new_fl); + trace_break_lease_unblock(inode, NULL); __locks_delete_block(&new_fl->c); if (error >= 0) { /* From 24cbf43337f46329ddda5983bc3c585174a020ee Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:46 +0100 Subject: [PATCH 28/34] filelock: add support for ignoring deleg breaks for dir change events If a NFS client requests a directory delegation with a notification bitmask covering directory change events, the server shouldn't recall the delegation. Instead the client will be notified of the change after the fact. Add support for ignoring lease breaks on directory changes. Add a new flags parameter to try_break_deleg() and teach __break_lease how to ignore certain types of delegation break events. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-2-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/attr.c | 2 +- fs/locks.c | 86 ++++++++++++++++++++++----------- fs/namei.c | 31 ++++++------ fs/posix_acl.c | 4 +- fs/xattr.c | 4 +- include/linux/filelock.h | 53 ++++++++++++++------ include/trace/events/filelock.h | 5 +- 7 files changed, 122 insertions(+), 63 deletions(-) diff --git a/fs/attr.c b/fs/attr.c index ded221defae6..4f437fabb7f0 100644 --- a/fs/attr.c +++ b/fs/attr.c @@ -547,7 +547,7 @@ int notify_change(struct mnt_idmap *idmap, struct dentry *dentry, * breaking the delegation in this case. */ if (!(ia_valid & ATTR_DELEG)) { - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) return error; } diff --git a/fs/locks.c b/fs/locks.c index 3306892900c4..31086d6153e2 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1583,29 +1583,63 @@ static bool leases_conflict(struct file_lock_core *lc, struct file_lock_core *bc } static bool -any_leases_conflict(struct inode *inode, struct file_lease *breaker) +ignore_dir_deleg_break(struct file_lease *fl, unsigned int flags) { - struct file_lock_context *ctx = inode->i_flctx; - struct file_lock_core *flc; + if ((flags & LEASE_BREAK_DIR_CREATE) && (fl->c.flc_flags & FL_IGN_DIR_CREATE)) + return true; + if ((flags & LEASE_BREAK_DIR_DELETE) && (fl->c.flc_flags & FL_IGN_DIR_DELETE)) + return true; + if ((flags & LEASE_BREAK_DIR_RENAME) && (fl->c.flc_flags & FL_IGN_DIR_RENAME)) + return true; - lockdep_assert_held(&ctx->flc_lock); - - list_for_each_entry(flc, &ctx->flc_lease, flc_list) { - if (leases_conflict(flc, &breaker->c)) - return true; - } return false; } +static unsigned int +break_lease_flags_to_type(unsigned int flags) +{ + if (flags & LEASE_BREAK_LEASE) + return FL_LEASE; + else if (flags & LEASE_BREAK_DELEG) + return FL_DELEG; + else if (flags & LEASE_BREAK_LAYOUT) + return FL_LAYOUT; + else + return 0; + +} + +static struct file_lease * +first_visible_lease(struct inode *inode, struct file_lease *new_fl, unsigned int flags) +{ + struct file_lock_context *ctx = locks_inode_context(inode); + struct file_lease *fl; + + lockdep_assert_held(&ctx->flc_lock); + + list_for_each_entry(fl, &ctx->flc_lease, c.flc_list) { + if (!leases_conflict(&fl->c, &new_fl->c)) + continue; + if (S_ISDIR(inode->i_mode) && ignore_dir_deleg_break(fl, flags)) + continue; + return fl; + } + return NULL; +} + + /** - * __break_lease - revoke all outstanding leases on file - * @inode: the inode of the file to return - * @flags: LEASE_BREAK_* flags + * __break_lease - revoke all outstanding leases on file + * @inode: the inode of the file to return + * @flags: LEASE_BREAK_* flags * - * break_lease (inlined for speed) has checked there already is at least - * some kind of lock (maybe a lease) on this file. Leases are broken on - * a call to open() or truncate(). This function can block waiting for the - * lease break unless you specify LEASE_BREAK_NONBLOCK. + * break_lease (inlined for speed) has checked there already is at least + * some kind of lock (maybe a lease) on this file. Leases and Delegations + * are broken on a call to open() or truncate(). Delegations are also + * broken on any event that would change the ctime. Directory delegations + * are broken whenever the directory changes (unless the delegation is set + * up to ignore the event). This function can block waiting for the lease + * break unless you specify LEASE_BREAK_NONBLOCK. */ int __break_lease(struct inode *inode, unsigned int flags) { @@ -1617,13 +1651,8 @@ int __break_lease(struct inode *inode, unsigned int flags) bool want_write = !(flags & LEASE_BREAK_OPEN_RDONLY); int error = 0; - if (flags & LEASE_BREAK_LEASE) - type = FL_LEASE; - else if (flags & LEASE_BREAK_DELEG) - type = FL_DELEG; - else if (flags & LEASE_BREAK_LAYOUT) - type = FL_LAYOUT; - else + type = break_lease_flags_to_type(flags); + if (!type) return -EINVAL; new_fl = lease_alloc(NULL, type, want_write ? F_WRLCK : F_RDLCK); @@ -1642,7 +1671,7 @@ int __break_lease(struct inode *inode, unsigned int flags) time_out_leases(inode, &dispose); - if (!any_leases_conflict(inode, new_fl)) + if (!first_visible_lease(inode, new_fl, flags)) goto out; break_time = 0; @@ -1655,6 +1684,8 @@ int __break_lease(struct inode *inode, unsigned int flags) list_for_each_entry_safe(fl, tmp, &ctx->flc_lease, c.flc_list) { if (!leases_conflict(&fl->c, &new_fl->c)) continue; + if (S_ISDIR(inode->i_mode) && ignore_dir_deleg_break(fl, flags)) + continue; if (want_write) { if (fl->c.flc_flags & FL_UNLOCK_PENDING) continue; @@ -1670,7 +1701,8 @@ int __break_lease(struct inode *inode, unsigned int flags) locks_delete_lock_ctx(&fl->c, &dispose); } - if (list_empty(&ctx->flc_lease)) + fl = first_visible_lease(inode, new_fl, flags); + if (!fl) goto out; if (flags & LEASE_BREAK_NONBLOCK) { @@ -1680,7 +1712,6 @@ int __break_lease(struct inode *inode, unsigned int flags) } restart: - fl = list_first_entry(&ctx->flc_lease, struct file_lease, c.flc_list); break_time = fl->fl_break_time; if (break_time != 0) { if (time_after(jiffies, break_time)) { @@ -1711,7 +1742,8 @@ int __break_lease(struct inode *inode, unsigned int flags) */ if (error == 0) time_out_leases(inode, &dispose); - if (any_leases_conflict(inode, new_fl)) + fl = first_visible_lease(inode, new_fl, flags); + if (fl) goto restart; error = 0; } diff --git a/fs/namei.c b/fs/namei.c index c7fac83c9a85..3a3a2e5e77a0 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -4198,7 +4198,7 @@ int vfs_create(struct mnt_idmap *idmap, struct dentry *dentry, umode_t mode, error = security_inode_create(dir, dentry, mode); if (error) return error; - error = try_break_deleg(dir, di); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, di); if (error) return error; error = dir->i_op->create(idmap, dir, dentry, mode, true); @@ -4497,7 +4497,7 @@ static struct dentry *lookup_open(struct nameidata *nd, struct file *file, /* Negative dentry, just create the file */ if (!dentry->d_inode && (open_flag & O_CREAT)) { /* but break the directory lease first! */ - error = try_break_deleg(dir_inode, delegated_inode); + error = try_break_deleg(dir_inode, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto out_dput; @@ -5113,7 +5113,7 @@ int vfs_mknod(struct mnt_idmap *idmap, struct inode *dir, if (error) return error; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) return error; @@ -5254,7 +5254,7 @@ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, if (max_links && dir->i_nlink >= max_links) goto err; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto err; @@ -5359,7 +5359,7 @@ int vfs_rmdir(struct mnt_idmap *idmap, struct inode *dir, if (error) goto out; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_DELETE, delegated_inode); if (error) goto out; @@ -5489,10 +5489,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir, else { error = security_inode_unlink(dir, dentry); if (!error) { - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_DELETE, delegated_inode); if (error) goto out; - error = try_break_deleg(target, delegated_inode); + error = try_break_deleg(target, 0, delegated_inode); if (error) goto out; error = dir->i_op->unlink(dir, dentry); @@ -5636,7 +5636,7 @@ int vfs_symlink(struct mnt_idmap *idmap, struct inode *dir, if (error) return error; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) return error; @@ -5767,9 +5767,9 @@ int vfs_link(struct dentry *old_dentry, struct mnt_idmap *idmap, else if (max_links && inode->i_nlink >= max_links) error = -EMLINK; else { - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (!error) - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (!error) error = dir->i_op->link(old_dentry, dir, new_dentry); } @@ -6033,21 +6033,24 @@ int vfs_rename(struct renamedata *rd) old_dir->i_nlink >= max_links) goto out; } - error = try_break_deleg(old_dir, delegated_inode); + error = try_break_deleg(old_dir, + old_dir == new_dir ? LEASE_BREAK_DIR_RENAME : + LEASE_BREAK_DIR_DELETE, + delegated_inode); if (error) goto out; if (new_dir != old_dir) { - error = try_break_deleg(new_dir, delegated_inode); + error = try_break_deleg(new_dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto out; } if (!is_dir) { - error = try_break_deleg(source, delegated_inode); + error = try_break_deleg(source, 0, delegated_inode); if (error) goto out; } if (target && !new_is_dir) { - error = try_break_deleg(target, delegated_inode); + error = try_break_deleg(target, 0, delegated_inode); if (error) goto out; } diff --git a/fs/posix_acl.c b/fs/posix_acl.c index 12591c95c925..b4bfe4ddf64e 100644 --- a/fs/posix_acl.c +++ b/fs/posix_acl.c @@ -1126,7 +1126,7 @@ int vfs_set_acl(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out_inode_unlock; - error = try_break_deleg(inode, &delegated_inode); + error = try_break_deleg(inode, 0, &delegated_inode); if (error) goto out_inode_unlock; @@ -1234,7 +1234,7 @@ int vfs_remove_acl(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out_inode_unlock; - error = try_break_deleg(inode, &delegated_inode); + error = try_break_deleg(inode, 0, &delegated_inode); if (error) goto out_inode_unlock; diff --git a/fs/xattr.c b/fs/xattr.c index 09ecbaaa1660..efdcf2a48585 100644 --- a/fs/xattr.c +++ b/fs/xattr.c @@ -306,7 +306,7 @@ __vfs_setxattr_locked(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out; - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) goto out; @@ -564,7 +564,7 @@ __vfs_removexattr_locked(struct mnt_idmap *idmap, if (error) goto out; - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) goto out; diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 5f0a2fb31450..9dd4e67a6f30 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -4,19 +4,22 @@ #include -#define FL_POSIX 1 -#define FL_FLOCK 2 -#define FL_DELEG 4 /* NFSv4 delegation */ -#define FL_ACCESS 8 /* not trying to lock, just looking */ -#define FL_EXISTS 16 /* when unlocking, test for existence */ -#define FL_LEASE 32 /* lease held on this file */ -#define FL_CLOSE 64 /* unlock on close */ -#define FL_SLEEP 128 /* A blocking lock */ -#define FL_DOWNGRADE_PENDING 256 /* Lease is being downgraded */ -#define FL_UNLOCK_PENDING 512 /* Lease is being broken */ -#define FL_OFDLCK 1024 /* lock is "owned" by struct file */ -#define FL_LAYOUT 2048 /* outstanding pNFS layout */ -#define FL_RECLAIM 4096 /* reclaiming from a reboot server */ +#define FL_POSIX BIT(0) /* POSIX lock */ +#define FL_FLOCK BIT(1) /* BSD lock */ +#define FL_DELEG BIT(2) /* NFSv4 delegation */ +#define FL_ACCESS BIT(3) /* not trying to lock, just looking */ +#define FL_EXISTS BIT(4) /* when unlocking, test for existence */ +#define FL_LEASE BIT(5) /* file lease */ +#define FL_CLOSE BIT(6) /* unlock on close */ +#define FL_SLEEP BIT(7) /* A blocking lock */ +#define FL_DOWNGRADE_PENDING BIT(8) /* Lease is being downgraded */ +#define FL_UNLOCK_PENDING BIT(9) /* Lease is being broken */ +#define FL_OFDLCK BIT(10) /* POSIX lock "owned" by struct file */ +#define FL_LAYOUT BIT(11) /* outstanding pNFS layout */ +#define FL_RECLAIM BIT(12) /* reclaiming from a reboot server */ +#define FL_IGN_DIR_CREATE BIT(13) /* ignore DIR_CREATE events */ +#define FL_IGN_DIR_DELETE BIT(14) /* ignore DIR_DELETE events */ +#define FL_IGN_DIR_RENAME BIT(15) /* ignore DIR_RENAME events */ #define FL_CLOSE_POSIX (FL_POSIX | FL_CLOSE) @@ -222,6 +225,10 @@ struct file_lease *locks_alloc_lease(void); #define LEASE_BREAK_LAYOUT BIT(2) // break layouts only #define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break #define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event +#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event +#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event +#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event + int __break_lease(struct inode *inode, unsigned int flags); void lease_get_mtime(struct inode *, struct timespec64 *time); @@ -516,12 +523,26 @@ static inline bool is_delegated(struct delegated_inode *di) return di->di_inode; } -static inline int try_break_deleg(struct inode *inode, +/** + * try_break_deleg - do a non-blocking delegation break + * @inode: inode that should have its delegations broken + * @flags: extra LEASE_BREAK_* flags to pass to break_deleg() + * @di: returns pointer to delegated inode (may be NULL) + * + * Break delegations in a non-blocking fashion. If there are + * outstanding delegations and @di is set, then an extra reference + * will be taken on @inode and @di->di_inode will be populated so + * that it may be waited upon. + * + * Returns 0 if there is no need to wait or an error. If -EWOULDBLOCK + * is returned, then @di will be populated (if non-NULL). + */ +static inline int try_break_deleg(struct inode *inode, unsigned int flags, struct delegated_inode *di) { int ret; - ret = break_deleg(inode, LEASE_BREAK_NONBLOCK); + ret = break_deleg(inode, flags | LEASE_BREAK_NONBLOCK); if (ret == -EWOULDBLOCK && di) { di->di_inode = inode; ihold(inode); @@ -574,7 +595,7 @@ static inline int break_deleg(struct inode *inode, unsigned int flags) return 0; } -static inline int try_break_deleg(struct inode *inode, +static inline int try_break_deleg(struct inode *inode, unsigned int flags, struct delegated_inode *delegated_inode) { return 0; diff --git a/include/trace/events/filelock.h b/include/trace/events/filelock.h index 116774886244..718b5c3f1737 100644 --- a/include/trace/events/filelock.h +++ b/include/trace/events/filelock.h @@ -28,7 +28,10 @@ { FL_DOWNGRADE_PENDING, "FL_DOWNGRADE_PENDING" }, \ { FL_UNLOCK_PENDING, "FL_UNLOCK_PENDING" }, \ { FL_OFDLCK, "FL_OFDLCK" }, \ - { FL_RECLAIM, "FL_RECLAIM"}) + { FL_RECLAIM, "FL_RECLAIM" }, \ + { FL_IGN_DIR_CREATE, "FL_IGN_DIR_CREATE" }, \ + { FL_IGN_DIR_DELETE, "FL_IGN_DIR_DELETE" }, \ + { FL_IGN_DIR_RENAME, "FL_IGN_DIR_RENAME" }) #define show_fl_type(val) \ __print_symbolic(val, \ From e39026a86b485b052ddff1145f5601fc0d04305f Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:47 +0100 Subject: [PATCH 29/34] filelock: add a tracepoint to start of break_lease() ...mostly to show the LEASE_BREAK_* flags. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-3-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 2 ++ include/trace/events/filelock.h | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/fs/locks.c b/fs/locks.c index 31086d6153e2..afa80fe6e9ff 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1651,6 +1651,8 @@ int __break_lease(struct inode *inode, unsigned int flags) bool want_write = !(flags & LEASE_BREAK_OPEN_RDONLY); int error = 0; + trace_break_lease(inode, flags); + type = break_lease_flags_to_type(flags); if (!type) return -EINVAL; diff --git a/include/trace/events/filelock.h b/include/trace/events/filelock.h index 718b5c3f1737..f2bb6b7aa281 100644 --- a/include/trace/events/filelock.h +++ b/include/trace/events/filelock.h @@ -120,6 +120,39 @@ DEFINE_EVENT(filelock_lock, flock_lock_inode, TP_PROTO(struct inode *inode, struct file_lock *fl, int ret), TP_ARGS(inode, fl, ret)); +#define show_lease_break_flags(val) \ + __print_flags(val, "|", \ + { LEASE_BREAK_LEASE, "LEASE" }, \ + { LEASE_BREAK_DELEG, "DELEG" }, \ + { LEASE_BREAK_LAYOUT, "LAYOUT" }, \ + { LEASE_BREAK_NONBLOCK, "NONBLOCK" }, \ + { LEASE_BREAK_OPEN_RDONLY, "OPEN_RDONLY" }, \ + { LEASE_BREAK_DIR_CREATE, "DIR_CREATE" }, \ + { LEASE_BREAK_DIR_DELETE, "DIR_DELETE" }, \ + { LEASE_BREAK_DIR_RENAME, "DIR_RENAME" }) + +TRACE_EVENT(break_lease, + TP_PROTO(struct inode *inode, unsigned int flags), + + TP_ARGS(inode, flags), + + TP_STRUCT__entry( + __field(unsigned long, i_ino) + __field(dev_t, s_dev) + __field(unsigned int, flags) + ), + + TP_fast_assign( + __entry->s_dev = inode->i_sb->s_dev; + __entry->i_ino = inode->i_ino; + __entry->flags = flags; + ), + + TP_printk("dev=0x%x:0x%x ino=0x%lx flags=%s", + MAJOR(__entry->s_dev), MINOR(__entry->s_dev), + __entry->i_ino, show_lease_break_flags(__entry->flags)) +); + DECLARE_EVENT_CLASS(filelock_lease, TP_PROTO(struct inode *inode, struct file_lease *fl), From 95825fdcc0b0e868571d1c755f6a6971caf9b7cb Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:48 +0100 Subject: [PATCH 30/34] filelock: add an inode_lease_ignore_mask helper Add a new routine that returns a mask of all dir change events that are currently ignored by any leases. nfsd will use this to determine how to configure the fsnotify_mark mask. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-4-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 32 ++++++++++++++++++++++++++++++++ include/linux/filelock.h | 1 + 2 files changed, 33 insertions(+) diff --git a/fs/locks.c b/fs/locks.c index afa80fe6e9ff..6e4ff7fcec05 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1582,6 +1582,38 @@ static bool leases_conflict(struct file_lock_core *lc, struct file_lock_core *bc return rc; } +#define IGNORE_MASK (FL_IGN_DIR_CREATE | FL_IGN_DIR_DELETE | FL_IGN_DIR_RENAME) + +/** + * inode_lease_ignore_mask - return union of all ignored inode events for this inode + * @inode: inode of which to get ignore mask + * + * Walk the list of leases, and return the result of all of + * their FL_IGN_DIR_* bits or'ed together. + */ +u32 +inode_lease_ignore_mask(struct inode *inode) +{ + struct file_lock_context *ctx; + struct file_lock_core *flc; + u32 mask = 0; + + ctx = locks_inode_context(inode); + if (!ctx) + return 0; + + spin_lock(&ctx->flc_lock); + list_for_each_entry(flc, &ctx->flc_lease, flc_list) { + mask |= flc->flc_flags & IGNORE_MASK; + /* If we already have everything, we can stop */ + if (mask == IGNORE_MASK) + break; + } + spin_unlock(&ctx->flc_lock); + return mask; +} +EXPORT_SYMBOL_GPL(inode_lease_ignore_mask); + static bool ignore_dir_deleg_break(struct file_lease *fl, unsigned int flags) { diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 9dd4e67a6f30..6e125902c58a 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -236,6 +236,7 @@ int generic_setlease(struct file *, int, struct file_lease **, void **priv); int kernel_setlease(struct file *, int, struct file_lease **, void **); int vfs_setlease(struct file *, int, struct file_lease **, void **); int lease_modify(struct file_lease *, int, struct list_head *); +u32 inode_lease_ignore_mask(struct inode *inode); struct notifier_block; int lease_register_notifier(struct notifier_block *); From ad4489dcd08dcfbc32ea6e6a4f558cdd459bd80c Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:49 +0100 Subject: [PATCH 31/34] fsnotify: new tracepoint in fsnotify() Add a tracepoint so we can see exactly how this is being called. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-5-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/notify/fsnotify.c | 5 ++++ include/trace/events/fsnotify.h | 51 +++++++++++++++++++++++++++++++++ include/trace/misc/fsnotify.h | 35 ++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 include/trace/events/fsnotify.h create mode 100644 include/trace/misc/fsnotify.h diff --git a/fs/notify/fsnotify.c b/fs/notify/fsnotify.c index b7198c4744e3..0031f45e702b 100644 --- a/fs/notify/fsnotify.c +++ b/fs/notify/fsnotify.c @@ -14,6 +14,9 @@ #include #include "fsnotify.h" +#define CREATE_TRACE_POINTS +#include + /* * Clear all of the marks on an inode when it is being evicted from core */ @@ -504,6 +507,8 @@ int fsnotify(__u32 mask, const void *data, int data_type, struct inode *dir, int ret = 0; __u32 test_mask, marks_mask = 0; + trace_fsnotify(mask, data, data_type, dir, file_name, inode, cookie); + if (path) mnt = real_mount(path->mnt); diff --git a/include/trace/events/fsnotify.h b/include/trace/events/fsnotify.h new file mode 100644 index 000000000000..341bbd57a39b --- /dev/null +++ b/include/trace/events/fsnotify.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#undef TRACE_SYSTEM +#define TRACE_SYSTEM fsnotify + +#if !defined(_TRACE_FSNOTIFY_H) || defined(TRACE_HEADER_MULTI_READ) +#define _TRACE_FSNOTIFY_H + +#include + +#include + +TRACE_EVENT(fsnotify, + TP_PROTO(__u32 mask, const void *data, int data_type, + struct inode *dir, const struct qstr *file_name, + struct inode *inode, u32 cookie), + + TP_ARGS(mask, data, data_type, dir, file_name, inode, cookie), + + TP_STRUCT__entry( + __field(__u32, mask) + __field(unsigned long, dir_ino) + __field(unsigned long, ino) + __field(dev_t, s_dev) + __field(int, data_type) + __field(u32, cookie) + __string(file_name, file_name ? (const char *)file_name->name : "") + ), + + TP_fast_assign( + __entry->mask = mask; + __entry->dir_ino = dir ? dir->i_ino : 0; + __entry->ino = inode ? inode->i_ino : 0; + __entry->s_dev = dir ? dir->i_sb->s_dev : + inode ? inode->i_sb->s_dev : 0; + __entry->data_type = data_type; + __entry->cookie = cookie; + __assign_str(file_name); + ), + + TP_printk("dev=%d:%d dir=%lu ino=%lu data_type=%d cookie=0x%x mask=0x%x %s name=%s", + MAJOR(__entry->s_dev), MINOR(__entry->s_dev), + __entry->dir_ino, __entry->ino, + __entry->data_type, __entry->cookie, + __entry->mask, show_fsnotify_mask(__entry->mask), + __get_str(file_name)) +); + +#endif /* _TRACE_FSNOTIFY_H */ + +/* This part must be outside protection */ +#include diff --git a/include/trace/misc/fsnotify.h b/include/trace/misc/fsnotify.h new file mode 100644 index 000000000000..a201e1bd6d8c --- /dev/null +++ b/include/trace/misc/fsnotify.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Display helpers for fsnotify events + */ + +#include + +#define show_fsnotify_mask(mask) \ + __print_flags(mask, "|", \ + { FS_ACCESS, "ACCESS" }, \ + { FS_MODIFY, "MODIFY" }, \ + { FS_ATTRIB, "ATTRIB" }, \ + { FS_CLOSE_WRITE, "CLOSE_WRITE" }, \ + { FS_CLOSE_NOWRITE, "CLOSE_NOWRITE" }, \ + { FS_OPEN, "OPEN" }, \ + { FS_MOVED_FROM, "MOVED_FROM" }, \ + { FS_MOVED_TO, "MOVED_TO" }, \ + { FS_CREATE, "CREATE" }, \ + { FS_DELETE, "DELETE" }, \ + { FS_DELETE_SELF, "DELETE_SELF" }, \ + { FS_MOVE_SELF, "MOVE_SELF" }, \ + { FS_OPEN_EXEC, "OPEN_EXEC" }, \ + { FS_UNMOUNT, "UNMOUNT" }, \ + { FS_Q_OVERFLOW, "Q_OVERFLOW" }, \ + { FS_ERROR, "ERROR" }, \ + { FS_OPEN_PERM, "OPEN_PERM" }, \ + { FS_ACCESS_PERM, "ACCESS_PERM" }, \ + { FS_OPEN_EXEC_PERM, "OPEN_EXEC_PERM" }, \ + { FS_PRE_ACCESS, "PRE_ACCESS" }, \ + { FS_MNT_ATTACH, "MNT_ATTACH" }, \ + { FS_MNT_DETACH, "MNT_DETACH" }, \ + { FS_EVENT_ON_CHILD, "EVENT_ON_CHILD" }, \ + { FS_RENAME, "RENAME" }, \ + { FS_DN_MULTISHOT, "DN_MULTISHOT" }, \ + { FS_ISDIR, "ISDIR" }) From 12ffbb117b64d9f4b1b02521010a5f2953a1972a Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:50 +0100 Subject: [PATCH 32/34] fsnotify: add fsnotify_modify_mark_mask() nfsd needs to be able to modify the mask on an existing mark when new directory delegations are set or unset. Add an exported function that allows the caller to set and clear bits in the mark->mask, and does the recalculation if something changed. Suggested-by: Jan Kara Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-6-5a0780ba9def@kernel.org Acked-by: Jan Kara Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/notify/mark.c | 29 +++++++++++++++++++++++++++++ include/linux/fsnotify_backend.h | 1 + 2 files changed, 30 insertions(+) diff --git a/fs/notify/mark.c b/fs/notify/mark.c index c2ed5b11b0fe..b1e73c6fd382 100644 --- a/fs/notify/mark.c +++ b/fs/notify/mark.c @@ -310,6 +310,35 @@ void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn) fsnotify_conn_set_children_dentry_flags(conn); } +/** + * fsnotify_modify_mark_mask - set and/or clear flags in a mark's mask + * @mark: mark to be modified + * @set: bits to be set in mask + * @clear: bits to be cleared in mask + * + * Modify a fsnotify_mark mask as directed, and update its associated conn. + * The caller is expected to hold a reference to the mark. + */ +void fsnotify_modify_mark_mask(struct fsnotify_mark *mark, u32 set, u32 clear) +{ + bool recalc = false; + u32 mask; + + WARN_ON_ONCE(clear & set); + + spin_lock(&mark->lock); + mask = mark->mask; + mark->mask |= set; + mark->mask &= ~clear; + if (mark->mask != mask) + recalc = true; + spin_unlock(&mark->lock); + + if (recalc) + fsnotify_recalc_mask(mark->connector); +} +EXPORT_SYMBOL_GPL(fsnotify_modify_mark_mask); + /* Free all connectors queued for freeing once SRCU period ends */ static void fsnotify_connector_destroy_workfn(struct work_struct *work) { diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 95985400d3d8..66e185bd1b1b 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -917,6 +917,7 @@ extern void fsnotify_get_mark(struct fsnotify_mark *mark); extern void fsnotify_put_mark(struct fsnotify_mark *mark); extern void fsnotify_finish_user_wait(struct fsnotify_iter_info *iter_info); extern bool fsnotify_prepare_user_wait(struct fsnotify_iter_info *iter_info); +extern void fsnotify_modify_mark_mask(struct fsnotify_mark *mark, u32 set, u32 clear); static inline void fsnotify_init_event(struct fsnotify_event *event) { From 010043003c0c81c0cedde267076cbe1e0911db49 Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:51 +0100 Subject: [PATCH 33/34] fsnotify: add FSNOTIFY_EVENT_RENAME data type Add a new fsnotify_rename_data struct and FSNOTIFY_EVENT_RENAME data type that carries both the moved dentry and the inode that was overwritten by the rename (if any). Update fsnotify_data_inode(), fsnotify_data_dentry(), and fsnotify_data_sb() to handle the new type, and add a new fsnotify_data_rename_target() helper for extracting the overwritten target inode. Update fsnotify_move() to use the new data type for FS_RENAME and FS_MOVED_TO events, passing the overwritten target inode through the event data. FS_MOVED_FROM is unchanged since the source directory doesn't need overwrite information. This is done so that fsnotify consumers like nfsd can atomically observe the overwritten file when a rename replaces an existing entry, without needing a separate FS_DELETE event. Assisted-by: Claude (Anthropic Claude Code) Reviewed-by: Jan Kara Reviewed-by: Amir Goldstein Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-7-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- include/linux/fsnotify.h | 8 ++++++-- include/linux/fsnotify_backend.h | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/include/linux/fsnotify.h b/include/linux/fsnotify.h index 079c18bcdbde..bda798bc67bc 100644 --- a/include/linux/fsnotify.h +++ b/include/linux/fsnotify.h @@ -257,6 +257,10 @@ static inline void fsnotify_move(struct inode *old_dir, struct inode *new_dir, __u32 new_dir_mask = FS_MOVED_TO; __u32 rename_mask = FS_RENAME; const struct qstr *new_name = &moved->d_name; + struct fsnotify_rename_data rd = { + .moved = moved, + .target = target, + }; if (isdir) { old_dir_mask |= FS_ISDIR; @@ -265,12 +269,12 @@ static inline void fsnotify_move(struct inode *old_dir, struct inode *new_dir, } /* Event with information about both old and new parent+name */ - fsnotify_name(rename_mask, moved, FSNOTIFY_EVENT_DENTRY, + fsnotify_name(rename_mask, &rd, FSNOTIFY_EVENT_RENAME, old_dir, old_name, 0); fsnotify_name(old_dir_mask, source, FSNOTIFY_EVENT_INODE, old_dir, old_name, fs_cookie); - fsnotify_name(new_dir_mask, source, FSNOTIFY_EVENT_INODE, + fsnotify_name(new_dir_mask, &rd, FSNOTIFY_EVENT_RENAME, new_dir, new_name, fs_cookie); if (target) diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 66e185bd1b1b..f8c8fb7f34ae 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -311,6 +311,7 @@ enum fsnotify_data_type { FSNOTIFY_EVENT_DENTRY, FSNOTIFY_EVENT_MNT, FSNOTIFY_EVENT_ERROR, + FSNOTIFY_EVENT_RENAME, }; struct fs_error_report { @@ -335,6 +336,11 @@ struct fsnotify_mnt { u64 mnt_id; }; +struct fsnotify_rename_data { + struct dentry *moved; /* the dentry that was renamed */ + struct inode *target; /* inode overwritten by rename, or NULL */ +}; + static inline struct inode *fsnotify_data_inode(const void *data, int data_type) { switch (data_type) { @@ -348,6 +354,8 @@ static inline struct inode *fsnotify_data_inode(const void *data, int data_type) return d_inode(file_range_path(data)->dentry); case FSNOTIFY_EVENT_ERROR: return ((struct fs_error_report *)data)->inode; + case FSNOTIFY_EVENT_RENAME: + return d_inode(((const struct fsnotify_rename_data *)data)->moved); default: return NULL; } @@ -363,6 +371,8 @@ static inline struct dentry *fsnotify_data_dentry(const void *data, int data_typ return ((const struct path *)data)->dentry; case FSNOTIFY_EVENT_FILE_RANGE: return file_range_path(data)->dentry; + case FSNOTIFY_EVENT_RENAME: + return ((struct fsnotify_rename_data *)data)->moved; default: return NULL; } @@ -395,6 +405,8 @@ static inline struct super_block *fsnotify_data_sb(const void *data, return file_range_path(data)->dentry->d_sb; case FSNOTIFY_EVENT_ERROR: return ((struct fs_error_report *) data)->sb; + case FSNOTIFY_EVENT_RENAME: + return ((const struct fsnotify_rename_data *)data)->moved->d_sb; default: return NULL; } @@ -430,6 +442,14 @@ static inline struct fs_error_report *fsnotify_data_error_report( } } +static inline struct inode *fsnotify_data_rename_target(const void *data, + int data_type) +{ + if (data_type == FSNOTIFY_EVENT_RENAME) + return ((const struct fsnotify_rename_data *)data)->target; + return NULL; +} + static inline const struct file_range *fsnotify_data_file_range( const void *data, int data_type) From 246bc86d0fd891273a8502314f158eab23af823c Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Sat, 16 May 2026 07:11:23 -0400 Subject: [PATCH 34/34] filelock: move LEASE_BREAK_* flags out of #ifdef CONFIG_FILE_LOCKING This was causing a build break when CONFIG_FILE_LOCKING was disabled. Move the LEASE_BREAK_* flags into the non-#ifdef'ed part of the file. Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202605161232.1lY6pZoM-lkp@intel.com/ Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260516-dir-deleg-fix-v1-1-1b68f0aa990a@kernel.org Signed-off-by: Christian Brauner --- include/linux/filelock.h | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 6e125902c58a..7a7a6e8a9a08 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -29,6 +29,15 @@ */ #define FILE_LOCK_DEFERRED 1 +#define LEASE_BREAK_LEASE BIT(0) // break leases and delegations +#define LEASE_BREAK_DELEG BIT(1) // break delegations only +#define LEASE_BREAK_LAYOUT BIT(2) // break layouts only +#define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break +#define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event +#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event +#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event +#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event + struct file_lock; struct file_lease; @@ -219,17 +228,6 @@ int locks_lock_inode_wait(struct inode *inode, struct file_lock *fl); void locks_init_lease(struct file_lease *); void locks_free_lease(struct file_lease *fl); struct file_lease *locks_alloc_lease(void); - -#define LEASE_BREAK_LEASE BIT(0) // break leases and delegations -#define LEASE_BREAK_DELEG BIT(1) // break delegations only -#define LEASE_BREAK_LAYOUT BIT(2) // break layouts only -#define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break -#define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event -#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event -#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event -#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event - - int __break_lease(struct inode *inode, unsigned int flags); void lease_get_mtime(struct inode *, struct timespec64 *time); int generic_setlease(struct file *, int, struct file_lease **, void **priv);