ext4: fix readdir error in case inline_data+^dir_index.
authorTao Ma <boyu.mt@taobao.com>
Fri, 19 Apr 2013 21:55:33 +0000 (17:55 -0400)
committerTheodore Ts'o <tytso@mit.edu>
Fri, 19 Apr 2013 21:55:33 +0000 (17:55 -0400)
Zach reported a problem that if inline data is enabled, we don't
tell the difference between the offset of '.' and '..'. And a
getdents will fail if the user only want to get '.'. And what's
worse, we may meet with duplicate dir entries as the offset
for inline dir and non-inline one is quite different.

This patch just try to resolve this problem if dir_index
is disabled. In this case, f_pos is the real offset with
the dir block, so for inline dir, we just pretend as if
we are a dir block and returns the offset like a norml
dir block does.

Reported-by: Zach Brown <zab@redhat.com>
Signed-off-by: Tao Ma <boyu.mt@taobao.com>
Signed-off-by: "Theodore Ts'o" <tytso@mit.edu>
fs/ext4/inline.c

index abf8b6278c3adb311379ba9441812894076fea11..3e2bf873e8a8b42b1564c44505e0ec8cba1971e6 100644 (file)
@@ -1396,6 +1396,14 @@ out:
        return ret;
 }
 
+/*
+ * So this function is called when the volume is mkfsed with
+ * dir_index disabled. In order to keep f_pos persistent
+ * after we convert from an inlined dir to a blocked based,
+ * we just pretend that we are a normal dir and return the
+ * offset as if '.' and '..' really take place.
+ *
+ */
 int ext4_read_inline_dir(struct file *filp,
                         void *dirent, filldir_t filldir,
                         int *has_inline_data)
@@ -1409,6 +1417,7 @@ int ext4_read_inline_dir(struct file *filp,
        int ret, inline_size = 0;
        struct ext4_iloc iloc;
        void *dir_buf = NULL;
+       int dotdot_offset, dotdot_size, extra_offset, extra_size;
 
        ret = ext4_get_inode_loc(inode, &iloc);
        if (ret)
@@ -1437,8 +1446,21 @@ int ext4_read_inline_dir(struct file *filp,
        sb = inode->i_sb;
        stored = 0;
        parent_ino = le32_to_cpu(((struct ext4_dir_entry_2 *)dir_buf)->inode);
+       offset = filp->f_pos;
+
+       /*
+        * dotdot_offset and dotdot_size is the real offset and
+        * size for ".." and "." if the dir is block based while
+        * the real size for them are only EXT4_INLINE_DOTDOT_SIZE.
+        * So we will use extra_offset and extra_size to indicate them
+        * during the inline dir iteration.
+        */
+       dotdot_offset = EXT4_DIR_REC_LEN(1);
+       dotdot_size = dotdot_offset + EXT4_DIR_REC_LEN(2);
+       extra_offset = dotdot_size - EXT4_INLINE_DOTDOT_SIZE;
+       extra_size = extra_offset + inline_size;
 
-       while (!error && !stored && filp->f_pos < inode->i_size) {
+       while (!error && !stored && filp->f_pos < extra_size) {
 revalidate:
                /*
                 * If the version has changed since the last call to
@@ -1447,15 +1469,23 @@ revalidate:
                 * dir to make sure.
                 */
                if (filp->f_version != inode->i_version) {
-                       for (i = 0;
-                            i < inode->i_size && i < offset;) {
+                       for (i = 0; i < extra_size && i < offset;) {
+                               /*
+                                * "." is with offset 0 and
+                                * ".." is dotdot_offset.
+                                */
                                if (!i) {
-                                       /* skip "." and ".." if needed. */
-                                       i += EXT4_INLINE_DOTDOT_SIZE;
+                                       i = dotdot_offset;
+                                       continue;
+                               } else if (i == dotdot_offset) {
+                                       i = dotdot_size;
                                        continue;
                                }
+                               /* for other entry, the real offset in
+                                * the buf has to be tuned accordingly.
+                                */
                                de = (struct ext4_dir_entry_2 *)
-                                       (dir_buf + i);
+                                       (dir_buf + i - extra_offset);
                                /* It's too expensive to do a full
                                 * dirent test each time round this
                                 * loop, but we do have to test at
@@ -1463,43 +1493,47 @@ revalidate:
                                 * failure will be detected in the
                                 * dirent test below. */
                                if (ext4_rec_len_from_disk(de->rec_len,
-                                       inline_size) < EXT4_DIR_REC_LEN(1))
+                                       extra_size) < EXT4_DIR_REC_LEN(1))
                                        break;
                                i += ext4_rec_len_from_disk(de->rec_len,
-                                                           inline_size);
+                                                           extra_size);
                        }
                        offset = i;
                        filp->f_pos = offset;
                        filp->f_version = inode->i_version;
                }
 
-               while (!error && filp->f_pos < inode->i_size) {
+               while (!error && filp->f_pos < extra_size) {
                        if (filp->f_pos == 0) {
                                error = filldir(dirent, ".", 1, 0, inode->i_ino,
                                                DT_DIR);
                                if (error)
                                        break;
                                stored++;
+                               filp->f_pos = dotdot_offset;
+                               continue;
+                       }
 
-                               error = filldir(dirent, "..", 2, 0, parent_ino,
-                                               DT_DIR);
+                       if (filp->f_pos == dotdot_offset) {
+                               error = filldir(dirent, "..", 2,
+                                               dotdot_offset,
+                                               parent_ino, DT_DIR);
                                if (error)
                                        break;
                                stored++;
 
-                               filp->f_pos = offset = EXT4_INLINE_DOTDOT_SIZE;
+                               filp->f_pos = dotdot_size;
                                continue;
                        }
 
-                       de = (struct ext4_dir_entry_2 *)(dir_buf + offset);
+                       de = (struct ext4_dir_entry_2 *)
+                               (dir_buf + filp->f_pos - extra_offset);
                        if (ext4_check_dir_entry(inode, filp, de,
                                                 iloc.bh, dir_buf,
-                                                inline_size, offset)) {
+                                                extra_size, filp->f_pos)) {
                                ret = stored;
                                goto out;
                        }
-                       offset += ext4_rec_len_from_disk(de->rec_len,
-                                                        inline_size);
                        if (le32_to_cpu(de->inode)) {
                                /* We might block in the next section
                                 * if the data destination is
@@ -1522,9 +1556,8 @@ revalidate:
                                stored++;
                        }
                        filp->f_pos += ext4_rec_len_from_disk(de->rec_len,
-                                                             inline_size);
+                                                             extra_size);
                }
-               offset = 0;
        }
 out:
        kfree(dir_buf);