| // SPDX-License-Identifier: GPL-2.0-only | 
 | /* | 
 |  * Copyright (C) 2024 Mike Snitzer <snitzer@hammerspace.com> | 
 |  * Copyright (C) 2024 NeilBrown <neilb@suse.de> | 
 |  */ | 
 |  | 
 | #include <linux/module.h> | 
 | #include <linux/list.h> | 
 | #include <linux/nfslocalio.h> | 
 | #include <linux/nfs3.h> | 
 | #include <linux/nfs4.h> | 
 | #include <linux/nfs_fs.h> | 
 | #include <net/netns/generic.h> | 
 |  | 
 | #include "localio_trace.h" | 
 |  | 
 | MODULE_LICENSE("GPL"); | 
 | MODULE_DESCRIPTION("NFS localio protocol bypass support"); | 
 |  | 
 | static DEFINE_SPINLOCK(nfs_uuids_lock); | 
 |  | 
 | /* | 
 |  * Global list of nfs_uuid_t instances | 
 |  * that is protected by nfs_uuids_lock. | 
 |  */ | 
 | static LIST_HEAD(nfs_uuids); | 
 |  | 
 | /* | 
 |  * Lock ordering: | 
 |  * 1: nfs_uuid->lock | 
 |  * 2: nfs_uuids_lock | 
 |  * 3: nfs_uuid->list_lock (aka nn->local_clients_lock) | 
 |  * | 
 |  * May skip locks in select cases, but never hold multiple | 
 |  * locks out of order. | 
 |  */ | 
 |  | 
 | void nfs_uuid_init(nfs_uuid_t *nfs_uuid) | 
 | { | 
 | 	RCU_INIT_POINTER(nfs_uuid->net, NULL); | 
 | 	nfs_uuid->dom = NULL; | 
 | 	nfs_uuid->list_lock = NULL; | 
 | 	INIT_LIST_HEAD(&nfs_uuid->list); | 
 | 	INIT_LIST_HEAD(&nfs_uuid->files); | 
 | 	spin_lock_init(&nfs_uuid->lock); | 
 | 	nfs_uuid->nfs3_localio_probe_count = 0; | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_uuid_init); | 
 |  | 
 | bool nfs_uuid_begin(nfs_uuid_t *nfs_uuid) | 
 | { | 
 | 	spin_lock(&nfs_uuid->lock); | 
 | 	if (rcu_access_pointer(nfs_uuid->net)) { | 
 | 		/* This nfs_uuid is already in use */ | 
 | 		spin_unlock(&nfs_uuid->lock); | 
 | 		return false; | 
 | 	} | 
 |  | 
 | 	spin_lock(&nfs_uuids_lock); | 
 | 	if (!list_empty(&nfs_uuid->list)) { | 
 | 		/* This nfs_uuid is already in use */ | 
 | 		spin_unlock(&nfs_uuids_lock); | 
 | 		spin_unlock(&nfs_uuid->lock); | 
 | 		return false; | 
 | 	} | 
 | 	list_add_tail(&nfs_uuid->list, &nfs_uuids); | 
 | 	spin_unlock(&nfs_uuids_lock); | 
 |  | 
 | 	uuid_gen(&nfs_uuid->uuid); | 
 | 	spin_unlock(&nfs_uuid->lock); | 
 |  | 
 | 	return true; | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_uuid_begin); | 
 |  | 
 | void nfs_uuid_end(nfs_uuid_t *nfs_uuid) | 
 | { | 
 | 	if (!rcu_access_pointer(nfs_uuid->net)) { | 
 | 		spin_lock(&nfs_uuid->lock); | 
 | 		if (!rcu_access_pointer(nfs_uuid->net)) { | 
 | 			/* Not local, remove from nfs_uuids */ | 
 | 			spin_lock(&nfs_uuids_lock); | 
 | 			list_del_init(&nfs_uuid->list); | 
 | 			spin_unlock(&nfs_uuids_lock); | 
 | 		} | 
 | 		spin_unlock(&nfs_uuid->lock); | 
 |         } | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_uuid_end); | 
 |  | 
 | static nfs_uuid_t * nfs_uuid_lookup_locked(const uuid_t *uuid) | 
 | { | 
 | 	nfs_uuid_t *nfs_uuid; | 
 |  | 
 | 	list_for_each_entry(nfs_uuid, &nfs_uuids, list) | 
 | 		if (uuid_equal(&nfs_uuid->uuid, uuid)) | 
 | 			return nfs_uuid; | 
 |  | 
 | 	return NULL; | 
 | } | 
 |  | 
 | static struct module *nfsd_mod; | 
 |  | 
 | void nfs_uuid_is_local(const uuid_t *uuid, struct list_head *list, | 
 | 		       spinlock_t *list_lock, struct net *net, | 
 | 		       struct auth_domain *dom, struct module *mod) | 
 | { | 
 | 	nfs_uuid_t *nfs_uuid; | 
 |  | 
 | 	spin_lock(&nfs_uuids_lock); | 
 | 	nfs_uuid = nfs_uuid_lookup_locked(uuid); | 
 | 	if (!nfs_uuid) { | 
 | 		spin_unlock(&nfs_uuids_lock); | 
 | 		return; | 
 | 	} | 
 |  | 
 | 	/* | 
 | 	 * We don't hold a ref on the net, but instead put | 
 | 	 * ourselves on @list (nn->local_clients) so the net | 
 | 	 * pointer can be invalidated. | 
 | 	 */ | 
 | 	spin_lock(list_lock); /* list_lock is nn->local_clients_lock */ | 
 | 	list_move(&nfs_uuid->list, list); | 
 | 	spin_unlock(list_lock); | 
 |  | 
 | 	spin_unlock(&nfs_uuids_lock); | 
 | 	/* Once nfs_uuid is parented to @list, avoid global nfs_uuids_lock */ | 
 | 	spin_lock(&nfs_uuid->lock); | 
 |  | 
 | 	__module_get(mod); | 
 | 	nfsd_mod = mod; | 
 |  | 
 | 	nfs_uuid->list_lock = list_lock; | 
 | 	kref_get(&dom->ref); | 
 | 	nfs_uuid->dom = dom; | 
 | 	rcu_assign_pointer(nfs_uuid->net, net); | 
 | 	spin_unlock(&nfs_uuid->lock); | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_uuid_is_local); | 
 |  | 
 | void nfs_localio_enable_client(struct nfs_client *clp) | 
 | { | 
 | 	/* nfs_uuid_is_local() does the actual enablement */ | 
 | 	trace_nfs_localio_enable_client(clp); | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_localio_enable_client); | 
 |  | 
 | /* | 
 |  * Cleanup the nfs_uuid_t embedded in an nfs_client. | 
 |  * This is the long-form of nfs_uuid_init(). | 
 |  */ | 
 | static bool nfs_uuid_put(nfs_uuid_t *nfs_uuid) | 
 | { | 
 | 	LIST_HEAD(local_files); | 
 | 	struct nfs_file_localio *nfl, *tmp; | 
 |  | 
 | 	spin_lock(&nfs_uuid->lock); | 
 | 	if (unlikely(!rcu_access_pointer(nfs_uuid->net))) { | 
 | 		spin_unlock(&nfs_uuid->lock); | 
 | 		return false; | 
 | 	} | 
 | 	RCU_INIT_POINTER(nfs_uuid->net, NULL); | 
 |  | 
 | 	if (nfs_uuid->dom) { | 
 | 		auth_domain_put(nfs_uuid->dom); | 
 | 		nfs_uuid->dom = NULL; | 
 | 	} | 
 |  | 
 | 	list_splice_init(&nfs_uuid->files, &local_files); | 
 | 	spin_unlock(&nfs_uuid->lock); | 
 |  | 
 | 	/* Walk list of files and ensure their last references dropped */ | 
 | 	list_for_each_entry_safe(nfl, tmp, &local_files, list) { | 
 | 		nfs_close_local_fh(nfl); | 
 | 		cond_resched(); | 
 | 	} | 
 |  | 
 | 	spin_lock(&nfs_uuid->lock); | 
 | 	BUG_ON(!list_empty(&nfs_uuid->files)); | 
 |  | 
 | 	/* Remove client from nn->local_clients */ | 
 | 	if (nfs_uuid->list_lock) { | 
 | 		spin_lock(nfs_uuid->list_lock); | 
 | 		BUG_ON(list_empty(&nfs_uuid->list)); | 
 | 		list_del_init(&nfs_uuid->list); | 
 | 		spin_unlock(nfs_uuid->list_lock); | 
 | 		nfs_uuid->list_lock = NULL; | 
 | 	} | 
 |  | 
 | 	module_put(nfsd_mod); | 
 | 	spin_unlock(&nfs_uuid->lock); | 
 |  | 
 | 	return true; | 
 | } | 
 |  | 
 | void nfs_localio_disable_client(struct nfs_client *clp) | 
 | { | 
 | 	if (nfs_uuid_put(&clp->cl_uuid)) | 
 | 		trace_nfs_localio_disable_client(clp); | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_localio_disable_client); | 
 |  | 
 | void nfs_localio_invalidate_clients(struct list_head *nn_local_clients, | 
 | 				    spinlock_t *nn_local_clients_lock) | 
 | { | 
 | 	LIST_HEAD(local_clients); | 
 | 	nfs_uuid_t *nfs_uuid, *tmp; | 
 | 	struct nfs_client *clp; | 
 |  | 
 | 	spin_lock(nn_local_clients_lock); | 
 | 	list_splice_init(nn_local_clients, &local_clients); | 
 | 	spin_unlock(nn_local_clients_lock); | 
 | 	list_for_each_entry_safe(nfs_uuid, tmp, &local_clients, list) { | 
 | 		if (WARN_ON(nfs_uuid->list_lock != nn_local_clients_lock)) | 
 | 			break; | 
 | 		clp = container_of(nfs_uuid, struct nfs_client, cl_uuid); | 
 | 		nfs_localio_disable_client(clp); | 
 | 	} | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_localio_invalidate_clients); | 
 |  | 
 | static void nfs_uuid_add_file(nfs_uuid_t *nfs_uuid, struct nfs_file_localio *nfl) | 
 | { | 
 | 	/* Add nfl to nfs_uuid->files if it isn't already */ | 
 | 	spin_lock(&nfs_uuid->lock); | 
 | 	if (list_empty(&nfl->list)) { | 
 | 		rcu_assign_pointer(nfl->nfs_uuid, nfs_uuid); | 
 | 		list_add_tail(&nfl->list, &nfs_uuid->files); | 
 | 	} | 
 | 	spin_unlock(&nfs_uuid->lock); | 
 | } | 
 |  | 
 | /* | 
 |  * Caller is responsible for calling nfsd_net_put and | 
 |  * nfsd_file_put (via nfs_to_nfsd_file_put_local). | 
 |  */ | 
 | struct nfsd_file *nfs_open_local_fh(nfs_uuid_t *uuid, | 
 | 		   struct rpc_clnt *rpc_clnt, const struct cred *cred, | 
 | 		   const struct nfs_fh *nfs_fh, struct nfs_file_localio *nfl, | 
 | 		   const fmode_t fmode) | 
 | { | 
 | 	struct net *net; | 
 | 	struct nfsd_file *localio; | 
 |  | 
 | 	/* | 
 | 	 * Not running in nfsd context, so must safely get reference on nfsd_serv. | 
 | 	 * But the server may already be shutting down, if so disallow new localio. | 
 | 	 * uuid->net is NOT a counted reference, but rcu_read_lock() ensures that | 
 | 	 * if uuid->net is not NULL, then calling nfsd_net_try_get() is safe | 
 | 	 * and if it succeeds we will have an implied reference to the net. | 
 | 	 * | 
 | 	 * Otherwise NFS may not have ref on NFSD and therefore cannot safely | 
 | 	 * make 'nfs_to' calls. | 
 | 	 */ | 
 | 	rcu_read_lock(); | 
 | 	net = rcu_dereference(uuid->net); | 
 | 	if (!net || !nfs_to->nfsd_net_try_get(net)) { | 
 | 		rcu_read_unlock(); | 
 | 		return ERR_PTR(-ENXIO); | 
 | 	} | 
 | 	rcu_read_unlock(); | 
 | 	/* We have an implied reference to net thanks to nfsd_net_try_get */ | 
 | 	localio = nfs_to->nfsd_open_local_fh(net, uuid->dom, rpc_clnt, | 
 | 					     cred, nfs_fh, fmode); | 
 | 	if (IS_ERR(localio)) | 
 | 		nfs_to_nfsd_net_put(net); | 
 | 	else | 
 | 		nfs_uuid_add_file(uuid, nfl); | 
 |  | 
 | 	return localio; | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_open_local_fh); | 
 |  | 
 | void nfs_close_local_fh(struct nfs_file_localio *nfl) | 
 | { | 
 | 	struct nfsd_file *ro_nf = NULL; | 
 | 	struct nfsd_file *rw_nf = NULL; | 
 | 	nfs_uuid_t *nfs_uuid; | 
 |  | 
 | 	rcu_read_lock(); | 
 | 	nfs_uuid = rcu_dereference(nfl->nfs_uuid); | 
 | 	if (!nfs_uuid) { | 
 | 		/* regular (non-LOCALIO) NFS will hammer this */ | 
 | 		rcu_read_unlock(); | 
 | 		return; | 
 | 	} | 
 |  | 
 | 	ro_nf = rcu_access_pointer(nfl->ro_file); | 
 | 	rw_nf = rcu_access_pointer(nfl->rw_file); | 
 | 	if (ro_nf || rw_nf) { | 
 | 		spin_lock(&nfs_uuid->lock); | 
 | 		if (ro_nf) | 
 | 			ro_nf = rcu_dereference_protected(xchg(&nfl->ro_file, NULL), 1); | 
 | 		if (rw_nf) | 
 | 			rw_nf = rcu_dereference_protected(xchg(&nfl->rw_file, NULL), 1); | 
 |  | 
 | 		/* Remove nfl from nfs_uuid->files list */ | 
 | 		RCU_INIT_POINTER(nfl->nfs_uuid, NULL); | 
 | 		list_del_init(&nfl->list); | 
 | 		spin_unlock(&nfs_uuid->lock); | 
 | 		rcu_read_unlock(); | 
 |  | 
 | 		if (ro_nf) | 
 | 			nfs_to_nfsd_file_put_local(ro_nf); | 
 | 		if (rw_nf) | 
 | 			nfs_to_nfsd_file_put_local(rw_nf); | 
 | 		return; | 
 | 	} | 
 | 	rcu_read_unlock(); | 
 | } | 
 | EXPORT_SYMBOL_GPL(nfs_close_local_fh); | 
 |  | 
 | /* | 
 |  * The NFS LOCALIO code needs to call into NFSD using various symbols, | 
 |  * but cannot be statically linked, because that will make the NFS | 
 |  * module always depend on the NFSD module. | 
 |  * | 
 |  * 'nfs_to' provides NFS access to NFSD functions needed for LOCALIO, | 
 |  * its lifetime is tightly coupled to the NFSD module and will always | 
 |  * be available to NFS LOCALIO because any successful client<->server | 
 |  * LOCALIO handshake results in a reference on the NFSD module (above), | 
 |  * so NFS implicitly holds a reference to the NFSD module and its | 
 |  * functions in the 'nfs_to' nfsd_localio_operations cannot disappear. | 
 |  * | 
 |  * If the last NFS client using LOCALIO disconnects (and its reference | 
 |  * on NFSD dropped) then NFSD could be unloaded, resulting in 'nfs_to' | 
 |  * functions being invalid pointers. But if NFSD isn't loaded then NFS | 
 |  * will not be able to handshake with NFSD and will have no cause to | 
 |  * try to call 'nfs_to' function pointers. If/when NFSD is reloaded it | 
 |  * will reinitialize the 'nfs_to' function pointers and make LOCALIO | 
 |  * possible. | 
 |  */ | 
 | const struct nfsd_localio_operations *nfs_to; | 
 | EXPORT_SYMBOL_GPL(nfs_to); |