OSDN Git Service

afs: Overhaul volume and server record caching and fileserver rotation
[tomoyo/tomoyo-test1.git] / fs / afs / security.c
index faca662..46a881a 100644 (file)
@@ -1,6 +1,6 @@
 /* AFS security handling
  *
- * Copyright (C) 2007 Red Hat, Inc. All Rights Reserved.
+ * Copyright (C) 2007, 2017 Red Hat, Inc. All Rights Reserved.
  * Written by David Howells (dhowells@redhat.com)
  *
  * This program is free software; you can redistribute it and/or
 #include <linux/fs.h>
 #include <linux/ctype.h>
 #include <linux/sched.h>
+#include <linux/hashtable.h>
 #include <keys/rxrpc-type.h>
 #include "internal.h"
 
+static DEFINE_HASHTABLE(afs_permits_cache, 10);
+static DEFINE_SPINLOCK(afs_permits_lock);
+
 /*
  * get a key
  */
@@ -46,167 +50,233 @@ struct key *afs_request_key(struct afs_cell *cell)
 }
 
 /*
- * dispose of a permits list
+ * Dispose of a list of permits.
  */
-void afs_zap_permits(struct rcu_head *rcu)
+static void afs_permits_rcu(struct rcu_head *rcu)
 {
        struct afs_permits *permits =
                container_of(rcu, struct afs_permits, rcu);
-       int loop;
-
-       _enter("{%d}", permits->count);
+       int i;
 
-       for (loop = permits->count - 1; loop >= 0; loop--)
-               key_put(permits->permits[loop].key);
+       for (i = 0; i < permits->nr_permits; i++)
+               key_put(permits->permits[i].key);
        kfree(permits);
 }
 
 /*
- * dispose of a permits list in which all the key pointers have been copied
+ * Discard a permission cache.
  */
-static void afs_dispose_of_permits(struct rcu_head *rcu)
+void afs_put_permits(struct afs_permits *permits)
 {
-       struct afs_permits *permits =
-               container_of(rcu, struct afs_permits, rcu);
-
-       _enter("{%d}", permits->count);
-
-       kfree(permits);
+       if (permits && refcount_dec_and_test(&permits->usage)) {
+               spin_lock(&afs_permits_lock);
+               hash_del_rcu(&permits->hash_node);
+               spin_unlock(&afs_permits_lock);
+               call_rcu(&permits->rcu, afs_permits_rcu);
+       }
 }
 
 /*
- * get the authorising vnode - this is the specified inode itself if it's a
- * directory or it's the parent directory if the specified inode is a file or
- * symlink
- * - the caller must release the ref on the inode
+ * Clear a permit cache on callback break.
  */
-static struct afs_vnode *afs_get_auth_inode(struct afs_vnode *vnode,
-                                           struct key *key)
+void afs_clear_permits(struct afs_vnode *vnode)
 {
-       struct afs_vnode *auth_vnode;
-       struct inode *auth_inode;
+       struct afs_permits *permits;
 
-       _enter("");
+       spin_lock(&vnode->lock);
+       permits = rcu_dereference_protected(vnode->permit_cache,
+                                           lockdep_is_held(&vnode->lock));
+       RCU_INIT_POINTER(vnode->permit_cache, NULL);
+       vnode->cb_break++;
+       spin_unlock(&vnode->lock);
 
-       if (S_ISDIR(vnode->vfs_inode.i_mode)) {
-               auth_inode = igrab(&vnode->vfs_inode);
-               ASSERT(auth_inode != NULL);
-       } else {
-               auth_inode = afs_iget(vnode->vfs_inode.i_sb, key,
-                                     &vnode->status.parent, NULL, NULL);
-               if (IS_ERR(auth_inode))
-                       return ERR_CAST(auth_inode);
-       }
-
-       auth_vnode = AFS_FS_I(auth_inode);
-       _leave(" = {%x}", auth_vnode->fid.vnode);
-       return auth_vnode;
+       if (permits)
+               afs_put_permits(permits);
 }
 
 /*
- * clear the permit cache on a directory vnode
+ * Hash a list of permits.  Use simple addition to make it easy to add an extra
+ * one at an as-yet indeterminate position in the list.
  */
