]> git.proxmox.com Git - mirror_ubuntu-artful-kernel.git/blobdiff - drivers/char/hw_random/core.c
hwrng: core - Fix current_rng init/cleanup race yet again
[mirror_ubuntu-artful-kernel.git] / drivers / char / hw_random / core.c
index 1500cfd799a79ee4f7d221ca8768469a35f00035..42827fd5f38dc99746a0832f86e1583b291162a0 100644 (file)
@@ -42,6 +42,7 @@
 #include <linux/delay.h>
 #include <linux/slab.h>
 #include <linux/random.h>
+#include <linux/err.h>
 #include <asm/uaccess.h>
 
 
 static struct hwrng *current_rng;
 static struct task_struct *hwrng_fill;
 static LIST_HEAD(rng_list);
+/* Protects rng_list and current_rng */
 static DEFINE_MUTEX(rng_mutex);
+/* Protects rng read functions, data_avail, rng_buffer and rng_fillbuf */
+static DEFINE_MUTEX(reading_mutex);
 static int data_avail;
 static u8 *rng_buffer, *rng_fillbuf;
 static unsigned short current_quality;
@@ -81,13 +85,73 @@ static void add_early_randomness(struct hwrng *rng)
        unsigned char bytes[16];
        int bytes_read;
 
+       mutex_lock(&reading_mutex);
        bytes_read = rng_get_data(rng, bytes, sizeof(bytes), 1);
+       mutex_unlock(&reading_mutex);
        if (bytes_read > 0)
                add_device_randomness(bytes, bytes_read);
 }
 
+static inline void cleanup_rng(struct kref *kref)
+{
+       struct hwrng *rng = container_of(kref, struct hwrng, ref);
+
+       if (rng->cleanup)
+               rng->cleanup(rng);
+
+       complete(&rng->cleanup_done);
+}
+
+static void set_current_rng(struct hwrng *rng)
+{
+       BUG_ON(!mutex_is_locked(&rng_mutex));
+       current_rng = rng;
+}
+
+static void drop_current_rng(void)
+{
+       BUG_ON(!mutex_is_locked(&rng_mutex));
+       if (!current_rng)
+               return;
+
+       /* decrease last reference for triggering the cleanup */
+       kref_put(&current_rng->ref, cleanup_rng);
+       current_rng = NULL;
+}
+
+/* Returns ERR_PTR(), NULL or refcounted hwrng */
+static struct hwrng *get_current_rng(void)
+{
+       struct hwrng *rng;
+
+       if (mutex_lock_interruptible(&rng_mutex))
+               return ERR_PTR(-ERESTARTSYS);
+
+       rng = current_rng;
+       if (rng)
+               kref_get(&rng->ref);
+
+       mutex_unlock(&rng_mutex);
+       return rng;
+}
+
+static void put_rng(struct hwrng *rng)
+{
+       /*
+        * Hold rng_mutex here so we serialize in case they set_current_rng
+        * on rng again immediately.
+        */
+       mutex_lock(&rng_mutex);
+       if (rng)
+               kref_put(&rng->ref, cleanup_rng);
+       mutex_unlock(&rng_mutex);
+}
+
 static inline int hwrng_init(struct hwrng *rng)
 {
+       if (kref_get_unless_zero(&rng->ref))
+               goto skip_init;
+
        if (rng->init) {
                int ret;
 
@@ -95,6 +159,11 @@ static inline int hwrng_init(struct hwrng *rng)
                if (ret)
                        return ret;
        }
+
+       kref_init(&rng->ref);
+       reinit_completion(&rng->cleanup_done);
+
+skip_init:
        add_early_randomness(rng);
 
        current_quality = rng->quality ? : default_quality;
@@ -108,12 +177,6 @@ static inline int hwrng_init(struct hwrng *rng)
        return 0;
 }
 
