If we want to support real hotplugging, we need to block new syscalls from
entering any drm_* fops. We also need to wait for these to finish before
unregistering a device.

This patch introduces drm_dev_get/put_active() which mark a device as
active during file-ops. If a device is unplugged, drm_dev_get_active()
will fail and prevent users from using this device.

Internally, we use rwsems to implement this. It allows simultaneous users
(down_read) and we can block on them (down_write) to wait until they are
done. This way, a drm_dev_unregister() can be called at any time and does
not have to wait for the last drm_release() call.

Note that the current "atomic_t unplugged" approach is not safe. Imagine
an unplugged device but a user-space context which already is beyong the
"drm_device_is_unplugged()" check. We have no way to prevent any following
mmap operation or buffer access. The percpu-rwsem avoids this by
protecting a whole file-op call and waiting with unplugging a device until
all pending calls are finished.

FIXME: We still need to update all the driver's fops in case they don't
use the DRM core stubs. A quick look showed only custom mmap callbacks.

Signed-off-by: David Herrmann <dh.herrmann at gmail.com>
---
 drivers/gpu/drm/Kconfig    |  1 +
 drivers/gpu/drm/drm_drv.c  |  6 +++--
 drivers/gpu/drm/drm_fops.c | 31 +++++++++++++++++-----
 drivers/gpu/drm/drm_stub.c | 64 +++++++++++++++++++++++++++++++++++++++++++---
 include/drm/drmP.h         |  6 +++++
 5 files changed, 96 insertions(+), 12 deletions(-)

diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index a7c54c8..e2a399e 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -11,6 +11,7 @@ menuconfig DRM
        select I2C
        select I2C_ALGOBIT
        select DMA_SHARED_BUFFER
+       select PERCPU_RWSEM
        help
          Kernel-level support for the Direct Rendering Infrastructure (DRI)
          introduced in XFree86 4.0. If you say Y here, you need to select
diff --git a/drivers/gpu/drm/drm_drv.c b/drivers/gpu/drm/drm_drv.c
index c294e89..db5e57b 100644
--- a/drivers/gpu/drm/drm_drv.c
+++ b/drivers/gpu/drm/drm_drv.c
@@ -318,7 +318,7 @@ long drm_ioctl(struct file *filp,

        dev = file_priv->minor->dev;

-       if (drm_device_is_unplugged(dev))
+       if (!drm_dev_get_active(dev))
                return -ENODEV;

        atomic_inc(&dev->ioctl_count);
@@ -412,7 +412,9 @@ long drm_ioctl(struct file *filp,

        if (kdata != stack_kdata)
                kfree(kdata);
-       atomic_dec(&dev->ioctl_count);
+
+       drm_dev_put_active(dev);
+
        if (retcode)
                DRM_DEBUG("ret = %d\n", retcode);
        return retcode;
diff --git a/drivers/gpu/drm/drm_fops.c b/drivers/gpu/drm/drm_fops.c
index f8a3ebc..b75af7d 100644
--- a/drivers/gpu/drm/drm_fops.c
+++ b/drivers/gpu/drm/drm_fops.c
@@ -122,7 +122,7 @@ int drm_open(struct inode *inode, struct file *filp)
        if (!(dev = minor->dev))
                return -ENODEV;

-       if (drm_device_is_unplugged(dev))
+       if (!drm_dev_get_active(dev))
                return -ENODEV;

        if (!dev->open_count++)
@@ -147,7 +147,9 @@ int drm_open(struct inode *inode, struct file *filp)
                if (retcode)
                        goto err_undo;
        }
-       return 0;
+
+       retcode = 0;
+       goto out_active;

 err_undo:
        mutex_lock(&dev->struct_mutex);
@@ -157,6 +159,8 @@ err_undo:
        dev->dev_mapping = old_mapping;
        mutex_unlock(&dev->struct_mutex);
        dev->open_count--;
+out_active:
+       drm_dev_put_active(dev);
        return retcode;
 }
 EXPORT_SYMBOL(drm_open);