-void afs_clear_permits(struct afs_vnode *vnode)
+static void afs_hash_permits(struct afs_permits *permits)
 {
-       struct afs_permits *permits;
-
-       _enter("{%x:%u}", vnode->fid.vid, vnode->fid.vnode);
+       unsigned long h = permits->nr_permits;
+       int i;
 
-       mutex_lock(&vnode->permits_lock);
-       permits = vnode->permits;
-       RCU_INIT_POINTER(vnode->permits, NULL);
-       mutex_unlock(&vnode->permits_lock);
+       for (i = 0; i < permits->nr_permits; i++) {
+               h += (unsigned long)permits->permits[i].key / sizeof(void *);
+               h += permits->permits[i].access;
+       }
 
-       if (permits)
-               call_rcu(&permits->rcu, afs_zap_permits);
-       _leave("");
+       permits->h = h;
 }
 
 /*
- * add the result obtained for a vnode to its or its parent directory's cache
- * for the key used to access it
+ * Cache the CallerAccess result obtained from doing a fileserver operation
+ * that returned a vnode status for a particular key.  If a callback break
+ * occurs whilst the operation was in progress then we have to ditch the cache
+ * as the ACL *may* have changed.
  */
-void afs_cache_permit(struct afs_vnode *vnode, struct key *key, long acl_order)
+void afs_cache_permit(struct afs_vnode *vnode, struct key *key,
+                     unsigned int cb_break)
 {
-       struct afs_permits *permits, *xpermits;
-       struct afs_permit *permit;
-       struct afs_vnode *auth_vnode;
-       int count, loop;
+       struct afs_permits *permits, *xpermits, *replacement, *new = NULL;
+       afs_access_t caller_access = READ_ONCE(vnode->status.caller_access);
+       size_t size = 0;
+       bool changed = false;
+       int i, j;
+
+       _enter("{%x:%u},%x,%x",
+              vnode->fid.vid, vnode->fid.vnode, key_serial(key), caller_access);
+
+       rcu_read_lock();
+
+       /* Check for the common case first: We got back the same access as last
+        * time we tried and already have it recorded.
+        */
+       permits = rcu_dereference(vnode->permit_cache);
+       if (permits) {
+               if (!permits->invalidated) {
+                       for (i = 0; i < permits->nr_permits; i++) {
+                               if (permits->permits[i].key < key)
+                                       continue;
+                               if (permits->permits[i].key > key)
+                                       break;
+                               if (permits->permits[i].access != caller_access) {
+                                       changed = true;
+                                       break;
+                               }
 
-       _enter("{%x:%u},%x,%lx",
-              vnode->fid.vid, vnode->fid.vnode, key_serial(key), acl_order);
+                               if (cb_break != (vnode->cb_break +
+                                                vnode->cb_interest->server->cb_s_break)) {
+                                       changed = true;
+                                       break;
+                               }
 
-       auth_vnode = afs_get_auth_inode(vnode, key);
-       if (IS_ERR(auth_vnode)) {
-               _leave(" [get error %ld]", PTR_ERR(auth_vnode));
-               return;
-       }
+                               /* The cache is still good. */
+                               rcu_read_unlock();
+                               return;
+                       }
+               }
+
+               changed |= permits->invalidated;
+               size = permits->nr_permits;
 
-       mutex_lock(&auth_vnode->permits_lock);
+               /* If this set of permits is now wrong, clear the permits
+                * pointer so that no one tries to use the stale information.
+                */
+               if (changed) {
+                       spin_lock(&vnode->lock);
+                       if (permits != rcu_access_pointer(vnode->permit_cache))
+                               goto someone_else_changed_it_unlock;
+                       RCU_INIT_POINTER(vnode->permit_cache, NULL);
+                       spin_unlock(&vnode->lock);
+
+                       afs_put_permits(permits);
+                       permits = NULL;
+                       size = 0;
+               }
+       }
 
-       /* guard against a rename being detected whilst we waited for the
-        * lock */
-       if (memcmp(&auth_vnode->fid, &vnode->status.parent,
-                  sizeof(struct afs_fid)) != 0) {
-               _debug("renamed");
-               goto out_unlock;
+       if (cb_break != (vnode->cb_break + vnode->cb_interest->server->cb_s_break)) {
+               rcu_read_unlock();
+               goto someone_else_changed_it;
        }
 
-       /* have to be careful as the directory's callback may be broken between
-        * us receiving the status we're trying to cache and us getting the
-        * lock to update the cache for the status */
-       if (auth_vnode->acl_order - acl_order > 0) {
-               _debug("ACL changed?");
-               goto out_unlock;
+       /* We need a ref on any permits list we want to copy as we'll have to
+        * drop the lock to do memory allocation.
+        */
+       if (permits && !refcount_inc_not_zero(&permits->usage)) {
+               rcu_read_unlock();
+               goto someone_else_changed_it;
        }
 
-       /* always update the anonymous mask */
-       _debug("anon access %x", vnode->status.anon_access);
-       auth_vnode->status.anon_access = vnode->status.anon_access;
-       if (key == vnode->volume->cell->anonymous_key)
-               goto out_unlock;
-
-       xpermits = auth_vnode->permits;
-       count = 0;
-       if (xpermits) {
-               /* see if the permit is already in the list
-                * - if it is then we just amend the list
-                */
-               count = xpermits->count;
-               permit = xpermits->permits;
-               for (loop = count; loop > 0; loop--) {
-                       if (permit->key == key) {
-                               permit->access_mask =
-                                       vnode->status.caller_access;
-                               goto out_unlock;
+       rcu_read_unlock();
+
+       /* Speculatively create a new list with the revised permission set.  We
+        * discard this if we find an extant match already in the hash, but
+        * it's easier to compare with memcmp this way.
+        *
+        * We fill in the key pointers at this time, but we don't get the refs
+        * yet.
+        */
+       size++;
+       new = kzalloc(sizeof(struct afs_permits) +
+                     sizeof(struct afs_permit) * size, GFP_NOFS);
+       if (!new)
+               return;
+
+       refcount_set(&new->usage, 1);
+       new->nr_permits = size;
+       i = j = 0;
+       if (permits) {
+               for (i = 0; i < permits->nr_permits; i++) {
+                       if (j == i && permits->permits[i].key > key) {
+                               new->permits[j].key = key;
+                               new->permits[j].access = caller_access;
+                               j++;
                        }
-                       permit++;
+                       new->permits[j].key = permits->permits[i].key;
+                       new->permits[j].access = permits->permits[i].access;
+                       j++;
+               }
+       }
+
+       if (j == i) {
+               new->permits[j].key = key;
+               new->permits[j].access = caller_access;
+       }
+
+       afs_hash_permits(new);
+
+       afs_put_permits(permits);
+
+       /* Now see if the permit list we want is actually already available */
+       spin_lock(&afs_permits_lock);
+
+       hash_for_each_possible(afs_permits_cache, xpermits, hash_node, new->h) {
+               if (xpermits->h != new->h ||
+                   xpermits->invalidated ||
+                   xpermits->nr_permits != new->nr_permits ||
+                   memcmp(xpermits->permits, new->permits,
+                          new->nr_permits * sizeof(struct afs_permit)) != 0)
+                       continue;
+
+               if (refcount_inc_not_zero(&xpermits->usage)) {
+                       replacement = xpermits;
+                       goto found;
                }
+
+               break;
        }
 
-       permits = kmalloc(sizeof(*permits) + sizeof(*permit) * (count + 1),
-                         GFP_NOFS);
-       if (!permits)
-               goto out_unlock;
-
-       if (xpermits)
-               memcpy(permits->permits, xpermits->permits,
-                       count * sizeof(struct afs_permit));
-
-       _debug("key %x access %x",
-              key_serial(key), vnode->status.caller_access);
-       permits->permits[count].access_mask = vnode->status.caller_access;
-       permits->permits[count].key = key_get(key);
-       permits->count = count + 1;
-
-       rcu_assign_pointer(auth_vnode->permits, permits);
-       if (xpermits)
-               call_rcu(&xpermits->rcu, afs_dispose_of_permits);
-
-out_unlock:
-       mutex_unlock(&auth_vnode->permits_lock);
-       iput(&auth_vnode->vfs_inode);
-       _leave("");
+       for (i = 0; i < new->nr_permits; i++)
+               key_get(new->permits[i].key);
+       hash_add_rcu(afs_permits_cache, &new->hash_node, new->h);
+       replacement = new;
+       new = NULL;
+
+found:
+       spin_unlock(&afs_permits_lock);
+
+       kfree(new);
+
+       spin_lock(&vnode->lock);
+       if (cb_break != (vnode->cb_break + vnode->cb_interest->server->cb_s_break) ||
+           permits != rcu_access_pointer(vnode->permit_cache))
+               goto someone_else_changed_it_unlock;
+       rcu_assign_pointer(vnode->permit_cache, replacement);
+       spin_unlock(&vnode->lock);
+       afs_put_permits(permits);
+       return;
+
+someone_else_changed_it_unlock:
+       spin_unlock(&vnode->lock);
+someone_else_changed_it:
+       /* Someone else changed the cache under us - don't recheck at this
+        * time.
+        */
+       return;
 }
 
 /*
@@ -218,56 +288,45 @@ static int afs_check_permit(struct afs_vnode *vnode, struct key *key,
                            afs_access_t *_access)
 {
        struct afs_permits *permits;
-       struct afs_permit *permit;
-       struct afs_vnode *auth_vnode;
-       bool valid;
-       int loop, ret;
+       bool valid = false;
+       int i, ret;
 
        _enter("{%x:%u},%x",
               vnode->fid.vid, vnode->fid.vnode, key_serial(key));
 
-       auth_vnode = afs_get_auth_inode(vnode, key);
-       if (IS_ERR(auth_vnode)) {
-               *_access = 0;
-               _leave(" = %ld", PTR_ERR(auth_vnode));
-               return PTR_ERR(auth_vnode);
-       }
-
-       ASSERT(S_ISDIR(auth_vnode->vfs_inode.i_mode));
+       permits = vnode->permit_cache;
 
        /* check the permits to see if we've got one yet */
-       if (key == auth_vnode->volume->cell->anonymous_key) {
+       if (key == vnode->volume->cell->anonymous_key) {
                _debug("anon");
-               *_access = auth_vnode->status.anon_access;
+               *_access = vnode->status.anon_access;
                valid = true;
        } else {
-               valid = false;
                rcu_read_lock();
-               permits = rcu_dereference(auth_vnode->permits);
+               permits = rcu_dereference(vnode->permit_cache);
                if (permits) {
-                       permit = permits->permits;
-                       for (loop = permits->count; loop > 0; loop--) {
-                               if (permit->key == key) {
-                                       _debug("found in cache");
-                                       *_access = permit->access_mask;
-                                       valid = true;
+                       for (i = 0; i < permits->nr_permits; i++) {
+                               if (permits->permits[i].key < key)
+                                       continue;
+                               if (permits->permits[i].key > key)
                                        break;
-                               }
-                               permit++;
+
+                               *_access = permits->permits[i].access;
+                               valid = !permits->invalidated;
+                               break;
                        }
                }
                rcu_read_unlock();
        }
 
        if (!valid) {
-               /* check the status on the file we're actually interested in
-                * (the post-processing will cache the result on auth_vnode) */
+               /* Check the status on the file we're actually interested in
+                * (the post-processing will cache the result).
+                */
                _debug("no valid permit");
 
-               set_bit(AFS_VNODE_CB_BROKEN, &vnode->flags);
-               ret = afs_vnode_fetch_status(vnode, auth_vnode, key);
+               ret = afs_fetch_status(vnode, key);
                if (ret < 0) {
-                       iput(&auth_vnode->vfs_inode);
                        *_access = 0;
                        _leave(" = %d", ret);
                        return ret;
@@ -275,7 +334,6 @@ static int afs_check_permit(struct afs_vnode *vnode, struct key *key,
                *_access = vnode->status.caller_access;
        }
 
-       iput(&auth_vnode->vfs_inode);
        _leave(" = 0 [access %x]", *_access);
        return 0;
 }
@@ -304,14 +362,9 @@ int afs_permission(struct inode *inode, int mask)
                return PTR_ERR(key);
        }
 
-       /* if the promise has expired, we need to check the server again */
-       if (!vnode->cb_promised) {
-               _debug("not promised");
-               ret = afs_vnode_fetch_status(vnode, NULL, key);
-               if (ret < 0)
-                       goto error;
-               _debug("new promise [fl=%lx]", vnode->flags);
-       }
+       ret = afs_validate(vnode, key);
+       if (ret < 0)
+               goto error;
 
        /* check the permits to see if we've got one yet */
        ret = afs_check_permit(vnode, key, &access);
@@ -365,3 +418,12 @@ error:
        _leave(" = %d", ret);
        return ret;
 }
+
+void __exit afs_clean_up_permit_cache(void)
+{
+       int i;
+
+       for (i = 0; i < HASH_SIZE(afs_permits_cache); i++)
+               WARN_ON_ONCE(!hlist_empty(&afs_permits_cache[i]));
+
+}