drivers/char: Eliminate use after free
[pandora-kernel.git] / drivers / char / virtio_console.c
index 7671914..8c99bf1 100644 (file)
@@ -78,6 +78,9 @@ struct console {
        /* The hvc device associated with this console port */
        struct hvc_struct *hvc;
 
+       /* The size of the console */
+       struct winsize ws;
+
        /*
         * This number identifies the number that we used to register
         * with hvc in hvc_instantiate() and hvc_alloc(); this is the
@@ -159,6 +162,9 @@ struct port {
         */
        spinlock_t inbuf_lock;
 
+       /* Protect the operations on the out_vq. */
+       spinlock_t outvq_lock;
+
        /* The IO vqs for this port */
        struct virtqueue *in_vq, *out_vq;
 
@@ -184,6 +190,8 @@ struct port {
        /* The 'id' to identify the port with the Host */
        u32 id;
 
+       bool outvq_full;
+
        /* Is the host device open */
        bool host_connected;
 
@@ -405,15 +413,33 @@ static ssize_t send_control_msg(struct port *port, unsigned int event,
        return __send_control_msg(port->portdev, port->id, event, value);
 }
 
-static ssize_t send_buf(struct port *port, void *in_buf, size_t in_count)
+/* Callers must take the port->outvq_lock */
+static void reclaim_consumed_buffers(struct port *port)
+{
+       void *buf;
+       unsigned int len;
+
+       while ((buf = virtqueue_get_buf(port->out_vq, &len))) {
+               kfree(buf);
+               port->outvq_full = false;
+       }
+}
+
+static ssize_t send_buf(struct port *port, void *in_buf, size_t in_count,
+                       bool nonblock)
 {
        struct scatterlist sg[1];
        struct virtqueue *out_vq;
        ssize_t ret;
+       unsigned long flags;
        unsigned int len;
 
        out_vq = port->out_vq;
 
+       spin_lock_irqsave(&port->outvq_lock, flags);
+
+       reclaim_consumed_buffers(port);
+
        sg_init_one(sg, in_buf, in_count);
        ret = virtqueue_add_buf(out_vq, sg, 1, 0, in_buf);
 
@@ -422,14 +448,29 @@ static ssize_t send_buf(struct port *port, void *in_buf, size_t in_count)
 
        if (ret < 0) {
                in_count = 0;
-               goto fail;
+               goto done;
        }
 
-       /* Wait till the host acknowledges it pushed out the data we sent. */
+       if (ret == 0)
+               port->outvq_full = true;
+
+       if (nonblock)
+               goto done;
+
+       /*
+        * Wait till the host acknowledges it pushed out the data we
+        * sent.  This is done for ports in blocking mode or for data
+        * from the hvc_console; the tty operations are performed with
+        * spinlocks held so we can't sleep here.
+        */
        while (!virtqueue_get_buf(out_vq, &len))
                cpu_relax();
-fail:
-       /* We're expected to return the amount of data we wrote */
+done:
+       spin_unlock_irqrestore(&port->outvq_lock, flags);
+       /*
+        * We're expected to return the amount of data we wrote -- all
+        * of it
+        */
        return in_count;
 }
 
@@ -479,9 +520,28 @@ static ssize_t fill_readbuf(struct port *port, char *out_buf, size_t out_count,
 }
 
 /* The condition that must be true for polling to end */
-static bool wait_is_over(struct port *port)
+static bool will_read_block(struct port *port)
 {
-       return port_has_data(port) || !port->host_connected;
+       return !port_has_data(port) && port->host_connected;
+}
+
+static bool will_write_block(struct port *port)
+{
+       bool ret;
+
+       if (!port->host_connected)
+               return true;
+
+       spin_lock_irq(&port->outvq_lock);
+       /*
+        * Check if the Host has consumed any buffers since we last
+        * sent data (this is only applicable for nonblocking ports).
+        */
+       reclaim_consumed_buffers(port);
+       ret = port->outvq_full;
+       spin_unlock_irq(&port->outvq_lock);
+
+       return ret;
 }
 
 static ssize_t port_fops_read(struct file *filp, char __user *ubuf,
@@ -504,7 +564,7 @@ static ssize_t port_fops_read(struct file *filp, char __user *ubuf,
                        return -EAGAIN;
 
                ret = wait_event_interruptible(port->waitqueue,
-                                              wait_is_over(port));
+                                              !will_read_block(port));
                if (ret < 0)
                        return ret;
        }
@@ -530,9 +590,22 @@ static ssize_t port_fops_write(struct file *filp, const char __user *ubuf,
        struct port *port;
        char *buf;
        ssize_t ret;
+       bool nonblock;
 
        port = filp->private_data;
 
+       nonblock = filp->f_flags & O_NONBLOCK;
+
+       if (will_write_block(port)) {
+               if (nonblock)
+                       return -EAGAIN;
+
+               ret = wait_event_interruptible(port->waitqueue,
+                                              !will_write_block(port));
+               if (ret < 0)
+                       return ret;
+       }
+
        count = min((size_t)(32 * 1024), count);
 
        buf = kmalloc(count, GFP_KERNEL);
@@ -545,9 +618,14 @@ static ssize_t port_fops_write(struct file *filp, const char __user *ubuf,
                goto free_buf;
        }
 
-       ret = send_buf(port, buf, count);
+       ret = send_buf(port, buf, count, nonblock);
+
+       if (nonblock && ret > 0)
+               goto out;
+
 free_buf:
        kfree(buf);
+out:
        return ret;
 }
 
@@ -562,7 +640,7 @@ static unsigned int port_fops_poll(struct file *filp, poll_table *wait)
        ret = 0;
        if (port->inbuf)
                ret |= POLLIN | POLLRDNORM;
-       if (port->host_connected)
+       if (!will_write_block(port))
                ret |= POLLOUT;
        if (!port->host_connected)
                ret |= POLLHUP;
@@ -586,6 +664,10 @@ static int port_fops_release(struct inode *inode, struct file *filp)
 
        spin_unlock_irq(&port->inbuf_lock);
 
+       spin_lock_irq(&port->outvq_lock);
+       reclaim_consumed_buffers(port);
+       spin_unlock_irq(&port->outvq_lock);
+
        return 0;
 }
 
@@ -614,6 +696,15 @@ static int port_fops_open(struct inode *inode, struct file *filp)
        port->guest_connected = true;
        spin_unlock_irq(&port->inbuf_lock);
 
+       spin_lock_irq(&port->outvq_lock);
+       /*
+        * There might be a chance that we missed reclaiming a few
+        * buffers in the window of the port getting previously closed
+        * and opening now.
+        */
+       reclaim_consumed_buffers(port);
+       spin_unlock_irq(&port->outvq_lock);
+
        /* Notify host of port being opened */
        send_control_msg(filp->private_data, VIRTIO_CONSOLE_PORT_OPEN, 1);
 
@@ -654,7 +745,7 @@ static int put_chars(u32 vtermno, const char *buf, int count)
        if (!port)
                return -EPIPE;
 
-       return send_buf(port, (void *)buf, count);
+       return send_buf(port, (void *)buf, count, false);
 }
 
 /*
@@ -685,22 +776,14 @@ static int get_chars(u32 vtermno, char *buf, int count)
 static void resize_console(struct port *port)
 {
        struct virtio_device *vdev;
-       struct winsize ws;
 
        /* The port could have been hot-unplugged */
