xorl %eax, %eax

CVE-2010-4650: Linux kernel FUSE IOVEC Length Overflow

leave a comment »

This was a fix by Miklos Szeredi of SUSE for a missing check inside the FUSE filesystem IOCTL handler located at fs/fuse/file.c like this…

long fuse_do_ioctl(struct file *file, unsigned int cmd, unsigned long arg,
                   unsigned int flags)
{
        struct fuse_file *ff = file->private_data;
        struct fuse_conn *fc = ff->fc;
   ...
        /*
         * If restricted, initialize IO parameters as encoded in @cmd.
         * RETRY from server is not allowed.
         */
        if (!(flags & FUSE_IOCTL_UNRESTRICTED)) {
                struct iovec *iov = page_address(iov_page);

                iov->iov_base = (void __user *)arg;
                iov->iov_len = _IOC_SIZE(cmd);

                if (_IOC_DIR(cmd) & _IOC_WRITE) {
                        in_iov = iov;
                        in_iovs = 1;
                }

                if (_IOC_DIR(cmd) & _IOC_READ) {
                        out_iov = iov;
                        out_iovs = 1;
                }
        }

 retry:
        inarg.in_size = in_size = iov_length(in_iov, in_iovs);
        inarg.out_size = out_size = iov_length(out_iov, out_iovs);
   ...
                /* okay, copy in iovs and retry */
                vaddr = kmap_atomic(pages[0], KM_USER0);
                memcpy(page_address(iov_page), vaddr, transferred);
                kunmap_atomic(vaddr, KM_USER0);

                in_iov = page_address(iov_page);
                out_iov = in_iov + in_iovs;

                goto retry;
        }
   ...
        return err ? err : outarg.result;
}

As you can read inside the ‘retry’ clause, it uses ‘in_iov’, ‘in_iovs’, ‘out_iov’ and ‘out_iovs’ to calculate the appropriate IOVEC lengths but this doesn’t check for a possible overflow as we can read at include/linux/uio.h where it is defined.

/*
 * Total number of bytes covered by an iovec.
 *
 * NOTE that it is not safe to use this function until all the iovec's
 * segment lengths have been validated.  Because the individual lengths can
 * overflow a size_t when added together.
 */
static inline size_t iov_length(const struct iovec *iov, unsigned long nr_segs)
{
        unsigned long seg;
        size_t ret = 0;

        for (seg = 0; seg < nr_segs; seg++)
                ret += iov[seg].iov_len;
        return ret;
}

So, to fix this a new routine was created in fs/fuse/file.c called fuse_verify_ioctl_iov() that verifies the passed lengths as shown below.

/* Make sure iov_length() won't overflow */
static int fuse_verify_ioctl_iov(struct iovec *iov, size_t count)
{
       size_t n;
       u32 max = FUSE_MAX_PAGES_PER_REQ << PAGE_SHIFT;

       for (n = 0; n < count; n++) {
               if (iov->iov_len > (size_t) max)
                       return -ENOMEM;
               max -= iov->iov_len;
       }
       return 0;
}

And of course, the buggy caclulations are now verified before re-executing ‘retry’ clause’s code.

                out_iov = in_iov + in_iovs;
 
+               err = fuse_verify_ioctl_iov(in_iov, in_iovs);
+               if (err)
+                       goto out;
+
+               err = fuse_verify_ioctl_iov(out_iov, out_iovs);
+               if (err)
+                       goto out;
+
                goto retry;

Written by xorl

January 7, 2011 at 00:10

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