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 <roland.mainz@nrubsig.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Link: https://patch.msgid.link/20260507-case-sensitivity-v14-13-e62cc8200435@oracle.com
Signed-off-by: Christian Brauner <brauner@kernel.org>
This commit is contained in:
Chuck Lever
2026-05-07 04:53:06 -04:00
committed by Christian Brauner
parent 7bbd51b1d7
commit 211cb2ba48
4 changed files with 121 additions and 10 deletions

View File

@@ -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;
}
}

View File

@@ -32,6 +32,7 @@
#include <linux/writeback.h>
#include <linux/security.h>
#include <linux/sunrpc/xdr.h>
#include <linux/fileattr.h>
#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;
}
}

View File

@@ -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);

View File

@@ -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 {