blob: 03c7735c7cb4ce31839557411e9ad8102884ec88 [file] [log] [blame]
#define MSNFS /* HACK HACK */
/*
* linux/fs/nfsd/export.c
*
* NFS exporting and validation.
*
* We maintain a list of clients, each of which has a list of
* exports. To export an fs to a given client, you first have
* to create the client entry with NFSCTL_ADDCLIENT, which
* creates a client control block and adds it to the hash
* table. Then, you call NFSCTL_EXPORT for each fs.
*
*
* Copyright (C) 1995, 1996 Olaf Kirch, <okir@monad.swb.de>
*/
#include <linux/unistd.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/stat.h>
#include <linux/in.h>
#include <linux/seq_file.h>
#include <linux/sunrpc/svc.h>
#include <linux/nfsd/nfsd.h>
#include <linux/nfsd/nfsfh.h>
#include <linux/nfsd/syscall.h>
#include <linux/lockd/bind.h>
#define NFSDDBG_FACILITY NFSDDBG_EXPORT
#define NFSD_PARANOIA 1
typedef struct svc_client svc_client;
typedef struct svc_export svc_export;
static svc_export * exp_parent(svc_client *clp, struct super_block *sb,
struct dentry *dentry);
static svc_export * exp_child(svc_client *clp, struct super_block *sb,
struct dentry *dentry);
static void exp_unexport_all(svc_client *clp);
static void exp_do_unexport(svc_export *unexp);
static svc_client * exp_getclientbyname(char *name);
static void exp_freeclient(svc_client *clp);
static void exp_unhashclient(svc_client *clp);
static int exp_verify_string(char *cp, int max);
#define CLIENT_HASHBITS 6
#define CLIENT_HASHMAX (1 << CLIENT_HASHBITS)
#define CLIENT_HASHMASK (CLIENT_HASHMAX - 1)
#define CLIENT_HASH(a) \
((((a)>>24) ^ ((a)>>16) ^ ((a)>>8) ^(a)) & CLIENT_HASHMASK)
/* XXX: is this adequate for 32bit kdev_t ? */
#define EXPORT_HASH(dev) (minor(dev) & (NFSCLNT_EXPMAX - 1))
struct svc_clnthash {
struct svc_clnthash * h_next;
struct in_addr h_addr;
struct svc_client * h_client;
};
static struct svc_clnthash * clnt_hash[CLIENT_HASHMAX];
static svc_client * clients;
static int initialized;
static int hash_lock;
static int want_lock;
static int hash_count;
static DECLARE_WAIT_QUEUE_HEAD( hash_wait );
/*
* Find the client's export entry matching xdev/xino.
*/
svc_export *
exp_get(svc_client *clp, kdev_t dev, ino_t ino)
{
struct list_head *head, *p;
svc_export *exp = NULL;
if (!clp)
return NULL;
head = &clp->cl_export[EXPORT_HASH(dev)];
list_for_each(p, head) {
exp = list_entry(p, svc_export, ex_hash);
if (exp->ex_ino == ino && kdev_same(exp->ex_dev, dev))
break;
}
return exp;
}
svc_export *
exp_get_by_name(svc_client *clp, struct vfsmount *mnt, struct dentry *dentry)
{
struct list_head *head, *p;
int hash = EXPORT_HASH(mnt->mnt_sb->s_dev);
svc_export *exp = NULL;
if (!clp)
return NULL;
head = &clp->cl_export[hash];
list_for_each(p, head) {
exp = list_entry(p, svc_export, ex_hash);
if (exp->ex_dentry == dentry && exp->ex_mnt == mnt)
break;
}
return exp;
}
/*
* Find the export entry for a given dentry. <gam3@acm.org>
*/
static svc_export *
exp_parent(svc_client *clp, struct super_block *sb, struct dentry *dentry)
{
struct list_head *head = &clp->cl_export[EXPORT_HASH(sb->s_dev)];
struct list_head *p;
svc_export *exp = NULL;
list_for_each(p, head) {
exp = list_entry(p, svc_export, ex_hash);
if (is_subdir(dentry, exp->ex_dentry))
break;
}
return exp;
}
/*
* Find the child export entry for a given fs. This function is used
* only by the export syscall to keep the export tree consistent.
* <gam3@acm.org>
*/
static svc_export *
exp_child(svc_client *clp, struct super_block *sb, struct dentry *dentry)
{
struct list_head *head = &clp->cl_export[EXPORT_HASH(sb->s_dev)];
struct list_head *p;
svc_export *exp = NULL;
struct dentry *ndentry;
list_for_each(p, head) {
exp = list_entry(p, svc_export, ex_hash);
ndentry = exp->ex_dentry;
if (ndentry && is_subdir(ndentry->d_parent, dentry))
break;
}
return exp;
}
/* Update parent pointers of all exports */
static void exp_change_parents(svc_client *clp, svc_export *old, svc_export *new)
{
struct list_head *head = &clp->cl_list;
struct list_head *p;
list_for_each(p, head) {
svc_export *exp = list_entry(p, svc_export, ex_list);
if (exp->ex_parent == old)
exp->ex_parent = new;
}
}
/*
* Export a file system.
*/
int
exp_export(struct nfsctl_export *nxp)
{
svc_client *clp;
svc_export *exp, *parent;
struct nameidata nd;
struct inode *inode = NULL;
int err;
kdev_t dev;
ino_t ino;
/* Consistency check */
err = -EINVAL;
if (!exp_verify_string(nxp->ex_path, NFS_MAXPATHLEN) ||
!exp_verify_string(nxp->ex_client, NFSCLNT_IDMAX))
goto out;
dprintk("exp_export called for %s:%s (%x/%ld fl %x).\n",
nxp->ex_client, nxp->ex_path,
nxp->ex_dev, (long) nxp->ex_ino, nxp->ex_flags);
dev = to_kdev_t(nxp->ex_dev);
ino = nxp->ex_ino;
/* Try to lock the export table for update */
if ((err = exp_writelock()) < 0)
goto out;
/* Look up client info */
err = -EINVAL;
if (!(clp = exp_getclientbyname(nxp->ex_client)))
goto out_unlock;
/*
* If there's already an export for this file, assume this
* is just a flag update.
*/
if ((exp = exp_get(clp, dev, ino)) != NULL) {
exp->ex_flags = nxp->ex_flags;
exp->ex_anon_uid = nxp->ex_anon_uid;
exp->ex_anon_gid = nxp->ex_anon_gid;
err = 0;
goto out_unlock;
}
/* Look up the dentry */
err = 0;
if (path_init(nxp->ex_path, LOOKUP_POSITIVE, &nd))
err = path_walk(nxp->ex_path, &nd);
if (err)
goto out_unlock;
inode = nd.dentry->d_inode;
err = -EINVAL;
if (!kdev_same(inode->i_dev, dev) || inode->i_ino != nxp->ex_ino) {
printk(KERN_DEBUG "exp_export: i_dev = %02x:%02x, dev = %02x:%02x\n",
major(inode->i_dev), minor(inode->i_dev),
major(dev), minor(dev));
/* I'm just being paranoid... */
goto finish;
}
/* We currently export only dirs and regular files.
* This is what umountd does.
*/
err = -ENOTDIR;
if (!S_ISDIR(inode->i_mode) && !S_ISREG(inode->i_mode))
goto finish;
err = -EINVAL;
if (!(inode->i_sb->s_type->fs_flags & FS_REQUIRES_DEV) ||
(inode->i_sb->s_op->read_inode == NULL
&& inode->i_sb->s_op->fh_to_dentry == NULL)) {
dprintk("exp_export: export of invalid fs type.\n");
goto finish;
}
if ((parent = exp_child(clp, inode->i_sb, nd.dentry)) != NULL) {
dprintk("exp_export: export not valid (Rule 3).\n");
goto finish;
}
/* Is this is a sub-export, must be a proper subset of FS */
if ((parent = exp_parent(clp, inode->i_sb, nd.dentry)) != NULL) {
dprintk("exp_export: sub-export not valid (Rule 2).\n");
goto finish;
}
err = -ENOMEM;
if (!(exp = kmalloc(sizeof(*exp), GFP_USER)))
goto finish;
dprintk("nfsd: created export entry %p for client %p\n", exp, clp);
strcpy(exp->ex_path, nxp->ex_path);
exp->ex_client = clp;
exp->ex_parent = parent;
exp->ex_dentry = nd.dentry;
exp->ex_mnt = nd.mnt;
exp->ex_flags = nxp->ex_flags;
exp->ex_dev = dev;
exp->ex_ino = ino;
exp->ex_anon_uid = nxp->ex_anon_uid;
exp->ex_anon_gid = nxp->ex_anon_gid;
/* Update parent pointers of all exports */
if (parent)
exp_change_parents(clp, parent, exp);
list_add(&exp->ex_hash, clp->cl_export + EXPORT_HASH(dev));
list_add_tail(&exp->ex_list, &clp->cl_list);
err = 0;
/* Unlock hashtable */
out_unlock:
exp_unlock();
out:
return err;
/* Release the dentry */
finish:
path_release(&nd);
goto out_unlock;
}
/*
* Unexport a file system. The export entry has already
* been removed from the client's list of exported fs's.
*/
static void
exp_do_unexport(svc_export *unexp)
{
struct dentry *dentry;
struct vfsmount *mnt;
struct inode *inode;
/* Update parent pointers. */
exp_change_parents(unexp->ex_client, unexp, unexp->ex_parent);
dentry = unexp->ex_dentry;
mnt = unexp->ex_mnt;
inode = dentry->d_inode;
if (!kdev_same(unexp->ex_dev, inode->i_dev) || unexp->ex_ino != inode->i_ino)
printk(KERN_WARNING "nfsd: bad dentry in unexport!\n");
dput(dentry);
mntput(mnt);
kfree(unexp);
}
/*
* Revoke all exports for a given client.
* This may look very awkward, but we have to do it this way in order
* to avoid race conditions (aka mind the parent pointer).
*/
static void
exp_unexport_all(svc_client *clp)
{
struct list_head *p = &clp->cl_list;
dprintk("unexporting all fs's for clnt %p\n", clp);
while (!list_empty(p)) {
svc_export *exp = list_entry(p->next, svc_export, ex_list);
list_del(&exp->ex_list);
list_del(&exp->ex_hash);
exp_do_unexport(exp);
}
}
/*
* unexport syscall.
*/
int
exp_unexport(struct nfsctl_export *nxp)
{
svc_client *clp;
int err;
/* Consistency check */
if (!exp_verify_string(nxp->ex_client, NFSCLNT_IDMAX))
return -EINVAL;
if ((err = exp_writelock()) < 0)
goto out;
err = -EINVAL;
clp = exp_getclientbyname(nxp->ex_client);
if (clp) {
kdev_t ex_dev = to_kdev_t(nxp->ex_dev);
svc_export *exp = exp_get(clp, ex_dev, nxp->ex_ino);
if (exp) {
list_del(&exp->ex_hash);
list_del(&exp->ex_list);
exp_do_unexport(exp);
err = 0;
}
}
exp_unlock();
out:
return err;
}
/*
* Obtain the root fh on behalf of a client.
* This could be done in user space, but I feel that it adds some safety
* since its harder to fool a kernel module than a user space program.
*/
int
exp_rootfh(struct svc_client *clp, char *path, struct knfsd_fh *f, int maxsize)
{
struct svc_export *exp;
struct nameidata nd;
struct inode *inode;
struct svc_fh fh;
kdev_t dev;
int err;
err = -EPERM;
/* NB: we probably ought to check that it's NUL-terminated */
if (path_init(path, LOOKUP_POSITIVE, &nd) &&
path_walk(path, &nd)) {
printk("nfsd: exp_rootfh path not found %s", path);
return err;
}
inode = nd.dentry->d_inode;
dev = inode->i_dev;
dprintk("nfsd: exp_rootfh(%s [%p] %s:%02x:%02x/%ld)\n",
path, nd.dentry, clp->cl_ident,
major(dev), minor(dev), (long) inode->i_ino);
exp = exp_parent(clp, inode->i_sb, nd.dentry);
if (!exp) {
dprintk("nfsd: exp_rootfh export not found.\n");
goto out;
}
/*
* fh must be initialized before calling fh_compose
*/
fh_init(&fh, maxsize);
if (fh_compose(&fh, exp, dget(nd.dentry), NULL))
err = -EINVAL;
else
err = 0;
memcpy(f, &fh.fh_handle, sizeof(struct knfsd_fh));
fh_put(&fh);
out:
if (path)
path_release(&nd);
return err;
}
/*
* Hashtable locking. Write locks are placed only by user processes
* wanting to modify export information.
*/
void
exp_readlock(void)
{
while (hash_lock || want_lock)
sleep_on(&hash_wait);
hash_count++;
}
int
exp_writelock(void)
{
/* fast track */
if (!hash_count && !hash_lock) {
lock_it:
hash_lock = 1;
return 0;
}
clear_thread_flag(TIF_SIGPENDING);
want_lock++;
while (hash_count || hash_lock) {
interruptible_sleep_on(&hash_wait);
if (signal_pending(current))
break;
}
want_lock--;
/* restore the task's signals */
spin_lock_irq(&current->sigmask_lock);
recalc_sigpending(current);
spin_unlock_irq(&current->sigmask_lock);
if (!hash_count && !hash_lock)
goto lock_it;
return -EINTR;
}
void
exp_unlock(void)
{
if (!hash_count && !hash_lock)
printk(KERN_WARNING "exp_unlock: not locked!\n");
if (hash_count)
hash_count--;
else
hash_lock = 0;
wake_up(&hash_wait);
}
/*
* Find a valid client given an inet address. We always move the most
* recently used client to the front of the hash chain to speed up
* future lookups.
* Locking against other processes is the responsibility of the caller.
*/
struct svc_client *
exp_getclient(struct sockaddr_in *sin)
{
struct svc_clnthash **hp, **head, *tmp;
unsigned long addr = sin->sin_addr.s_addr;
if (!initialized)
return NULL;
head = &clnt_hash[CLIENT_HASH(addr)];
for (hp = head; (tmp = *hp) != NULL; hp = &(tmp->h_next)) {
if (tmp->h_addr.s_addr == addr) {
/* Move client to the front */
if (head != hp) {
*hp = tmp->h_next;
tmp->h_next = *head;
*head = tmp;
}
return tmp->h_client;
}
}
return NULL;
}
/*
* Find a client given its identifier.
*/
static svc_client *
exp_getclientbyname(char *ident)
{
svc_client * clp;
for (clp = clients; clp; clp = clp->cl_next) {
if (!strcmp(clp->cl_ident, ident))
return clp;
}
return NULL;
}
/* Iterator */
static void *e_start(struct seq_file *m, loff_t *pos)
{
loff_t n = *pos;
unsigned client, export;
svc_client *clp;
struct list_head *p;
exp_readlock();
if (!n--)
return (void *)1;
client = n >> 32;
export = n & ((1LL<<32) - 1);
for (clp = clients; client && clp; clp = clp->cl_next, client--)
;
if (!clp)
return NULL;
list_for_each(p, &clp->cl_list)
if (!export--)
return list_entry(p, svc_export, ex_list);
n &= ~((1LL<<32) - 1);
do {
clp = clp->cl_next;
n += 1LL<<32;
} while(clp && list_empty(&clp->cl_list));
if (!clp)
return NULL;
*pos = n+1;
return list_entry(clp->cl_list.next, svc_export, ex_list);
}
static void *e_next(struct seq_file *m, void *p, loff_t *pos)
{
svc_export *exp = p;
svc_client *clp;
if (p == (void *)1)
clp = clients;
else if (exp->ex_list.next == &exp->ex_client->cl_list)
clp = exp->ex_client->cl_next;
else {
++*pos;
return list_entry(exp->ex_list.next, svc_export, ex_list);
}
*pos &= ~((1LL<<32) - 1);
while (clp && list_empty(&clp->cl_list)) {
clp = clp->cl_next;
*pos += 1LL<<32;
}
if (!clp)
return NULL;
++*pos;
return list_entry(clp->cl_list.next, svc_export, ex_list);
}
static void e_stop(struct seq_file *m, void *p)
{
exp_unlock();
}
struct flags {
int flag;
char *name[2];
} expflags[] = {
{ NFSEXP_READONLY, {"ro", "rw"}},
{ NFSEXP_INSECURE_PORT, {"insecure", ""}},
{ NFSEXP_ROOTSQUASH, {"root_squash", "no_root_squash"}},
{ NFSEXP_ALLSQUASH, {"all_squash", ""}},
{ NFSEXP_ASYNC, {"async", "sync"}},
{ NFSEXP_GATHERED_WRITES, {"wdelay", "no_wdelay"}},
{ NFSEXP_UIDMAP, {"uidmap", ""}},
{ NFSEXP_KERBEROS, { "kerberos", ""}},
{ NFSEXP_SUNSECURE, { "sunsecure", ""}},
{ NFSEXP_CROSSMNT, {"nohide", ""}},
{ NFSEXP_NOSUBTREECHECK, {"no_subtree_check", ""}},
{ NFSEXP_NOAUTHNLM, {"insecure_locks", ""}},
#ifdef MSNFS
{ NFSEXP_MSNFS, {"msnfs", ""}},
#endif
{ 0, {"", ""}}
};
static void exp_flags(struct seq_file *m, int flag)
{
int first = 0;
struct flags *flg;
for (flg = expflags; flg->flag; flg++) {
int state = (flg->flag & flag)?0:1;
if (*flg->name[state])
seq_printf(m, "%s%s", first++?",":"", flg->name[state]);
}
}
static inline void mangle(struct seq_file *m, const char *s)
{
seq_escape(m, s, " \t\n\\");
}
static int e_show(struct seq_file *m, void *p)
{
struct svc_export *exp = p;
struct svc_client *clp;
int j, first = 0;
if (p == (void *)1) {
seq_puts(m, "# Version 1.1\n");
seq_puts(m, "# Path Client(Flags) # IPs\n");
return 0;
}
clp = exp->ex_client;
mangle(m, exp->ex_path);
seq_putc(m, '\t');
mangle(m, clp->cl_ident);
seq_putc(m, '(');
exp_flags(m, exp->ex_flags);
seq_puts(m, ") # ");
for (j = 0; j < clp->cl_naddr; j++) {
struct svc_clnthash **hp, **head, *tmp;
struct in_addr addr = clp->cl_addr[j];
head = &clnt_hash[CLIENT_HASH(addr.s_addr)];
for (hp = head; (tmp = *hp) != NULL; hp = &(tmp->h_next)) {
if (tmp->h_addr.s_addr == addr.s_addr)
break;
}
if (tmp) {
if (first++)
seq_putc(m, ' ');
if (tmp->h_client != clp)
seq_putc(m, '(');
seq_printf(m, "%d.%d.%d.%d",
htonl(addr.s_addr) >> 24 & 0xff,
htonl(addr.s_addr) >> 16 & 0xff,
htonl(addr.s_addr) >> 8 & 0xff,
htonl(addr.s_addr) >> 0 & 0xff);
if (tmp->h_client != clp)
seq_putc(m, ')');
}
}
seq_putc(m, '\n');
return 0;
}
struct seq_operations nfs_exports_op = {
start: e_start,
next: e_next,
stop: e_stop,
show: e_show,
};
/*
* Add or modify a client.
* Change requests may involve the list of host addresses. The list of
* exports and possibly existing uid maps are left untouched.
*/
int
exp_addclient(struct nfsctl_client *ncp)
{
struct svc_clnthash * ch[NFSCLNT_ADDRMAX];
svc_client * clp;
int i, err, change = 0, ilen;
/* First, consistency check. */
err = -EINVAL;
if (!(ilen = exp_verify_string(ncp->cl_ident, NFSCLNT_IDMAX)))
goto out;
if (ncp->cl_naddr > NFSCLNT_ADDRMAX)
goto out;
/* Lock the hashtable */
if ((err = exp_writelock()) < 0)
goto out;
/* First check if this is a change request for a client. */
for (clp = clients; clp; clp = clp->cl_next)
if (!strcmp(clp->cl_ident, ncp->cl_ident))
break;
err = -ENOMEM;
if (clp) {
change = 1;
} else {
if (!(clp = kmalloc(sizeof(*clp), GFP_KERNEL)))
goto out_unlock;
memset(clp, 0, sizeof(*clp));
for (i = 0; i < NFSCLNT_EXPMAX; i++)
INIT_LIST_HEAD(&clp->cl_export[i]);
INIT_LIST_HEAD(&clp->cl_list);
dprintk("created client %s (%p)\n", ncp->cl_ident, clp);
strcpy(clp->cl_ident, ncp->cl_ident);
clp->cl_idlen = ilen;
}
/* Allocate hash buckets */
for (i = 0; i < ncp->cl_naddr; i++) {
ch[i] = kmalloc(sizeof(struct svc_clnthash), GFP_KERNEL);
if (!ch[i]) {
while (i--)
kfree(ch[i]);
if (!change)
kfree(clp);
goto out_unlock;
}
}
/* Copy addresses. */
for (i = 0; i < ncp->cl_naddr; i++) {
clp->cl_addr[i] = ncp->cl_addrlist[i];
}
clp->cl_naddr = ncp->cl_naddr;
/* Remove old client hash entries. */
if (change)
exp_unhashclient(clp);
/* Insert client into hashtable. */
for (i = 0; i < ncp->cl_naddr; i++) {
struct in_addr addr = clp->cl_addr[i];
int hash;
hash = CLIENT_HASH(addr.s_addr);
ch[i]->h_client = clp;
ch[i]->h_addr = addr;
ch[i]->h_next = clnt_hash[hash];
clnt_hash[hash] = ch[i];
}
if (!change) {
clp->cl_next = clients;
clients = clp;
}
err = 0;
out_unlock:
exp_unlock();
out:
return err;
}
/*
* Delete a client given an identifier.
*/
int
exp_delclient(struct nfsctl_client *ncp)
{
svc_client **clpp, *clp;
int err;
err = -EINVAL;
if (!exp_verify_string(ncp->cl_ident, NFSCLNT_IDMAX))
goto out;
/* Lock the hashtable */
if ((err = exp_writelock()) < 0)
goto out;
err = -EINVAL;
for (clpp = &clients; (clp = *clpp); clpp = &(clp->cl_next))
if (!strcmp(ncp->cl_ident, clp->cl_ident))
break;
if (clp) {
*clpp = clp->cl_next;
exp_freeclient(clp);
err = 0;
}
exp_unlock();
out:
return err;
}
/*
* Free a client. The caller has already removed it from the client list.
*/
static void
exp_freeclient(svc_client *clp)
{
exp_unhashclient(clp);
/* umap_free(&(clp->cl_umap)); */
exp_unexport_all(clp);
nfsd_lockd_unexport(clp);
kfree (clp);
}
/*
* Remove client from hashtable. We first collect all hashtable
* entries and free them in one go.
* The hash table must be writelocked by the caller.
*/
static void
exp_unhashclient(svc_client *clp)
{
struct svc_clnthash **hpp, *hp, *ch[NFSCLNT_ADDRMAX];
int i, count, err;
again:
err = 0;
for (i = 0, count = 0; i < CLIENT_HASHMAX && !err; i++) {
hpp = clnt_hash + i;
while ((hp = *hpp) && !err) {
if (hp->h_client == clp) {
*hpp = hp->h_next;
ch[count++] = hp;
err = (count >= NFSCLNT_ADDRMAX);
} else {
hpp = &(hp->h_next);
}
}
}
if (count != clp->cl_naddr)
printk(KERN_WARNING "nfsd: bad address count in freeclient!\n");
if (err)
goto again;
for (i = 0; i < count; i++)
kfree (ch[i]);
}
/*
* Lockd is shutting down and tells us to unregister all clients
*/
void
exp_nlmdetach(void)
{
struct svc_client *clp;
for (clp = clients; clp; clp = clp->cl_next)
nfsd_lockd_unexport(clp);
}
/*
* Verify that string is non-empty and does not exceed max length.
*/
static int
exp_verify_string(char *cp, int max)
{
int i;
for (i = 0; i < max; i++)
if (!cp[i])
return i;
cp[i] = 0;
printk(KERN_NOTICE "nfsd: couldn't validate string %s\n", cp);
return 0;
}
/*
* Initialize the exports module.
*/
void
nfsd_export_init(void)
{
int i;
dprintk("nfsd: initializing export module.\n");
if (initialized)
return;
for (i = 0; i < CLIENT_HASHMAX; i++)
clnt_hash[i] = NULL;
clients = NULL;
initialized = 1;
}
/*
* Shutdown the exports module.
*/
void
nfsd_export_shutdown(void)
{
int i;
dprintk("nfsd: shutting down export module.\n");
if (!initialized)
return;
if (exp_writelock() < 0) {
printk(KERN_WARNING "Weird: hashtable locked in exp_shutdown");
return;
}
for (i = 0; i < CLIENT_HASHMAX; i++) {
while (clnt_hash[i])
exp_freeclient(clnt_hash[i]->h_client);
}
clients = NULL; /* we may be restarted before the module unloads */
exp_unlock();
dprintk("nfsd: export shutdown complete.\n");
}