NFS: Fix a readdirplus bug
[pandora-kernel.git] / fs / nfs / dir.c
index 07ac384..f0a384e 100644 (file)
@@ -34,6 +34,7 @@
 #include <linux/mount.h>
 #include <linux/sched.h>
 #include <linux/vmalloc.h>
+#include <linux/kmemleak.h>
 
 #include "delegation.h"
 #include "iostat.h"
@@ -161,6 +162,7 @@ struct nfs_cache_array_entry {
        u64 cookie;
        u64 ino;
        struct qstr string;
+       unsigned char d_type;
 };
 
 struct nfs_cache_array {
@@ -170,8 +172,6 @@ struct nfs_cache_array {
        struct nfs_cache_array_entry array[0];
 };
 
-#define MAX_READDIR_ARRAY ((PAGE_SIZE - sizeof(struct nfs_cache_array)) / sizeof(struct nfs_cache_array_entry))
-
 typedef __be32 * (*decode_dirent_t)(struct xdr_stream *, struct nfs_entry *, struct nfs_server *, int);
 typedef struct {
        struct file     *file;
@@ -194,9 +194,13 @@ typedef struct {
 static
 struct nfs_cache_array *nfs_readdir_get_array(struct page *page)
 {
+       void *ptr;
        if (page == NULL)
                return ERR_PTR(-EIO);
-       return (struct nfs_cache_array *)kmap(page);
+       ptr = kmap(page);
+       if (ptr == NULL)
+               return ERR_PTR(-ENOMEM);
+       return ptr;
 }
 
 static
@@ -213,6 +217,9 @@ int nfs_readdir_clear_array(struct page *page, gfp_t mask)
 {
        struct nfs_cache_array *array = nfs_readdir_get_array(page);
        int i;
+
+       if (IS_ERR(array))
+               return PTR_ERR(array);
        for (i = 0; i < array->size; i++)
                kfree(array->array[i].string.name);
        nfs_readdir_release_array(page);
@@ -231,6 +238,11 @@ int nfs_readdir_make_qstr(struct qstr *string, const char *name, unsigned int le
        string->name = kmemdup(name, len, GFP_KERNEL);
        if (string->name == NULL)
                return -ENOMEM;
+       /*
+        * Avoid a kmemleak false positive. The pointer to the name is stored
+        * in a page cache page which kmemleak does not scan.
+        */
+       kmemleak_not_leak(string->name);
        string->hash = full_name_hash(name, len);
        return 0;
 }
@@ -244,20 +256,24 @@ int nfs_readdir_add_to_array(struct nfs_entry *entry, struct page *page)
 
        if (IS_ERR(array))
                return PTR_ERR(array);
-       ret = -EIO;
-       if (array->size >= MAX_READDIR_ARRAY)
-               goto out;
 
        cache_entry = &array->array[array->size];
+
+       /* Check that this entry lies within the page bounds */
+       ret = -ENOSPC;
+       if ((char *)&cache_entry[1] - (char *)page_address(page) > PAGE_SIZE)
+               goto out;
+
        cache_entry->cookie = entry->prev_cookie;
        cache_entry->ino = entry->ino;
+       cache_entry->d_type = entry->d_type;
        ret = nfs_readdir_make_qstr(&cache_entry->string, entry->name, entry->len);
        if (ret)
                goto out;
        array->last_cookie = entry->cookie;
+       array->size++;
        if (entry->eof == 1)
                array->eof_index = array->size;
-       array->size++;
 out:
        nfs_readdir_release_array(page);
        return ret;
@@ -272,7 +288,7 @@ int nfs_readdir_search_for_pos(struct nfs_cache_array *array, nfs_readdir_descri
        if (diff < 0)
                goto out_eof;
        if (diff >= array->size) {
-               if (array->eof_index > 0)
+               if (array->eof_index >= 0)
                        goto out_eof;
                desc->current_index += array->size;
                return -EAGAIN;
@@ -281,8 +297,6 @@ int nfs_readdir_search_for_pos(struct nfs_cache_array *array, nfs_readdir_descri
        index = (unsigned int)diff;
        *desc->dir_cookie = array->array[index].cookie;
        desc->cache_entry_index = index;
-       if (index == array->eof_index)
-               desc->eof = 1;
        return 0;
 out_eof:
        desc->eof = 1;
@@ -296,17 +310,17 @@ int nfs_readdir_search_for_cookie(struct nfs_cache_array *array, nfs_readdir_des
        int status = -EAGAIN;
 
        for (i = 0; i < array->size; i++) {
-               if (i == array->eof_index) {
-                       desc->eof = 1;
-                       status = -EBADCOOKIE;
-               }
                if (array->array[i].cookie == *desc->dir_cookie) {
                        desc->cache_entry_index = i;
                        status = 0;
-                       break;
+                       goto out;
                }
        }
-
+       if (i == array->eof_index) {
+               desc->eof = 1;
+               status = -EBADCOOKIE;
+       }
+out:
        return status;
 }
 
@@ -381,13 +395,9 @@ int xdr_decode(nfs_readdir_descriptor_t *desc, struct nfs_entry *entry, struct x
 static
 int nfs_same_file(struct dentry *dentry, struct nfs_entry *entry)
 {
-       struct nfs_inode *node;
        if (dentry->d_inode == NULL)
                goto different;
-       node = NFS_I(dentry->d_inode);
-       if (node->fh.size != entry->fh->size)
-               goto different;
-       if (strncmp(node->fh.data, entry->fh->data, node->fh.size) != 0)
+       if (nfs_compare_fh(entry->fh, NFS_FH(dentry->d_inode)) != 0)
                goto different;
        return 1;
 different:
@@ -449,14 +459,15 @@ out:
 
 /* Perform conversion from xdr to cache array */
 static
-void nfs_readdir_page_filler(nfs_readdir_descriptor_t *desc, struct nfs_entry *entry,
+int nfs_readdir_page_filler(nfs_readdir_descriptor_t *desc, struct nfs_entry *entry,
                                void *xdr_page, struct page *page, unsigned int buflen)
 {
        struct xdr_stream stream;
        struct xdr_buf buf;
        __be32 *ptr = xdr_page;
-       int status;
        struct nfs_cache_array *array;
+       unsigned int count = 0;
+       int status;
 
        buf.head->iov_base = xdr_page;
        buf.head->iov_len = buflen;
@@ -471,21 +482,32 @@ void nfs_readdir_page_filler(nfs_readdir_descriptor_t *desc, struct nfs_entry *e
 
        do {
                status = xdr_decode(desc, entry, &stream);
-               if (status != 0)
+               if (status != 0) {
+                       if (status == -EAGAIN)
+                               status = 0;
                        break;
+               }
+
+               count++;
 
-               if (nfs_readdir_add_to_array(entry, page) == -1)
-                       break;
                if (desc->plus == 1)
                        nfs_prime_dcache(desc->file->f_path.dentry, entry);
+
+               status = nfs_readdir_add_to_array(entry, page);
+               if (status != 0)
+                       break;
        } while (!entry->eof);
 
-       if (status == -EBADCOOKIE && entry->eof) {
+       if (count == 0 || (status == -EBADCOOKIE && entry->eof == 1)) {
                array = nfs_readdir_get_array(page);
-               array->eof_index = array->size - 1;
-               status = 0;
-               nfs_readdir_release_array(page);
+               if (!IS_ERR(array)) {
+                       array->eof_index = array->size;
+                       status = 0;
+                       nfs_readdir_release_array(page);
+               } else
+                       status = PTR_ERR(array);
        }
+       return status;
 }
 
 static
@@ -537,7 +559,7 @@ int nfs_readdir_xdr_to_array(nfs_readdir_descriptor_t *desc, struct page *page,
        struct nfs_entry entry;
        struct file     *file = desc->file;
        struct nfs_cache_array *array;
-       int status = 0;
+       int status = -ENOMEM;
        unsigned int array_size = ARRAY_SIZE(pages);
 
        entry.prev_cookie = 0;
@@ -549,6 +571,10 @@ int nfs_readdir_xdr_to_array(nfs_readdir_descriptor_t *desc, struct page *page,
                goto out;
 
        array = nfs_readdir_get_array(page);
+       if (IS_ERR(array)) {
+               status = PTR_ERR(array);
+               goto out;
+       }
        memset(array, 0, sizeof(struct nfs_cache_array));
        array->eof_index = -1;
 
@@ -556,12 +582,19 @@ int nfs_readdir_xdr_to_array(nfs_readdir_descriptor_t *desc, struct page *page,
        if (!pages_ptr)
                goto out_release_array;
        do {
+               unsigned int pglen;
                status = nfs_readdir_xdr_filler(pages, desc, &entry, file, inode);
 
                if (status < 0)
                        break;
-               nfs_readdir_page_filler(desc, &entry, pages_ptr, page, array_size * PAGE_SIZE);
-       } while (array->eof_index < 0 && array->size < MAX_READDIR_ARRAY);
+               pglen = status;
+               status = nfs_readdir_page_filler(desc, &entry, pages_ptr, page, pglen);
+               if (status < 0) {
+                       if (status == -ENOSPC)
+                               status = 0;
+                       break;
+               }
+       } while (array->eof_index < 0);
 
        nfs_readdir_free_large_page(pages_ptr, pages, array_size);
 out_release_array:
@@ -582,8 +615,10 @@ static
 int nfs_readdir_filler(nfs_readdir_descriptor_t *desc, struct page* page)
 {
        struct inode    *inode = desc->file->f_path.dentry->d_inode;
+       int ret;
 
-       if (nfs_readdir_xdr_to_array(desc, page, inode) < 0)
+       ret = nfs_readdir_xdr_to_array(desc, page, inode);
+       if (ret < 0)
                goto error;
        SetPageUptodate(page);
 
@@ -595,7 +630,7 @@ int nfs_readdir_filler(nfs_readdir_descriptor_t *desc, struct page* page)
        return 0;
  error:
        unlock_page(page);
-       return -EIO;
+       return ret;
 }
 
 static
@@ -608,12 +643,8 @@ void cache_page_release(nfs_readdir_descriptor_t *desc)
 static
 struct page *get_cache_page(nfs_readdir_descriptor_t *desc)
 {
-       struct page *page;
-       page = read_cache_page(desc->file->f_path.dentry->d_inode->i_mapping,
+       return read_cache_page(desc->file->f_path.dentry->d_inode->i_mapping,
                        desc->page_index, (filler_t *)nfs_readdir_filler, desc);
-       if (IS_ERR(page))
-               desc->eof = 1;
-       return page;
 }
 
 /*
@@ -639,8 +670,10 @@ int find_cache_page(nfs_readdir_descriptor_t *desc)
 static inline
 int readdir_search_pagecache(nfs_readdir_descriptor_t *desc)
 {
-       int res = -EAGAIN;
+       int res;
 
+       if (desc->page_index == 0)
+               desc->current_index = 0;
        while (1) {
                res = find_cache_page(desc);
                if (res != -EAGAIN)
@@ -666,35 +699,36 @@ int nfs_do_filldir(nfs_readdir_descriptor_t *desc, void *dirent,
        int i = 0;
        int res = 0;
        struct nfs_cache_array *array = NULL;
-       unsigned int d_type = DT_UNKNOWN;
-       struct dentry *dentry = NULL;
 
        array = nfs_readdir_get_array(desc->page);
+       if (IS_ERR(array)) {
+               res = PTR_ERR(array);
+               goto out;
+       }
 
        for (i = desc->cache_entry_index; i < array->size; i++) {
-               d_type = DT_UNKNOWN;
+               struct nfs_cache_array_entry *ent;
 
-               res = filldir(dirent, array->array[i].string.name,
-                       array->array[i].string.len, file->f_pos,
-                       nfs_compat_user_ino64(array->array[i].ino), d_type);
-               if (res < 0)
+               ent = &array->array[i];
+               if (filldir(dirent, ent->string.name, ent->string.len,
+                   file->f_pos, nfs_compat_user_ino64(ent->ino),
+                   ent->d_type) < 0) {
+                       desc->eof = 1;
                        break;
+               }
                file->f_pos++;
                desc->cache_entry_index = i;
                if (i < (array->size-1))
                        *desc->dir_cookie = array->array[i+1].cookie;
                else
                        *desc->dir_cookie = array->last_cookie;
-               if (i == array->eof_index) {
-                       desc->eof = 1;
-                       break;
-               }
        }
+       if (i == array->eof_index)
+               desc->eof = 1;
 
        nfs_readdir_release_array(desc->page);
+out:
        cache_page_release(desc);
-       if (dentry != NULL)
-               dput(dentry);
        dfprintk(DIRCACHE, "NFS: nfs_do_filldir() filling ended @ cookie %Lu; returning = %d\n",
                        (unsigned long long)*desc->dir_cookie, res);
        return res;
@@ -729,13 +763,13 @@ int uncached_readdir(nfs_readdir_descriptor_t *desc, void *dirent,
                goto out;
        }
 
-       if (nfs_readdir_xdr_to_array(desc, page, inode) == -1) {
-               status = -EIO;
-               goto out_release;
-       }
-
        desc->page_index = 0;
        desc->page = page;
+
+       status = nfs_readdir_xdr_to_array(desc, page, inode);
+       if (status < 0)
+               goto out_release;
+
        status = nfs_do_filldir(desc, dirent, filldir);
 
  out:
@@ -786,14 +820,14 @@ static int nfs_readdir(struct file *filp, void *dirent, filldir_t filldir)
                res = readdir_search_pagecache(desc);
 
                if (res == -EBADCOOKIE) {
+                       res = 0;
                        /* This means either end of directory */
                        if (*desc->dir_cookie && desc->eof == 0) {
                                /* Or that the server has 'lost' a cookie */
                                res = uncached_readdir(desc, dirent, filldir);
-                               if (res >= 0)
+                               if (res == 0)
                                        continue;
                        }
-                       res = 0;
                        break;
                }
                if (res == -ETOOSMALL && desc->plus) {
@@ -808,10 +842,8 @@ static int nfs_readdir(struct file *filp, void *dirent, filldir_t filldir)
                        break;
 
                res = nfs_do_filldir(desc, dirent, filldir);
-               if (res < 0) {
-                       res = 0;
+               if (res < 0)
                        break;
-               }
        }
 out:
        nfs_unblock_sillyrename(dentry);
@@ -1345,12 +1377,12 @@ static struct dentry *nfs_atomic_lookup(struct inode *dir, struct dentry *dentry
                                res = NULL;
                                goto out;
                        /* This turned out not to be a regular file */
-                       case -EISDIR:
                        case -ENOTDIR:
                                goto no_open;
                        case -ELOOP:
                                if (!(nd->intent.open.flags & O_NOFOLLOW))
                                        goto no_open;
+                       /* case -EISDIR: */
                        /* case -EINVAL: */
                        default:
                                res = ERR_CAST(inode);