xorl %eax, %eax

CVE-2010-3080: Linux kernel /dev/sequencer Double Free

leave a comment »

This design flaw was discovered by Tavis Ormandy and you can find the buggy code at sound/core/seq/oss/seq_oss_init.c like this…

/*
 * open sequencer device
 */
int
snd_seq_oss_open(struct file *file, int level)
{
        int i, rc;
        struct seq_oss_devinfo *dp;

        dp = kzalloc(sizeof(*dp), GFP_KERNEL);
        if (!dp) {
                snd_printk(KERN_ERR "can't malloc device info\n");
                return -ENOMEM;
        }
        debug_printk(("oss_open: dp = %p\n", dp));
      ...
 _error:
        snd_seq_oss_writeq_delete(dp->writeq);
        snd_seq_oss_readq_delete(dp->readq);
        snd_seq_oss_synth_cleanup(dp);
        snd_seq_oss_midi_cleanup(dp);
        delete_port(dp);
        delete_seq_queue(dp->queue);
        kfree(dp);

        return rc;
}

The snd_seq_oss_open() can fail in various situations. If an error is encountered, it will jump to the ‘_error’ label to release the allocated resources. T. Ormandy noticed that delete_port() can lead to a double-free vulnerability. This function is used to delete an ALSA port like this:

static int
delete_port(struct seq_oss_devinfo *dp)
{
        if (dp->port < 0)
                return 0;

        debug_printk(("delete_port %i\n", dp->port));
        return snd_seq_event_port_detach(dp->cseq, dp->port);
}

So, if the sequencer’s port number is negative it will return zero; otherwise it will invoke snd_seq_event_port_detach() on the sequencer’s client number and port number. This latter routine is available at sound/core/seq/seq_ports.c like this:

int snd_seq_event_port_detach(int client, int port)
{
        struct snd_seq_port_info portinfo;
        int  err;

        memset(&portinfo, 0, sizeof(portinfo));
        portinfo.addr.client = client;
        portinfo.addr.port   = port;
        err = snd_seq_kernel_client_ctl(client,
                                        SNDRV_SEQ_IOCTL_DELETE_PORT,
                                        &portinfo);

        return err;
}

This function is used to detach the driver from the provided port. Clearly this is just a wrapper around snd_seq_kernel_client_ctl() in order to call ‘SNDRV_SEQ_IOCTL_DELETE_PORT’ which is leading us to sound/core/seq/seq_clientmgr.c and specifically to the following…

int snd_seq_kernel_client_ctl(int clientid, unsigned int cmd, void *arg)
{
        struct snd_seq_client *client;
        mm_segment_t fs;
        int result;

        client = clientptr(clientid);
        if (client == NULL)
                return -ENXIO;
        fs = snd_enter_user();
        result = snd_seq_do_ioctl(client, cmd, (void __user *)arg);
        snd_leave_user(fs);
        return result;
}

This routine is equivalent to the userland IOCTL call as you can read and by requesting ‘SNDRV_SEQ_IOCTL_DELETE_PORT’ of snd_seq_do_ioctl() we jump to this code which is located in the same source code file like this

