#include <linux/fs.h>
 #include <linux/mutex.h>
 #include <linux/buffer_head.h>
+#include <linux/blkdev.h>
 #include "hfsplus_raw.h"
 
 #define DBG_BNODE_REFS 0x00000001
 struct hfs_btree;
 
 struct hfsplus_sb_info {
+       void *s_vhdr_buf;
        struct hfsplus_vh *s_vhdr;
+       void *s_backup_vhdr_buf;
        struct hfsplus_vh *s_backup_vhdr;
        struct hfs_btree *ext_tree;
        struct hfs_btree *cat_tree;
        struct hfsplus_cat_key key;
 };
 
+/*
+ * Find minimum acceptible I/O size for an hfsplus sb.
+ */
+static inline unsigned short hfsplus_min_io_size(struct super_block *sb)
+{
+       return max_t(unsigned short, bdev_logical_block_size(sb->s_bdev),
+                    HFSPLUS_SECTOR_SIZE);
+}
+
 #define hfs_btree_open hfsplus_btree_open
 #define hfs_btree_close hfsplus_btree_close
 #define hfs_btree_write hfsplus_btree_write
 /* wrapper.c */
 int hfsplus_read_wrapper(struct super_block *);
 int hfs_part_find(struct super_block *, sector_t *, sector_t *);
-int hfsplus_submit_bio(struct block_device *bdev, sector_t sector,
-               void *data, int rw);
+int hfsplus_submit_bio(struct super_block *sb, sector_t sector,
+               void *buf, void **data, int rw);
 
 /* time macros */
 #define __hfsp_mt2ut(t)                (be32_to_cpu(t) - 2082844800U)
 
        return -ENOENT;
 }
 
-static int hfs_parse_new_pmap(struct super_block *sb, struct new_pmap *pm,
-               sector_t *part_start, sector_t *part_size)
+static int hfs_parse_new_pmap(struct super_block *sb, void *buf,
+               struct new_pmap *pm, sector_t *part_start, sector_t *part_size)
 {
        struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
        int size = be32_to_cpu(pm->pmMapBlkCnt);
+       int buf_size = hfsplus_min_io_size(sb);
        int res;
        int i = 0;
 
                if (++i >= size)
                        return -ENOENT;
 
-               res = hfsplus_submit_bio(sb->s_bdev,
-                                        *part_start + HFS_PMAP_BLK + i,
-                                        pm, READ);
-               if (res)
-                       return res;
+               pm = (struct new_pmap *)((u8 *)pm + HFSPLUS_SECTOR_SIZE);
+               if ((u8 *)pm - (u8 *)buf >= buf_size) {
+                       res = hfsplus_submit_bio(sb,
+                                                *part_start + HFS_PMAP_BLK + i,
+                                                buf, (void **)&pm, READ);
+                       if (res)
+                               return res;
+               }
        } while (pm->pmSig == cpu_to_be16(HFS_NEW_PMAP_MAGIC));
 
        return -ENOENT;
 int hfs_part_find(struct super_block *sb,
                sector_t *part_start, sector_t *part_size)
 {
-       void *data;
+       void *buf, *data;
        int res;
 
-       data = kmalloc(HFSPLUS_SECTOR_SIZE, GFP_KERNEL);
-       if (!data)
+       buf = kmalloc(hfsplus_min_io_size(sb), GFP_KERNEL);
+       if (!buf)
                return -ENOMEM;
 
-       res = hfsplus_submit_bio(sb->s_bdev, *part_start + HFS_PMAP_BLK,
-                                data, READ);
+       res = hfsplus_submit_bio(sb, *part_start + HFS_PMAP_BLK,
+                                buf, &data, READ);
        if (res)
                goto out;
 
                res = hfs_parse_old_pmap(sb, data, part_start, part_size);
                break;
        case HFS_NEW_PMAP_MAGIC:
-               res = hfs_parse_new_pmap(sb, data, part_start, part_size);
+               res = hfs_parse_new_pmap(sb, buf, data, part_start, part_size);
                break;
        default:
                res = -ENOENT;
                break;
        }
 out:
-       kfree(data);
+       kfree(buf);
        return res;
 }
 
                write_backup = 1;
        }
 
-       error2 = hfsplus_submit_bio(sb->s_bdev,
+       error2 = hfsplus_submit_bio(sb,
                                   sbi->part_start + HFSPLUS_VOLHEAD_SECTOR,
-                                  sbi->s_vhdr, WRITE_SYNC);
+                                  sbi->s_vhdr_buf, NULL, WRITE_SYNC);
        if (!error)
                error = error2;
        if (!write_backup)
                goto out;
 
-       error2 = hfsplus_submit_bio(sb->s_bdev,
+       error2 = hfsplus_submit_bio(sb,
                                  sbi->part_start + sbi->sect_count - 2,
-                                 sbi->s_backup_vhdr, WRITE_SYNC);
+                                 sbi->s_backup_vhdr_buf, NULL, WRITE_SYNC);
        if (!error)
                error2 = error;
 out:
        hfs_btree_close(sbi->ext_tree);
        iput(sbi->alloc_file);
        iput(sbi->hidden_dir);