-       if (!port)
+       if (!port || !is_console_port(port))
                return;
 
        vdev = port->portdev->vdev;
-       if (virtio_has_feature(vdev, VIRTIO_CONSOLE_F_SIZE)) {
-               vdev->config->get(vdev,
-                                 offsetof(struct virtio_console_config, cols),
-                                 &ws.ws_col, sizeof(u16));
-               vdev->config->get(vdev,
-                                 offsetof(struct virtio_console_config, rows),
-                                 &ws.ws_row, sizeof(u16));
-               hvc_resize(port->cons.hvc, ws);
-       }
+       if (virtio_has_feature(vdev, VIRTIO_CONSOLE_F_SIZE))
+               hvc_resize(port->cons.hvc, port->cons.ws);
 }
 
 /* We set the configuration at this point, since we now have a tty */
@@ -784,6 +867,13 @@ int init_port_console(struct port *port)
        spin_unlock_irq(&pdrvdata_lock);
        port->guest_connected = true;
 
+       /*
+        * Start using the new console output if this is the first
+        * console to come up.
+        */
+       if (early_put_chars)
+               early_put_chars = NULL;
+
        /* Notify host of port being opened */
        send_control_msg(port, VIRTIO_CONSOLE_PORT_OPEN, 1);
 
@@ -838,6 +928,8 @@ static ssize_t debugfs_read(struct file *filp, char __user *ubuf,
                               "guest_connected: %d\n", port->guest_connected);
        out_offset += snprintf(buf + out_offset, out_count - out_offset,
                               "host_connected: %d\n", port->host_connected);