@@ -188,9 +192,6 @@ int drm_stub_open(struct inode *inode, struct file *filp)
        if (!(dev = minor->dev))
                goto out;

-       if (drm_device_is_unplugged(dev))
-               goto out;
-
        old_fops = filp->f_op;
        filp->f_op = fops_get(dev->driver->fops);
        if (filp->f_op == NULL) {
@@ -654,14 +655,24 @@ ssize_t drm_read(struct file *filp, char __user *buffer,
                 size_t count, loff_t *offset)
 {
        struct drm_file *file_priv = filp->private_data;
+       struct drm_device *dev = file_priv->minor->dev;
        struct drm_pending_event *e;
        size_t total;
        ssize_t ret;

+       /* No locking needed around "unplugged" as we sleep during wait_event()
+        * below, anyway. We acquire the active device after we woke up so we
+        * never use unplugged devices. */
+       if (dev->unplugged)
+               return -ENODEV;
+
        ret = wait_event_interruptible(file_priv->event_wait,
-                                      !list_empty(&file_priv->event_list));
+                                      !list_empty(&file_priv->event_list) ||
+                                      dev->unplugged);
        if (ret < 0)
                return ret;
+       if (!drm_dev_get_active(dev))
+               return -ENODEV;

        total = 0;
        while (drm_dequeue_event(file_priv, total, count, &e)) {
@@ -675,6 +686,7 @@ ssize_t drm_read(struct file *filp, char __user *buffer,
                e->destroy(e);
        }

+       drm_dev_put_active(dev);
        return total;
 }
 EXPORT_SYMBOL(drm_read);
@@ -682,13 +694,20 @@ EXPORT_SYMBOL(drm_read);
 unsigned int drm_poll(struct file *filp, struct poll_table_struct *wait)
 {
        struct drm_file *file_priv = filp->private_data;
+       struct drm_device *dev = file_priv->minor->dev;
        unsigned int mask = 0;

+       /* signal HUP/IN on device removal */
+       if (!drm_dev_get_active(dev))
+               return POLLHUP | POLLIN | POLLRDNORM;
+
        poll_wait(filp, &file_priv->event_wait, wait);

        if (!list_empty(&file_priv->event_list))
                mask |= POLLIN | POLLRDNORM;

+       drm_dev_put_active(dev);
+
        return mask;
 }
 EXPORT_SYMBOL(drm_poll);
diff --git a/drivers/gpu/drm/drm_stub.c b/drivers/gpu/drm/drm_stub.c
index 4ff1227..c0e76c0 100644
--- a/drivers/gpu/drm/drm_stub.c
+++ b/drivers/gpu/drm/drm_stub.c
@@ -449,9 +449,12 @@ struct drm_device *drm_dev_alloc(struct drm_driver *driver,
        dev->types[4] = _DRM_STAT_LOCKS;
        dev->types[5] = _DRM_STAT_UNLOCKS;

-       if (drm_ht_create(&dev->map_hash, 12))
+       if (percpu_init_rwsem(&dev->active))
                goto err_free;

+       if (drm_ht_create(&dev->map_hash, 12))
+               goto err_rwsem;
+
        ret = drm_ctxbitmap_init(dev);
        if (ret) {
                DRM_ERROR("Cannot allocate memory for context bitmap.\n");
@@ -472,6 +475,8 @@ err_ctxbitmap:
        drm_ctxbitmap_cleanup(dev);
 err_map_hash:
        drm_ht_remove(&dev->map_hash);
+err_rwsem:
+       percpu_free_rwsem(&dev->active);
 err_free:
        kfree(dev);
        return NULL;
@@ -496,6 +501,7 @@ void drm_dev_free(struct drm_device *dev)
        drm_ctxbitmap_cleanup(dev);
        drm_ht_remove(&dev->map_hash);

+       percpu_free_rwsem(&dev->active);
        kfree(dev->devname);
        kfree(dev);
 }
@@ -576,14 +582,21 @@ EXPORT_SYMBOL(drm_dev_register);
  * drm_dev_unregister - Unregister DRM device
  * @dev: Device to unregister
  *
- * Unregister the DRM device from the system. This does the reverse of
- * drm_dev_register() but does not deallocate the device. The caller must call
- * drm_dev_free() to free all resources.
+ * Mark DRM device as unplugged, wait for any pending user request and then
+ * unregister the DRM device from the system. This does the reverse of
+ * drm_dev_register().
  */
 void drm_dev_unregister(struct drm_device *dev)
 {
        struct drm_map_list *r_list, *list_temp;

+       /* Wait for pending users and then mark the device as unplugged. We
+        * must not hold the global-mutex while doing this. Otherwise, calls
+        * like drm_ioctl() or drm_lock() will dead-lock. */
+       percpu_down_write(&dev->active);
+       dev->unplugged = true;
+       percpu_up_write(&dev->active);
+
        drm_lastclose(dev);

        if (dev->driver->unload)
@@ -605,3 +618,46 @@ void drm_dev_unregister(struct drm_device *dev)
        list_del(&dev->driver_item);
 }
 EXPORT_SYMBOL(drm_dev_unregister);
+
+/**
+ * drm_dev_get_active - Mark device as active
+ * @dev: Device to mark
+ *
+ * Whenever a DRM driver performs an action on behalf of user-space, it should
+ * mark the DRM device as active. Once it is done, call drm_dev_put_active() to
+ * release that mark. This allows DRM core to wait for pending user-space
+ * actions before unplugging a device. But this also means, user-space must
+ * not sleep for an indefinite period while a device is marked active.
+ * If you have to sleep for an indefinite period, call drm_dev_put_active() and
+ * try to reacquire the device once you wake up.
+ *
+ * Recursive calls are not allowed! They will dead-lock!
+ *
+ * RETURNS:
+ * True iff the device was marked active and can be used. False if the device
+ * was unplugged and must not be used.
+ */
+bool drm_dev_get_active(struct drm_device *dev)
+{
+       percpu_down_read(&dev->active);
+       if (!dev->unplugged)
+               return true;
+       percpu_up_read(&dev->active);
+       return false;
+}
+EXPORT_SYMBOL(drm_dev_get_active);
+
+/**
+ * drm_dev_put_active - Unmark active device
+ * @dev: Active device to unmark
+ *
+ * This finished a call to drm_dev_get_active(). You must not call it if
+ * drm_dev_get_active() failed.
+ * This marks the device as inactive again, iff no other user currently has the
+ * device marked as active. See drm_dev_get_active().
+ */
+void drm_dev_put_active(struct drm_device *dev)
+{
+       percpu_up_read(&dev->active);
+}
+EXPORT_SYMBOL(drm_dev_put_active);
diff --git a/include/drm/drmP.h b/include/drm/drmP.h
index 381679e..9689173 100644
--- a/include/drm/drmP.h
+++ b/include/drm/drmP.h
@@ -1209,7 +1209,11 @@ struct drm_device {
        /*@} */
        int switch_power_state;

+       /** \name Hotplug Management */
+       /*@{ */
+       struct percpu_rw_semaphore active;      /**< protect active users */
        atomic_t unplugged; /* device has been unplugged or gone away */
+       /*@} */
 };

 #define DRM_SWITCH_POWER_ON 0
@@ -1710,6 +1714,8 @@ struct drm_device *drm_dev_alloc(struct drm_driver 
*driver,
 void drm_dev_free(struct drm_device *dev);
 int drm_dev_register(struct drm_device *dev);
 void drm_dev_unregister(struct drm_device *dev);
+bool drm_dev_get_active(struct drm_device *dev);
+void drm_dev_put_active(struct drm_device *dev);
 int drm_get_minor(struct drm_device *dev, struct drm_minor **minor, int type);
 /*@}*/

-- 
1.8.3.3

Reply via email to