xorl %eax, %eax

Linux kernel loop Device NULL Pointer Dereference

leave a comment »

Another one from the ChangeLog file of 2.6.32-rc6 I noticed was this really interesting bug. The bug was discovered and reported by Alexey Dobriyan and it was fixed in 2.6.32-rc6 release of the Linux kernel.
Here is the susceptible code as seen in 2.6.31’s drivers/block/loop.c source code file…

static int loop_clr_fd(struct loop_device *lo, struct block_device *bdev)
{
        struct file *filp = lo->lo_backing_file;
        gfp_t gfp = lo->old_gfp_mask;

        if (lo->lo_state != Lo_bound)
    ...
        if (lo->lo_refcnt > 1)  /* we needed one fd for the ioctl */
    ...
        if (filp == NULL)
    ...
        if (bdev)
                bd_set_size(bdev, 0);
    ...
        /* This is safe: open() is still holding a reference. */
        module_put(THIS_MODULE);
        if (max_part > 0)
                ioctl_by_bdev(bdev, BLKRRPART, 0);
    ...
        return 0;
}

This routine is called by the IOCTL handler routine as you can read here:

static int lo_ioctl(struct block_device *bdev, fmode_t mode,
        unsigned int cmd, unsigned long arg)
{
        struct loop_device *lo = bdev->bd_disk->private_data;
        int err;

        mutex_lock_nested(&lo->lo_ctl_mutex, 1);
        switch (cmd) {
    ...
        case LOOP_CLR_FD:
                /* loop_clr_fd would have unlocked lo_ctl_mutex on success */
                err = loop_clr_fd(lo, bdev);
                if (!err)
                        goto out_unlocked;
                break;
    ...
        return err;
}

And its second argument is the block device to be work on. However, it’s not really hard to see that if for some reason mount of that device fails, the block device pointer represented by ‘bdev’ will be set to NULL.
If we move back to loop_clr_fd() we’ll see that bd_set_size() checks that the ‘bdev’ is not NULL but the subsequent call to ioctl_by_bdev() is issued based entirely on the ‘max_part’ (maximum number of partitions per loop device) being greater than zero. This call to ioctl_by_bdev() located at fs/block_dev.c will result in a NULL pointer dereference since it will invoke blkdev_ioctl() with no checks on the NULL block device pointer like this:

int ioctl_by_bdev(struct block_device *bdev, unsigned cmd, unsigned long arg)
{
        int res;
        mm_segment_t old_fs = get_fs();
        set_fs(KERNEL_DS);
        res = blkdev_ioctl(bdev, 0, cmd, arg);
        set_fs(old_fs);
        return res;
}

And blkdev_ioctl() which resides at block/ioctl.c will execute this code:

/*
 * always keep this in sync with compat_blkdev_ioctl() and
 * compat_blkdev_locked_ioctl()
 */
int blkdev_ioctl(struct block_device *bdev, fmode_t mode, unsigned cmd,
                        unsigned long arg)
{
        struct gendisk *disk = bdev->bd_disk;
        struct backing_dev_info *bdi;
        loff_t size;
        int ret, n;

        switch(cmd) {
     ...
        case BLKRRPART:
                lock_kernel();
                ret = blkdev_reread_part(bdev);
                unlock_kernel();
                break;
     ...
        return ret;
}

Next, blkdev_reread_part() leads to this:

static int blkdev_reread_part(struct block_device *bdev)
{
        struct gendisk *disk = bdev->bd_disk;
        int res;

        if (!disk_partitionable(disk) || bdev != bdev->bd_contains)
                return -EINVAL;
        if (!capable(CAP_SYS_ADMIN))
                return -EACCES;
        if (!mutex_trylock(&bdev->bd_mutex))
                return -EBUSY;
        res = rescan_partitions(disk, bdev);
        mutex_unlock(&bdev->bd_mutex);
        return res;
}

Interesting bug… rescan_partitions() from fs/partitions/check.c looks very interesting knowing that you can control both of its arguments. This was fixed by adding the missing NULL check in the block device pointer before issuing the IOCTL call like this:

 	/* This is safe: open() is still holding a reference. */
 	module_put(THIS_MODULE);
-	if (max_part > 0)
+	if (max_part > 0 && bdev)
 		ioctl_by_bdev(bdev, BLKRRPART, 0);

Written by xorl

November 5, 2009 at 22:19

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s