-static inline void hwrng_cleanup(struct hwrng *rng)
-{
-       if (rng && rng->cleanup)
-               rng->cleanup(rng);
-}
-
 static int rng_dev_open(struct inode *inode, struct file *filp)
 {
        /* enforce read-only access to this chrdev */
@@ -128,6 +191,7 @@ static inline int rng_get_data(struct hwrng *rng, u8 *buffer, size_t size,
                        int wait) {
        int present;
 
+       BUG_ON(!mutex_is_locked(&reading_mutex));
        if (rng->read)
                return rng->read(rng, (void *)buffer, size, wait);
 
@@ -148,25 +212,27 @@ static ssize_t rng_dev_read(struct file *filp, char __user *buf,
        ssize_t ret = 0;
        int err = 0;
        int bytes_read, len;
+       struct hwrng *rng;
 
        while (size) {
-               if (mutex_lock_interruptible(&rng_mutex)) {
-                       err = -ERESTARTSYS;
+               rng = get_current_rng();
+               if (IS_ERR(rng)) {
+                       err = PTR_ERR(rng);
                        goto out;
                }
-
-               if (!current_rng) {
+               if (!rng) {
                        err = -ENODEV;
-                       goto out_unlock;
+                       goto out;
                }
 
+               mutex_lock(&reading_mutex);
                if (!data_avail) {
-                       bytes_read = rng_get_data(current_rng, rng_buffer,
+                       bytes_read = rng_get_data(rng, rng_buffer,
                                rng_buffer_size(),
                                !(filp->f_flags & O_NONBLOCK));
                        if (bytes_read < 0) {
                                err = bytes_read;
-                               goto out_unlock;
+                               goto out_unlock_reading;
                        }
                        data_avail = bytes_read;
                }
@@ -174,7 +240,7 @@ static ssize_t rng_dev_read(struct file *filp, char __user *buf,
                if (!data_avail) {
                        if (filp->f_flags & O_NONBLOCK) {
                                err = -EAGAIN;
-                               goto out_unlock;
+                               goto out_unlock_reading;
                        }
                } else {
                        len = data_avail;
@@ -186,14 +252,15 @@ static ssize_t rng_dev_read(struct file *filp, char __user *buf,
                        if (copy_to_user(buf + ret, rng_buffer + data_avail,
                                                                len)) {
                                err = -EFAULT;
-                               goto out_unlock;
+                               goto out_unlock_reading;
                        }
 
                        size -= len;
                        ret += len;
                }
 
-               mutex_unlock(&rng_mutex);
+               mutex_unlock(&reading_mutex);
+               put_rng(rng);
 
                if (need_resched())
                        schedule_timeout_interruptible(1);
@@ -205,8 +272,10 @@ static ssize_t rng_dev_read(struct file *filp, char __user *buf,
        }
 out:
        return ret ? : err;
-out_unlock:
-       mutex_unlock(&rng_mutex);
+
+out_unlock_reading:
+       mutex_unlock(&reading_mutex);
+       put_rng(rng);
        goto out;
 }
 
@@ -246,8 +315,8 @@ static ssize_t hwrng_attr_current_store(struct device *dev,
                        err = hwrng_init(rng);
                        if (err)
                                break;
-                       hwrng_cleanup(current_rng);
-                       current_rng = rng;
+                       drop_current_rng();
+                       set_current_rng(rng);
                        err = 0;
                        break;
                }
@@ -261,17 +330,15 @@ static ssize_t hwrng_attr_current_show(struct device *dev,
                                       struct device_attribute *attr,
                                       char *buf)
 {
-       int err;
        ssize_t ret;
-       const char *name = "none";
+       struct hwrng *rng;
 
-       err = mutex_lock_interruptible(&rng_mutex);
-       if (err)
-               return -ERESTARTSYS;
-       if (current_rng)
-               name = current_rng->name;
-       ret = snprintf(buf, PAGE_SIZE, "%s\n", name);
-       mutex_unlock(&rng_mutex);
+       rng = get_current_rng();
+       if (IS_ERR(rng))
+               return PTR_ERR(rng);
+
+       ret = snprintf(buf, PAGE_SIZE, "%s\n", rng ? rng->name : "none");
+       put_rng(rng);
 
        return ret;
 }
@@ -342,15 +409,22 @@ static int hwrng_fillfn(void *unused)
        long rc;
 
        while (!kthread_should_stop()) {
-               if (!current_rng)
+               struct hwrng *rng;
+
+               rng = get_current_rng();
+               if (IS_ERR(rng) || !rng)
                        break;
-               rc = rng_get_data(current_rng, rng_fillbuf,
+               mutex_lock(&reading_mutex);
+               rc = rng_get_data(rng, rng_fillbuf,
                                  rng_buffer_size(), 1);
+               mutex_unlock(&reading_mutex);
+               put_rng(rng);
                if (rc <= 0) {
                        pr_warn("hwrng: no data available\n");
                        msleep_interruptible(10000);
                        continue;
                }
+               /* Outside lock, sure, but y'know: randomness. */
                add_hwgenerator_randomness((void *)rng_fillbuf, rc,
                                           rc * current_quality * 8 >> 10);
        }
@@ -400,23 +474,23 @@ int hwrng_register(struct hwrng *rng)
                        goto out_unlock;
        }
 
+       init_completion(&rng->cleanup_done);
+       complete(&rng->cleanup_done);
+
        old_rng = current_rng;
+       err = 0;
        if (!old_rng) {
                err = hwrng_init(rng);
                if (err)
                        goto out_unlock;
-               current_rng = rng;
-       }
-       err = 0;
-       if (!old_rng) {
+               set_current_rng(rng);
+
                err = register_miscdev();
                if (err) {
-                       hwrng_cleanup(rng);
-                       current_rng = NULL;
+                       drop_current_rng();
                        goto out_unlock;
                }
        }
-       INIT_LIST_HEAD(&rng->list);
        list_add_tail(&rng->list, &rng_list);
 
        if (old_rng && !rng->init) {
@@ -439,29 +513,30 @@ EXPORT_SYMBOL_GPL(hwrng_register);
 
 void hwrng_unregister(struct hwrng *rng)
 {
-       int err;
-
        mutex_lock(&rng_mutex);
 
        list_del(&rng->list);
        if (current_rng == rng) {
-               hwrng_cleanup(rng);
-               if (list_empty(&rng_list)) {
-                       current_rng = NULL;
-               } else {
-                       current_rng = list_entry(rng_list.prev, struct hwrng, list);
-                       err = hwrng_init(current_rng);
-                       if (err)
-                               current_rng = NULL;
+               drop_current_rng();
+               if (!list_empty(&rng_list)) {
+                       struct hwrng *tail;
+
+                       tail = list_entry(rng_list.prev, struct hwrng, list);
+
+                       if (hwrng_init(tail) == 0)
+                               set_current_rng(tail);
                }
        }
+
        if (list_empty(&rng_list)) {
+               mutex_unlock(&rng_mutex);
                unregister_miscdev();
                if (hwrng_fill)
                        kthread_stop(hwrng_fill);
-       }
+       } else
+               mutex_unlock(&rng_mutex);
 
-       mutex_unlock(&rng_mutex);
+       wait_for_completion(&rng->cleanup_done);
 }
 EXPORT_SYMBOL_GPL(hwrng_unregister);