-       kfree(sbi->s_vhdr);
-       kfree(sbi->s_backup_vhdr);
+       kfree(sbi->s_vhdr_buf);
+       kfree(sbi->s_backup_vhdr_buf);
        unload_nls(sbi->nls);
        kfree(sb->s_fs_info);
        sb->s_fs_info = NULL;
 
        complete(bio->bi_private);
 }
 
-int hfsplus_submit_bio(struct block_device *bdev, sector_t sector,
-               void *data, int rw)
+/*
+ * hfsplus_submit_bio - Perfrom block I/O
+ * @sb: super block of volume for I/O
+ * @sector: block to read or write, for blocks of HFSPLUS_SECTOR_SIZE bytes
+ * @buf: buffer for I/O
+ * @data: output pointer for location of requested data
+ * @rw: direction of I/O
+ *
+ * The unit of I/O is hfsplus_min_io_size(sb), which may be bigger than
+ * HFSPLUS_SECTOR_SIZE, and @buf must be sized accordingly. On reads
+ * @data will return a pointer to the start of the requested sector,
+ * which may not be the same location as @buf.
+ *
+ * If @sector is not aligned to the bdev logical block size it will
+ * be rounded down. For writes this means that @buf should contain data
+ * that starts at the rounded-down address. As long as the data was
+ * read using hfsplus_submit_bio() and the same buffer is used things
+ * will work correctly.
+ */
+int hfsplus_submit_bio(struct super_block *sb, sector_t sector,
+               void *buf, void **data, int rw)
 {
        DECLARE_COMPLETION_ONSTACK(wait);
        struct bio *bio;
        int ret = 0;
+       unsigned int io_size;
+       loff_t start;
+       int offset;
+
+       /*
+        * Align sector to hardware sector size and find offset. We
+        * assume that io_size is a power of two, which _should_
+        * be true.
+        */
+       io_size = hfsplus_min_io_size(sb);
+       start = (loff_t)sector << HFSPLUS_SECTOR_SHIFT;
+       offset = start & (io_size - 1);
+       sector &= ~((io_size >> HFSPLUS_SECTOR_SHIFT) - 1);
 
        bio = bio_alloc(GFP_NOIO, 1);
        bio->bi_sector = sector;
-       bio->bi_bdev = bdev;
+       bio->bi_bdev = sb->s_bdev;
        bio->bi_end_io = hfsplus_end_io_sync;
        bio->bi_private = &wait;
 
-       /*
-        * We always submit one sector at a time, so bio_add_page must not fail.
-        */
-       if (bio_add_page(bio, virt_to_page(data), HFSPLUS_SECTOR_SIZE,
-                        offset_in_page(data)) != HFSPLUS_SECTOR_SIZE)
-               BUG();
+       if (!(rw & WRITE) && data)
+               *data = (u8 *)buf + offset;
+
+       while (io_size > 0) {
+               unsigned int page_offset = offset_in_page(buf);
+               unsigned int len = min_t(unsigned int, PAGE_SIZE - page_offset,
+                                        io_size);
+
+               ret = bio_add_page(bio, virt_to_page(buf), len, page_offset);
+               if (ret != len) {
+                       ret = -EIO;
+                       goto out;
+               }
+               io_size -= len;
+               buf = (u8 *)buf + len;
+       }
 
        submit_bio(rw, bio);
        wait_for_completion(&wait);
        if (!bio_flagged(bio, BIO_UPTODATE))
                ret = -EIO;
 
+out:
        bio_put(bio);
-       return ret;
+       return ret < 0 ? ret : 0;
 }
 
 static int hfsplus_read_mdb(void *bufptr, struct hfsplus_wd *wd)
                goto out;
 
        error = -ENOMEM;
-       sbi->s_vhdr = kmalloc(HFSPLUS_SECTOR_SIZE, GFP_KERNEL);
-       if (!sbi->s_vhdr)
+       sbi->s_vhdr_buf = kmalloc(hfsplus_min_io_size(sb), GFP_KERNEL);
+       if (!sbi->s_vhdr_buf)
                goto out;
-       sbi->s_backup_vhdr = kmalloc(HFSPLUS_SECTOR_SIZE, GFP_KERNEL);
-       if (!sbi->s_backup_vhdr)
+       sbi->s_backup_vhdr_buf = kmalloc(hfsplus_min_io_size(sb), GFP_KERNEL);
+       if (!sbi->s_backup_vhdr_buf)
                goto out_free_vhdr;
 
 reread:
-       error = hfsplus_submit_bio(sb->s_bdev,
-                                  part_start + HFSPLUS_VOLHEAD_SECTOR,
-                                  sbi->s_vhdr, READ);
+       error = hfsplus_submit_bio(sb, part_start + HFSPLUS_VOLHEAD_SECTOR,
+                                  sbi->s_vhdr_buf, (void **)&sbi->s_vhdr,
+                                  READ);
        if (error)
                goto out_free_backup_vhdr;
 
                goto reread;
        }
 
-       error = hfsplus_submit_bio(sb->s_bdev,
-                                  part_start + part_size - 2,
-                                  sbi->s_backup_vhdr, READ);
+       error = hfsplus_submit_bio(sb, part_start + part_size - 2,
+                                  sbi->s_backup_vhdr_buf,
+                                  (void **)&sbi->s_backup_vhdr, READ);
        if (error)
                goto out_free_backup_vhdr;