]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
nfs: Implement fileattr_get for case sensitivity
authorChuck Lever <chuck.lever@oracle.com>
Thu, 7 May 2026 08:53:03 +0000 (04:53 -0400)
committerChristian Brauner <brauner@kernel.org>
Mon, 11 May 2026 14:50:29 +0000 (16:50 +0200)
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 <roland.mainz@nrubsig.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Link: https://patch.msgid.link/20260507-case-sensitivity-v14-10-e62cc8200435@oracle.com
Signed-off-by: Christian Brauner <brauner@kernel.org>
fs/nfs/client.c
fs/nfs/inode.c
fs/nfs/internal.h
fs/nfs/namespace.c
fs/nfs/nfs3proc.c
fs/nfs/nfs3xdr.c
fs/nfs/nfs4proc.c
fs/nfs/proc.c
fs/nfs/symlink.c
include/linux/nfs_fs_sb.h
include/linux/nfs_xdr.h

index be02bb227741d57aea784920ab94fecb548b077e..3db2f18315b87ba78e72f912f00c1018c31d75e5 100644 (file)
@@ -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 &&
index e26030e736966b4df4052ee32364b778680e3919..170d32c217ae44d75ab3f856f1414e0cd348258d 100644 (file)
@@ -41,6 +41,7 @@
 #include <linux/freezer.h>
 #include <linux/uaccess.h>
 #include <linux/iversion.h>
+#include <linux/fileattr.h>
 
 #include "nfs4_fs.h"
 #include "callback.h"
@@ -1095,6 +1096,20 @@ out:
 }
 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);
index 18d46b0e71ddc83a7b63f03eacf4337ed2735b61..ec2b3d9843989d248efb998047d89884c27564ce 100644 (file)
@@ -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 {
index af9be0c5f516300b3edaf124ebd09a6c7a46fc28..6d0073c24771bc4e9e78d2690a492dfdb89a4cd9 100644 (file)
@@ -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)
index 95d7cd564b7465463a94b77daa3975e728fc22a0..b80d0c5efc279bb4926668d98a32fe1c544f2326 100644 (file)
@@ -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,
index e17d7290841256613c422ee2b6c4b545f7e7092b..e745e78faab0a084b68a0eefc0475c1126f530fb 100644 (file)
@@ -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;
 }
 
index a9b8d482d28946ad6eaac57c68f23ce3b52d47db..0715a6745d1fa2460ecc7087ec0ec870808a6190 100644 (file)
@@ -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,
index 70795684b8e8467192dd4b47b5c5117223e88c03..03c2c1f31be9a202ffe6ccb21955367af79915f5 100644 (file)
@@ -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 = {
index 58146e9354020fc482ae2b6241c4a1fe8af63d79..74a072896f8d9de82d8144cee29dfc5af943416f 100644 (file)
@@ -22,6 +22,8 @@
 #include <linux/mm.h>
 #include <linux/string.h>
 
+#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,
 };
index 4daee27fa5eb41b9936cefa5449902c62895f9ce..34d294774f8cec9d18c89db229d1d687076b3054 100644 (file)
@@ -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)
index fcbd21b5685f46136a210c8e11c20a54d6ed9dad..83ee991cde2be1c30e32ad0bb88bd0f97c542690 100644 (file)
@@ -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 {