+       out_offset += snprintf(buf + out_offset, out_count - out_offset,
+                              "outvq_full: %d\n", port->outvq_full);
        out_offset += snprintf(buf + out_offset, out_count - out_offset,
                               "is_console: %s\n",
                               is_console_port(port) ? "yes" : "no");
@@ -855,6 +947,15 @@ static const struct file_operations port_debugfs_ops = {
        .read  = debugfs_read,
 };
 
+static void set_console_size(struct port *port, u16 rows, u16 cols)
+{
+       if (!port || !is_console_port(port))
+               return;
+
+       port->cons.ws.ws_row = rows;
+       port->cons.ws.ws_col = cols;
+}
+
 static unsigned int fill_queue(struct virtqueue *vq, spinlock_t *lock)
 {
        struct port_buffer *buf;
@@ -903,8 +1004,12 @@ static int add_port(struct ports_device *portdev, u32 id)
        port->inbuf = NULL;
        port->cons.hvc = NULL;
 
+       port->cons.ws.ws_row = port->cons.ws.ws_col = 0;
+
        port->host_connected = port->guest_connected = false;
 
+       port->outvq_full = false;
+
        port->in_vq = portdev->in_vqs[port->id];
        port->out_vq = portdev->out_vqs[port->id];
 
@@ -929,6 +1034,7 @@ static int add_port(struct ports_device *portdev, u32 id)
        }
 
        spin_lock_init(&port->inbuf_lock);
+       spin_lock_init(&port->outvq_lock);
        init_waitqueue_head(&port->waitqueue);
 
        /* Fill the in_vq with buffers so the host can send us data. */
@@ -984,7 +1090,7 @@ free_port:
        kfree(port);
 fail:
        /* The host might want to notify management sw about port add failure */
-       send_control_msg(port, VIRTIO_CONSOLE_PORT_READY, 0);
+       __send_control_msg(portdev, id, VIRTIO_CONSOLE_PORT_READY, 0);
        return err;
 }
 
@@ -1024,6 +1130,8 @@ static int remove_port(struct port *port)
        /* Remove unused data this port might have received. */
        discard_port_data(port);
 
+       reclaim_consumed_buffers(port);
+
        /* Remove buffers we queued up for the Host to send us data in. */
        while ((buf = virtqueue_detach_unused_buf(port->in_vq)))
                free_buf(buf);