static struct seq_ioctl_table {
        unsigned int cmd;
        int (*func)(struct snd_seq_client *client, void __user * arg);
} ioctl_tables[] = {
     ...
        { SNDRV_SEQ_IOCTL_DELETE_PORT, snd_seq_ioctl_delete_port },
     ...
static int snd_seq_do_ioctl(struct snd_seq_client *client, unsigned int cmd,
                            void __user *arg)
{
        struct seq_ioctl_table *p;
     ...
        if (! arg)
                return -EFAULT;
        for (p = ioctl_tables; p->cmd; p++) {
                if (p->cmd == cmd)
                        return p->func(client, arg);
        }
        snd_printd("seq unknown ioctl() 0x%x (type='%c', number=0x%02x)\n",
                   cmd, _IOC_TYPE(cmd), _IOC_NR(cmd));
        return -ENOTTY;
}

So we’re reaching now snd_seq_ioctl_delete_port() and this means that the code below will be executed…

static int snd_seq_ioctl_delete_port(struct snd_seq_client *client,
                                     void __user *arg)
{
        struct snd_seq_port_info info;
        int err;

        /* set passed parameters */
        if (copy_from_user(&info, arg, sizeof(info)))
                return -EFAULT;
        
        /* it is not allowed to remove the port for an another client */
        if (info.addr.client != client->number)
                return -EPERM;

        err = snd_seq_delete_port(client, info.addr.port);
        if (err >= 0)
                snd_seq_system_client_ev_port_exit(client->number, info.addr.port);
        return err;
}

After retrieving the sequencer information through copy_from_user() and checking that the port is valid for the given client, it will call snd_seq_delete_port() passing the requested client and port. This means that we move to sound/core/seq/seq_ports.c and we can read the following:

int snd_seq_delete_port(struct snd_seq_client *client, int port)
{
        unsigned long flags;
        struct snd_seq_client_port *found = NULL, *p;

        mutex_lock(&client->ports_mutex);
        write_lock_irqsave(&client->ports_lock, flags);
        list_for_each_entry(p, &client->ports_list_head, list) {
                if (p->addr.port == port) {
                        /* ok found.  delete from the list at first */
                        list_del(&p->list);
                        client->num_ports--;
                        found = p;
                        break;
                }
        }
        write_unlock_irqrestore(&client->ports_lock, flags);
        mutex_unlock(&client->ports_mutex);
        if (found)
                return port_delete(client, found);
        else
                return -ENOENT;
}

This is a simple MUTEX locked code that will iterate through every client’s port until it finds the port with the given port number. When it finds it, it will invoke list_del() to remove it from the list, decrement the client’s number of ports and after unlocking the MUTEX and if it had found a port, it will call port_delete() on the client for the found port (stored in variable ‘found’).
Now, port_delete() is a quiet simple function that will eventually call kfree() like this:

static int port_delete(struct snd_seq_client *client,
                       struct snd_seq_client_port *port)
{
        /* set closing flag and wait for all port access are gone */
        port->closing = 1;
        snd_use_lock_sync(&port->use_lock); 

        /* clear subscribers info */
        clear_subscriber_list(client, port, &port->c_src, SRC_LIST);
        clear_subscriber_list(client, port, &port->c_dest, DEST_LIST);

        if (port->private_free)
                port->private_free(port->private_data);

        snd_BUG_ON(port->c_src.count != 0);
        snd_BUG_ON(port->c_dest.count != 0);

        kfree(port);
        return 0;
}

As you can also read, before reaching the kfree() call it checks the ‘port->private_free’ pointer and if it’s present it will invoke it. If we have a look at the creation function at sound/core/seq/oss/seq_oss_init.c we can notice the following…

static int
create_port(struct seq_oss_devinfo *dp)
{
        int rc;
        struct snd_seq_port_info port;
        struct snd_seq_port_callback callback;

        memset(&port, 0, sizeof(port));
        port.addr.client = dp->cseq;
      ...
        memset(&callback, 0, sizeof(callback));
        callback.owner = THIS_MODULE;
        callback.private_data = dp;
        callback.event_input = snd_seq_oss_event_input;
        callback.private_free = free_devinfo;
        port.kernel = &callback;
      ...
}

So, this callback function is set to free_devinfo() which frees the device information like this:

static void
free_devinfo(void *private)
{
        struct seq_oss_devinfo *dp = (struct seq_oss_devinfo *)private;

        if (dp->timer)
                snd_seq_oss_timer_delete(dp->timer);
                
        if (dp->writeq)
                snd_seq_oss_writeq_delete(dp->writeq);

        if (dp->readq)
                snd_seq_oss_readq_delete(dp->readq);
        
        kfree(dp);
}

As you can read, it ends up calling kfree() on the device information pointer (named ‘dp’). However, if you move back to the first function of the analysis (which is the snd_seq_oss_open()) you’ll notice that it ends up calling “kfree(dp);” after calling delete_port(). Since the latter leads to the above kfree() inside free_devinfo(), the subsequent call of snd_seq_oss_open() results in an invalid pointer access inside kfree().
Since the ‘dp’ variable is allocated during the opening of the sequencer device, a user can trigger this vulnerability by forcing the raise of an error during the opening of the device in order to jump to label ‘_error’ and let the magic begin. For example, such error could occur if many programs open sequencers until one fails to open.
To fix this, Takashi Iwai of SUSE committed the following patch:

  _error:
-	snd_seq_oss_writeq_delete(dp->writeq);
-	snd_seq_oss_readq_delete(dp->readq);
 	snd_seq_oss_synth_cleanup(dp);
 	snd_seq_oss_midi_cleanup(dp);
-	delete_port(dp);
 	delete_seq_queue(dp->queue);
-	kfree(dp);
+	delete_port(dp);
 
 	return rc;

This code removes snd_seq_oss_writeq_delete() and snd_seq_oss_readq_delete() and kfree(). At last, it moves delete_port() to the end of the execution. Also, the following patch was committed to delete_port():

 delete_port(struct seq_oss_devinfo *dp)
 {
-	if (dp->port < 0)
+	if (dp->port < 0) {
+		kfree(dp);
 		return 0;
+	}
 
 	debug_printk(("delete_port %i\n", dp->port));

To avoid the possible memory leak (since the kfree() of snd_seq_oss_open() was removed) the handling code for negative port numbers is moved to delete_port().

Written by xorl

September 21, 2010 at 21:34

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