mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2026-08-09 06:14:34 +02:00
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>
387 lines
9.8 KiB
C
387 lines
9.8 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* linux/fs/nfs/namespace.c
|
|
*
|
|
* Copyright (C) 2005 Trond Myklebust <Trond.Myklebust@netapp.com>
|
|
* - Modified by David Howells <dhowells@redhat.com>
|
|
*
|
|
* NFS namespace
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/dcache.h>
|
|
#include <linux/gfp.h>
|
|
#include <linux/mount.h>
|
|
#include <linux/namei.h>
|
|
#include <linux/nfs_fs.h>
|
|
#include <linux/string.h>
|
|
#include <linux/sunrpc/clnt.h>
|
|
#include <linux/vfs.h>
|
|
#include <linux/sunrpc/gss_api.h>
|
|
#include "internal.h"
|
|
#include "nfs.h"
|
|
|
|
#define NFSDBG_FACILITY NFSDBG_VFS
|
|
|
|
static void nfs_expire_automounts(struct work_struct *work);
|
|
|
|
static LIST_HEAD(nfs_automount_list);
|
|
static DECLARE_DELAYED_WORK(nfs_automount_task, nfs_expire_automounts);
|
|
int nfs_mountpoint_expiry_timeout = 500 * HZ;
|
|
|
|
/*
|
|
* nfs_path - reconstruct the path given an arbitrary dentry
|
|
* @base - used to return pointer to the end of devname part of path
|
|
* @dentry_in - pointer to dentry
|
|
* @buffer - result buffer
|
|
* @buflen_in - length of buffer
|
|
* @flags - options (see below)
|
|
*
|
|
* Helper function for constructing the server pathname
|
|
* by arbitrary hashed dentry.
|
|
*
|
|
* This is mainly for use in figuring out the path on the
|
|
* server side when automounting on top of an existing partition
|
|
* and in generating /proc/mounts and friends.
|
|
*
|
|
* Supported flags:
|
|
* NFS_PATH_CANONICAL: ensure there is exactly one slash after
|
|
* the original device (export) name
|
|
* (if unset, the original name is returned verbatim)
|
|
*/
|
|
char *nfs_path(char **p, struct dentry *dentry_in, char *buffer,
|
|
ssize_t buflen_in, unsigned flags)
|
|
{
|
|
char *end;
|
|
int namelen;
|
|
unsigned seq;
|
|
const char *base;
|
|
struct dentry *dentry;
|
|
ssize_t buflen;
|
|
|
|
rename_retry:
|
|
buflen = buflen_in;
|
|
dentry = dentry_in;
|
|
end = buffer+buflen;
|
|
*--end = '\0';
|
|
buflen--;
|
|
|
|
seq = read_seqbegin(&rename_lock);
|
|
rcu_read_lock();
|
|
while (1) {
|
|
spin_lock(&dentry->d_lock);
|
|
if (IS_ROOT(dentry))
|
|
break;
|
|
namelen = dentry->d_name.len;
|
|
buflen -= namelen + 1;
|
|
if (buflen < 0)
|
|
goto Elong_unlock;
|
|
end -= namelen;
|
|
memcpy(end, dentry->d_name.name, namelen);
|
|
*--end = '/';
|
|
spin_unlock(&dentry->d_lock);
|
|
dentry = dentry->d_parent;
|
|
}
|
|
if (read_seqretry(&rename_lock, seq)) {
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
goto rename_retry;
|
|
}
|
|
if ((flags & NFS_PATH_CANONICAL) && *end != '/') {
|
|
if (--buflen < 0) {
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
goto Elong;
|
|
}
|
|
*--end = '/';
|
|
}
|
|
*p = end;
|
|
base = dentry->d_fsdata;
|
|
if (!base) {
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
WARN_ON(1);
|
|
return end;
|
|
}
|
|
namelen = strlen(base);
|
|
if (*end == '/') {
|
|
/* Strip off excess slashes in base string */
|
|
while (namelen > 0 && base[namelen - 1] == '/')
|
|
namelen--;
|
|
}
|
|
buflen -= namelen;
|
|
if (buflen < 0) {
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
goto Elong;
|
|
}
|
|
end -= namelen;
|
|
memcpy(end, base, namelen);
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
return end;
|
|
Elong_unlock:
|
|
spin_unlock(&dentry->d_lock);
|
|
rcu_read_unlock();
|
|
if (read_seqretry(&rename_lock, seq))
|
|
goto rename_retry;
|
|
Elong:
|
|
return ERR_PTR(-ENAMETOOLONG);
|
|
}
|
|
EXPORT_SYMBOL_GPL(nfs_path);
|
|
|
|
/*
|
|
* nfs_d_automount - Handle crossing a mountpoint on the server
|
|
* @path - The mountpoint
|
|
*
|
|
* When we encounter a mountpoint on the server, we want to set up
|
|
* a mountpoint on the client too, to prevent inode numbers from
|
|
* colliding, and to allow "df" to work properly.
|
|
* On NFSv4, we also want to allow for the fact that different
|
|
* filesystems may be migrated to different servers in a failover
|
|
* situation, and that different filesystems may want to use
|
|
* different security flavours.
|
|
*/
|
|
struct vfsmount *nfs_d_automount(struct path *path)
|
|
{
|
|
struct nfs_fs_context *ctx;
|
|
struct fs_context *fc;
|
|
struct vfsmount *mnt = ERR_PTR(-ENOMEM);
|
|
struct nfs_server *server = NFS_SB(path->dentry->d_sb);
|
|
struct nfs_client *client = server->nfs_client;
|
|
unsigned long s_flags = path->dentry->d_sb->s_flags;
|
|
int timeout = READ_ONCE(nfs_mountpoint_expiry_timeout);
|
|
int ret;
|
|
|
|
if (IS_ROOT(path->dentry))
|
|
return ERR_PTR(-ESTALE);
|
|
|
|
/* Open a new filesystem context, transferring parameters from the
|
|
* parent superblock, including the network namespace.
|
|
*/
|
|
fc = fs_context_for_submount(path->mnt->mnt_sb->s_type, path->dentry);
|
|
if (IS_ERR(fc))
|
|
return ERR_CAST(fc);
|
|
|
|
ctx = nfs_fc2context(fc);
|
|
ctx->clone_data.dentry = path->dentry;
|
|
ctx->clone_data.sb = path->dentry->d_sb;
|
|
ctx->clone_data.fattr = nfs_alloc_fattr();
|
|
if (!ctx->clone_data.fattr)
|
|
goto out_fc;
|
|
|
|
if (fc->cred != server->cred) {
|
|
put_cred(fc->cred);
|
|
fc->cred = get_cred(server->cred);
|
|
}
|
|
|
|
if (fc->net_ns != client->cl_net) {
|
|
put_net(fc->net_ns);
|
|
fc->net_ns = get_net(client->cl_net);
|
|
}
|
|
|
|
/* Inherit the flags covered by NFS_SB_MASK */
|
|
fc->sb_flags_mask |= NFS_SB_MASK;
|
|
fc->sb_flags &= ~NFS_SB_MASK;
|
|
fc->sb_flags |= s_flags & NFS_SB_MASK;
|
|
|
|
/* for submounts we want the same server; referrals will reassign */
|
|
memcpy(&ctx->nfs_server._address, &client->cl_addr, client->cl_addrlen);
|
|
ctx->nfs_server.addrlen = client->cl_addrlen;
|
|
ctx->nfs_server.port = server->port;
|
|
|
|
ctx->version = client->rpc_ops->version;
|
|
ctx->minorversion = client->cl_minorversion;
|
|
ctx->nfs_mod = client->cl_nfs_mod;
|
|
get_nfs_version(ctx->nfs_mod);
|
|
|
|
/* Inherit block sizes if they were specified as mount parameters */
|
|
if (server->automount_inherit & NFS_AUTOMOUNT_INHERIT_BSIZE)
|
|
ctx->bsize = server->bsize;
|
|
|
|
ret = client->rpc_ops->submount(fc, server);
|
|
if (ret < 0) {
|
|
mnt = ERR_PTR(ret);
|
|
goto out_fc;
|
|
}
|
|
|
|
up_write(&fc->root->d_sb->s_umount);
|
|
mnt = vfs_create_mount(fc);
|
|
if (IS_ERR(mnt))
|
|
goto out_fc;
|
|
|
|
if (timeout <= 0)
|
|
goto out_fc;
|
|
|
|
mnt_set_expiry(mnt, &nfs_automount_list);
|
|
schedule_delayed_work(&nfs_automount_task, timeout);
|
|
|
|
out_fc:
|
|
put_fs_context(fc);
|
|
return mnt;
|
|
}
|
|
|
|
static int
|
|
nfs_namespace_getattr(struct mnt_idmap *idmap,
|
|
const struct path *path, struct kstat *stat,
|
|
u32 request_mask, unsigned int query_flags)
|
|
{
|
|
if (NFS_FH(d_inode(path->dentry))->size != 0)
|
|
return nfs_getattr(idmap, path, stat, request_mask,
|
|
query_flags);
|
|
generic_fillattr(&nop_mnt_idmap, request_mask, d_inode(path->dentry),
|
|
stat);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
nfs_namespace_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
|
|
struct iattr *attr)
|
|
{
|
|
if (NFS_FH(d_inode(dentry))->size != 0)
|
|
return nfs_setattr(idmap, dentry, attr);
|
|
return -EACCES;
|
|
}
|
|
|
|
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)
|
|
{
|
|
struct list_head *list = &nfs_automount_list;
|
|
int timeout = READ_ONCE(nfs_mountpoint_expiry_timeout);
|
|
|
|
mark_mounts_for_expiry(list);
|
|
if (!list_empty(list) && timeout > 0)
|
|
schedule_delayed_work(&nfs_automount_task, timeout);
|
|
}
|
|
|
|
void nfs_release_automount_timer(void)
|
|
{
|
|
if (list_empty(&nfs_automount_list))
|
|
cancel_delayed_work(&nfs_automount_task);
|
|
}
|
|
|
|
/**
|
|
* nfs_do_submount - set up mountpoint when crossing a filesystem boundary
|
|
* @fc: pointer to struct nfs_fs_context
|
|
*
|
|
*/
|
|
int nfs_do_submount(struct fs_context *fc)
|
|
{
|
|
struct nfs_fs_context *ctx = nfs_fc2context(fc);
|
|
struct dentry *dentry = ctx->clone_data.dentry;
|
|
struct nfs_server *server;
|
|
char *buffer, *p;
|
|
int ret;
|
|
|
|
/* create a new volume representation */
|
|
server = ctx->nfs_mod->rpc_ops->clone_server(NFS_SB(ctx->clone_data.sb),
|
|
ctx->mntfh,
|
|
ctx->clone_data.fattr,
|
|
ctx->selected_flavor);
|
|
|
|
if (IS_ERR(server))
|
|
return PTR_ERR(server);
|
|
|
|
ctx->server = server;
|
|
|
|
buffer = kmalloc(4096, GFP_USER);
|
|
if (!buffer)
|
|
return -ENOMEM;
|
|
|
|
ctx->internal = true;
|
|
|
|
p = nfs_devname(dentry, buffer, 4096);
|
|
if (IS_ERR(p)) {
|
|
nfs_errorf(fc, "NFS: Couldn't determine submount pathname");
|
|
ret = PTR_ERR(p);
|
|
} else {
|
|
ret = vfs_parse_fs_qstr(fc, "source",
|
|
&QSTR_LEN(p, buffer + 4096 - p));
|
|
if (!ret)
|
|
ret = vfs_get_tree(fc);
|
|
}
|
|
kfree(buffer);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(nfs_do_submount);
|
|
|
|
int nfs_submount(struct fs_context *fc, struct nfs_server *server)
|
|
{
|
|
struct nfs_fs_context *ctx = nfs_fc2context(fc);
|
|
struct dentry *dentry = ctx->clone_data.dentry;
|
|
struct dentry *parent = dget_parent(dentry);
|
|
int err;
|
|
|
|
/* Look it up again to get its attributes */
|
|
err = server->nfs_client->rpc_ops->lookup(d_inode(parent), dentry, &dentry->d_name,
|
|
ctx->mntfh, ctx->clone_data.fattr);
|
|
dput(parent);
|
|
if (err != 0)
|
|
return err;
|
|
|
|
ctx->selected_flavor = server->client->cl_auth->au_flavor;
|
|
return nfs_do_submount(fc);
|
|
}
|
|
EXPORT_SYMBOL_GPL(nfs_submount);
|
|
|
|
static int param_set_nfs_timeout(const char *val, const struct kernel_param *kp)
|
|
{
|
|
long num;
|
|
int ret;
|
|
|
|
if (!val)
|
|
return -EINVAL;
|
|
ret = kstrtol(val, 0, &num);
|
|
if (ret)
|
|
return -EINVAL;
|
|
if (num > 0) {
|
|
if (num >= INT_MAX / HZ)
|
|
num = INT_MAX;
|
|
else
|
|
num *= HZ;
|
|
*((int *)kp->arg) = num;
|
|
if (!list_empty(&nfs_automount_list))
|
|
mod_delayed_work(system_percpu_wq, &nfs_automount_task, num);
|
|
} else {
|
|
*((int *)kp->arg) = -1*HZ;
|
|
cancel_delayed_work(&nfs_automount_task);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int param_get_nfs_timeout(char *buffer, const struct kernel_param *kp)
|
|
{
|
|
long num = *((int *)kp->arg);
|
|
|
|
if (num > 0) {
|
|
if (num >= INT_MAX - (HZ - 1))
|
|
num = INT_MAX / HZ;
|
|
else
|
|
num = (num + (HZ - 1)) / HZ;
|
|
} else
|
|
num = -1;
|
|
return sysfs_emit(buffer, "%li\n", num);
|
|
}
|
|
|
|
static const struct kernel_param_ops param_ops_nfs_timeout = {
|
|
.set = param_set_nfs_timeout,
|
|
.get = param_get_nfs_timeout,
|
|
};
|
|
#define param_check_nfs_timeout(name, p) __param_check(name, p, int)
|
|
|
|
module_param(nfs_mountpoint_expiry_timeout, nfs_timeout, 0644);
|
|
MODULE_PARM_DESC(nfs_mountpoint_expiry_timeout,
|
|
"Set the NFS automounted mountpoint timeout value (seconds)."
|
|
"Values <= 0 turn expiration off.");
|