@@ -1058,14 +1166,8 @@ static void handle_control_message(struct ports_device *portdev,
        switch (cpkt->event) {
        case VIRTIO_CONSOLE_PORT_ADD:
                if (port) {
-                       /*
-                        * This can happen for port 0: we have to
-                        * create a console port during probe() as was
-                        * the behaviour before the MULTIPORT feature.
-                        * On a newer host, when the host tells us
-                        * that a port 0 is available, we should just
-                        * say we have the port all set up.
-                        */
+                       dev_dbg(&portdev->vdev->dev,
+                               "Port %u already added\n", port->id);
                        send_control_msg(port, VIRTIO_CONSOLE_PORT_READY, 1);
                        break;
                }
@@ -1092,15 +1194,34 @@ static void handle_control_message(struct ports_device *portdev,
                 * have to notify the host first.
                 */
                break;
-       case VIRTIO_CONSOLE_RESIZE:
+       case VIRTIO_CONSOLE_RESIZE: {
+               struct {
+                       __u16 rows;
+                       __u16 cols;
+               } size;
+
                if (!is_console_port(port))
                        break;
+
+               memcpy(&size, buf->buf + buf->offset + sizeof(*cpkt),
+                      sizeof(size));
+               set_console_size(port, size.rows, size.cols);
+
                port->cons.hvc->irq_requested = 1;
                resize_console(port);
                break;
+       }
        case VIRTIO_CONSOLE_PORT_OPEN:
                port->host_connected = cpkt->value;
                wake_up_interruptible(&port->waitqueue);
+               /*
+                * If the host port got closed and the host had any
+                * unconsumed buffers, we'll be able to reclaim them
+                * now.
+                */
+               spin_lock_irq(&port->outvq_lock);
+               reclaim_consumed_buffers(port);
+               spin_unlock_irq(&port->outvq_lock);
                break;
        case VIRTIO_CONSOLE_PORT_NAME:
                /*
@@ -1215,13 +1336,29 @@ static void config_intr(struct virtio_device *vdev)
 
        portdev = vdev->priv;
 
-       /*
-        * We'll use this way of resizing only for legacy support.
-        * For newer userspace (VIRTIO_CONSOLE_F_MULTPORT+), use
-        * control messages to indicate console size changes so that
-        * it can be done per-port
-        */
-       resize_console(find_port_by_id(portdev, 0));
+       if (!use_multiport(portdev)) {
+               struct port *port;
+               u16 rows, cols;
+
+               vdev->config->get(vdev,
+                                 offsetof(struct virtio_console_config, cols),
+                                 &cols, sizeof(u16));
+               vdev->config->get(vdev,
+                                 offsetof(struct virtio_console_config, rows),
+                                 &rows, sizeof(u16));
+
+               port = find_port_by_id(portdev, 0);
+               set_console_size(port, rows, cols);
+
+               /*
+                * We'll use this way of resizing only for legacy
+                * support.  For newer userspace
+                * (VIRTIO_CONSOLE_F_MULTPORT+), use control messages
+                * to indicate console size changes so that it can be
+                * done per-port.
+                */
+               resize_console(port);
+       }
 }
 
 static int init_vqs(struct ports_device *portdev)
@@ -1409,22 +1546,22 @@ static int __devinit virtcons_probe(struct virtio_device *vdev)
                        err = -ENOMEM;
                        goto free_vqs;
                }
+       } else {
+               /*
+                * For backward compatibility: Create a console port
+                * if we're running on older host.
+                */
+               add_port(portdev, 0);
        }
 
-       /*
-        * For backward compatibility: if we're running on an older
-        * host, we always want to create a console port.
-        */
-       add_port(portdev, 0);
-
-       /* Start using the new console output. */
-       early_put_chars = NULL;
-
        __send_control_msg(portdev, VIRTIO_CONSOLE_BAD_ID,
                           VIRTIO_CONSOLE_DEVICE_READY, 1);
        return 0;
 
 free_vqs:
+       /* The host might want to notify mgmt sw about device add failure */
+       __send_control_msg(portdev, VIRTIO_CONSOLE_BAD_ID,
+                          VIRTIO_CONSOLE_DEVICE_READY, 0);
        vdev->config->del_vqs(vdev);
        kfree(portdev->in_vqs);
        kfree(portdev->out_vqs);
@@ -1433,9 +1570,6 @@ free_chrdev:
 free:
        kfree(portdev);
 fail:
-       /* The host might want to notify mgmt sw about device add failure */
-       __send_control_msg(portdev, VIRTIO_CONSOLE_BAD_ID,
-                          VIRTIO_CONSOLE_DEVICE_READY, 0);
        return err;
 }