btrfs: fix readdir deadlock with pagefault
Readdir does dir_emit while under the btree lock. dir_emit can trigger
the page fault which means we can deadlock. Fix this by allocating a
buffer on opening a directory and copying the readdir into this buffer
and doing dir_emit from outside of the tree lock.
Thread A
readdir <holding tree lock>
dir_emit
<page fault>
down_read(mmap_sem)
Thread B
mmap write
down_write(mmap_sem)
page_mkwrite
wait_ordered_extents
Process C
finish_ordered_extent
insert_reserved_file_extent
try to lock leaf <hang>
Signed-off-by: Josef Bacik <jbacik@fb.com>
Reviewed-by: David Sterba <dsterba@suse.com>
[ copy the deadlock scenario to changelog ]
Signed-off-by: David Sterba <dsterba@suse.com>
diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c
index 7a3c56c..b21558b 100644
--- a/fs/btrfs/ioctl.c
+++ b/fs/btrfs/ioctl.c
@@ -3966,6 +3966,7 @@ static long btrfs_ioctl_trans_start(struct file *file)
struct btrfs_fs_info *fs_info = btrfs_sb(inode->i_sb);
struct btrfs_root *root = BTRFS_I(inode)->root;
struct btrfs_trans_handle *trans;
+ struct btrfs_file_private *private;
int ret;
static bool warned = false;
@@ -3984,8 +3985,16 @@ static long btrfs_ioctl_trans_start(struct file *file)
}
ret = -EINPROGRESS;
- if (file->private_data)
+ private = file->private_data;
+ if (private && private->trans)
goto out;
+ if (!private) {
+ private = kzalloc(sizeof(struct btrfs_file_private),
+ GFP_KERNEL);
+ if (!private)
+ return -ENOMEM;
+ file->private_data = private;
+ }
ret = -EROFS;
if (btrfs_root_readonly(root))
@@ -4002,7 +4011,7 @@ static long btrfs_ioctl_trans_start(struct file *file)
if (IS_ERR(trans))
goto out_drop;
- file->private_data = trans;
+ private->trans = trans;
return 0;
out_drop:
@@ -4257,14 +4266,13 @@ long btrfs_ioctl_trans_end(struct file *file)
{
struct inode *inode = file_inode(file);
struct btrfs_root *root = BTRFS_I(inode)->root;
- struct btrfs_trans_handle *trans;
+ struct btrfs_file_private *private = file->private_data;
- trans = file->private_data;
- if (!trans)
+ if (!private || !private->trans)
return -EINVAL;
- file->private_data = NULL;
- btrfs_end_transaction(trans);
+ btrfs_end_transaction(private->trans);
+ private->trans = NULL;
atomic_dec(&root->fs_info->open_ioctl_trans);