xorl %eax, %eax

CVE-2009-4410: Linux kernel FUSE Invalid Pointer Access

leave a comment »

This vulnerability was originally reported to the Red Hat’s bug tracking system by David Shaw on 21 December 2009. This issue affects Linux kernel releases 2.6.29-rc1 through 2.6.30 and the vulnerable code is located at fs/fuse/file.c like this:

static int fuse_ioctl_copy_user(struct page **pages, struct iovec *iov,
                        unsigned int nr_segs, size_t bytes, bool to_user)
{
        struct iov_iter ii;
        int page_idx = 0;

        if (!bytes)
                return 0;

        iov_iter_init(&ii, iov, nr_segs, bytes, 0);

        while (iov_iter_count(&ii)) {
                struct page *page = pages[page_idx++];
                size_t todo = min_t(size_t, PAGE_SIZE, iov_iter_count(&ii));
                void *kaddr, *map;

                kaddr = map = kmap(page);

                while (todo) {
       ...
                }
       
                kunmap(map);
        }

        return 0;
}

So, as you can read from the above code snippet, fuse_ioctl_copy_user() uses kmap() to retrieve the address of ‘page’ page. The latter routine can be found at arch/x86/mm/highmem_32.c (for x86 processors) and it is quite simple…

void *kmap(struct page *page)
{
        might_sleep();
        if (!PageHighMem(page))
                return page_address(page);
        return kmap_high(page);
}

Where page_address() simply returns the virtual address of the page passed to it as an argument. The equivalent code for unmapping is kunmap() which is located in the same source code file…

void kunmap(struct page *page)
{
        if (in_interrupt())
                BUG();
        if (!PageHighMem(page))
                return;
        kunmap_high(page);
}

The problem with fuse_ioctl_copy_user() is that instead of unmapping the actual page, the code attempts to unmap the virtual address of the mapped space in that page’s virtual address space (the page_address() return value) which would result in an attempt to unmap an invalid pointer. The fix to this bug was to replace the kunmap() call like that:

                }
 
-               kunmap(map);
+               kunmap(page);
        }

Assuming that we passed an invalid page pointer to kunmap() that it’s not in an interrupt, the following code will be executed:

/**
 * kunmap_high - map a highmem page into memory
 * @page: &struct page to unmap
 *
 * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
 * only from user context.
 */
void kunmap_high(struct page *page)
{
        unsigned long vaddr;
        unsigned long nr;
        unsigned long flags;
        int need_wakeup;

        lock_kmap_any(flags);
        vaddr = (unsigned long)page_address(page);
        BUG_ON(!vaddr);
        nr = PKMAP_NR(vaddr);

        /*
         * A count must never go down to zero
         * without a TLB flush!
         */
      ...
}

So, unless there is a way to make the page_address() return a non-NULL value to ‘vaddr’, the result of this bug would be a call to BUG_ON() which is of course a kernel panic. However, if a user manages to have a valid page structure in that address the subsequent operations might result in exploitable conditions even though I was not able to confirm this.
In any case, since the vulnerability is part of an IOCTL handler it can be reached really easily through fuse_file_do_ioctl() which is a well documented IOCTL handling routine.

static long fuse_file_do_ioctl(struct file *file, unsigned int cmd,
                               unsigned long arg, unsigned int flags)
{
        struct inode *inode = file->f_dentry->d_inode;
        struct fuse_file *ff = file->private_data;
        struct fuse_conn *fc = get_fuse_conn(inode);
        struct fuse_ioctl_in inarg = {
                .fh = ff->fh,
                .cmd = cmd,
                .arg = arg,
                .flags = flags
        };
      ...
        /* okay, let's send it to the client */
        req->in.h.opcode = FUSE_IOCTL;
        req->in.h.nodeid = get_node_id(inode);
        req->in.numargs = 1;
        req->in.args[0].size = sizeof(inarg);
        req->in.args[0].value = &inarg;
        if (in_size) {
                req->in.numargs++;
                req->in.args[1].size = in_size;
                req->in.argpages = 1;

                err = fuse_ioctl_copy_user(pages, in_iov, in_iovs, in_size,
                                           false);
                if (err)
                        goto out;
        }
      ...
}

Written by xorl

January 1, 2010 at 05:53

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