DRM leases are only returned as anonymous file descriptors.
Device managers such as udev and logind cannot discover these leases,
assign them to seats, or hand them to sessions via their device APIs.

Accept O_CREAT in DRM_IOCTL_MODE_CREATE_LEASE to expose the lease
as a DRM class device named after its primary node and lessee ID.
Make the device a sibling of the primary node with DEVTYPE=drm_lease.

The exposed node deliberately reuses the anonymous lease file's state,
including its private data and file operations.
This keeps DRM and driver ioctl handling on the original lessee
without introducing a proxy driver.
The anonymous lease file remains alive while the exposed node is open.
The device is unregistered when the lease is destroyed.

Mirror DRM hotplug uevents to exposed leases so that a display server
monitoring its assigned node observes connector changes.

Userspace can identify these nodes through DEVTYPE=drm_lease.
Udev rules should tag lease nodes as seat masters.
Since they inherit the physical device's ID_PATH,
lease nodes should be excluded from ordinary DRM card by-path symlinks.

Signed-off-by: Andrey Erokhin <[email protected]>
---
Changes in v2:

  * Restrict exposed-lease minor allocation and hotplug iteration to the
    256 minors covered by register_chrdev().
  * Replace get_file_rcu() with get_file_active() when pinning the
    anonymous lease file.
  * Pin the copied file operations under the XArray lock and copy
    f_mapping from the anonymous lease file.
  * Reorder device setup and error unwinding so the XArray entry is
    removed before the final device reference is dropped.
  * Remove the stale <linux/idr.h> include and reformat the commit
    message.

v1: 
https://lore.kernel.org/dri-devel/[email protected]/

RFC questions:

  * Is using O_CREAT to request device-node exposure acceptable UAPI,
    or should this use a DRM-specific flag?
  * Are there lifetime or locking constraints missed by sharing the
    anonymous lease file's private_data and copied file operations?

The kernel only exposes the lease device; a userspace broker is still
needed. For a static multiseat configuration the broker must run before
logind. It opens the physical DRM node, acquires DRM master, creates all
leases from the same lessor, and keeps the lessor and returned lease
file descriptors open. The QEMU test setup used this logind.service
drop-in:

    [Unit]
    Requires=drm-lease-broker.service
    After=drm-lease-broker.service

The QEMU virtio-gpu resources used for the two leases were:

    lease 1 / seat0:
      connector 40 (Virtual-1)
      CRTC 39
      primary plane 35 and cursor plane 36 (implicitly included)

    lease 2 / seat1:
      connector 47 (Virtual-2)
      CRTC 46
      primary plane 42 and cursor plane 43 (implicitly included)

The minimal broker used for this test is available at:
https://gist.github.com/languagelawyer/cfe02f4a0d9ba9726a86f74ecb73f990
Its invocation and output were:

    [root@archlinux ~]# /root/release /dev/dri/card0 [ 40 39 ] [ 47 46 ]
    Created lease 1 for group 1: 40 39
    Created lease 2 for group 2: 47 46

These IDs are specific to this QEMU configuration; a real broker must
enumerate the resources dynamically. DRM_CLIENT_CAP_UNIVERSAL_PLANES was
not enabled, so the compatible planes were added implicitly by DRM. With
that client capability enabled, the broker must include the plane IDs
explicitly.

With the lease devices and separate input devices assigned through udev,
logind reported:

    [root@archlinux ~]# loginctl seat-status seat0
    seat0
    Devices: n/a
             ├─/sys/devices/pci0000:00/0000:00:02.0/drm/card0-lessee-1
             │ [MASTER] drm:card0-lessee-1
             ├─/sys/devices/pci0000:00/0000:00:07.0/virtio6/input/input3
             │ input:input3 "QEMU Virtio Tablet"
             ├─/sys/devices/platform/LNXPWRBN:00/input/input0
             │ input:input0 "Power Button"
             ├─/sys/devices/platform/i8042/serio0/input/input4
             │ input:input4 "AT Translated Set 2 keyboard"
             └─/sys/devices/platform/i8042/serio1/input/input6
               input:input6 "ImExPS/2 Generic Explorer Mouse"
    [root@archlinux ~]# loginctl seat-status seat1
    seat1
    Devices: n/a
             ├─/sys/devices/pci0000:00/0000:00:02.0/drm/card0-lessee-2
             │ [MASTER] drm:card0-lessee-2
             ├─/sys/devices/pci0000:00/0000:00:03.0/virtio2/input/input1
             │ input:input1 "QEMU Virtio Keyboard"
             └─/sys/devices/pci0000:00/0000:00:04.0/virtio3/input/input2
               input:input2 "QEMU Virtio Tablet"

Two Xorg servers ran concurrently with different modes:

    Virtual-1 connected primary 1024x768+0+0
    Virtual-2 connected primary 800x600+0+0

The corresponding systemd/udev change is available at
https://github.com/languagelawyer/systemd/commit/9781cf0be05f6bcec9bd428db86f37e7d3030570
It tags drm_lease devices as seat masters and prevents them from
replacing the physical card's by-path symlink.
It is not part of this kernel patch.

 Documentation/gpu/drm-uapi.rst |  41 +++++++
 drivers/gpu/drm/drm_drv.c      |   5 +
 drivers/gpu/drm/drm_internal.h |   5 +
 drivers/gpu/drm/drm_lease.c    | 205 ++++++++++++++++++++++++++++++++-
 drivers/gpu/drm/drm_sysfs.c    |   2 +
 include/uapi/drm/drm_mode.h    |   7 +-
 6 files changed, 263 insertions(+), 2 deletions(-)

diff --git a/Documentation/gpu/drm-uapi.rst b/Documentation/gpu/drm-uapi.rst
index 93df92c4ac8c..52f36946874d 100644
--- a/Documentation/gpu/drm-uapi.rst
+++ b/Documentation/gpu/drm-uapi.rst
@@ -48,6 +48,47 @@ DRM Display Resource Leasing
 .. kernel-doc:: drivers/gpu/drm/drm_lease.c
    :doc: drm leasing
 
+Exposing a lease as a device
+----------------------------
+
+By default, :c:macro:`DRM_IOCTL_MODE_CREATE_LEASE` only returns an anonymous
+file descriptor. Passing ``O_CREAT`` in ``drm_mode_create_lease.flags`` also
+registers the lease as a DRM class device. The libdrm wrapper can be used as
+follows::
+
+    uint32_t lessee_id;
+    int lease_fd;
+
+    lease_fd = drmModeCreateLease(lessor_fd, object_ids, object_count,
+                                  O_CLOEXEC | O_CREAT, &lessee_id);
+    if (lease_fd < 0)
+            /* Handle the error. */
+
+The device is a sibling of the primary DRM device and is named after the
+primary node and the lessee ID. For example, lessee 1 of ``card0`` is exposed
+as ``/dev/dri/card0-lessee-1``. Its uevent contains ``DEVTYPE=drm_lease`` so
+that device managers can distinguish it from a DRM primary node.
+
+Opening the device accesses the same lease as the anonymous file descriptor.
+In particular, all opens share the lessee's DRM file private data, including
+its DRM master state, client capabilities and GEM handle namespace. Each open
+device file keeps the lease alive. The device is unregistered after the last
+reference to the lease file is closed. Revoking the lease removes its objects
+but does not unregister the device while references remain open.
+
+Connector hotplug changes generate ``HOTPLUG=1`` change uevents for the
+exposed lease as well as for the primary node. A device manager can make an
+exposed lease available for seat assignment with a rule such as::
+
+    SUBSYSTEM=="drm", ENV{DEVTYPE}=="drm_lease", \
+      ENV{ID_FOR_SEAT}="drm-lease-$kernel", TAG+="seat", \
+      TAG+="master-of-seat"
+
+An exposed lease inherits the physical device's path information. Rules which
+create ``dri/by-path/*-card`` links should therefore restrict those links to
+``DEVTYPE=drm_minor`` so that a lease does not replace its primary node's
+link.
+
 Open-Source Userspace Requirements
 ==================================
 
diff --git a/drivers/gpu/drm/drm_drv.c b/drivers/gpu/drm/drm_drv.c
index c808958a2188..add36fbf693f 100644
--- a/drivers/gpu/drm/drm_drv.c
+++ b/drivers/gpu/drm/drm_drv.c
@@ -1250,6 +1250,7 @@ static void drm_core_exit(void)
        drm_privacy_screen_lookup_exit();
        drm_panic_exit();
        accel_core_exit();
+       drm_lease_cleanup();
        unregister_chrdev(DRM_MAJOR, "drm");
        drm_debugfs_remove_root();
        drm_sysfs_destroy();
@@ -1283,6 +1284,10 @@ static int __init drm_core_init(void)
 
        drm_panic_init();
 
+       ret = drm_lease_init();
+       if (ret < 0)
+               goto error;
+
        drm_privacy_screen_lookup_init();
 
        ret = drm_ras_genl_family_register();
diff --git a/drivers/gpu/drm/drm_internal.h b/drivers/gpu/drm/drm_internal.h
index f893b1e3a596..1d72dd7419b6 100644
--- a/drivers/gpu/drm/drm_internal.h
+++ b/drivers/gpu/drm/drm_internal.h
@@ -101,6 +101,10 @@ int drm_prime_add_buf_handle(struct drm_prime_file_private 
*prime_fpriv,
 void drm_prime_remove_buf_handle(struct drm_prime_file_private *prime_fpriv,
                                 uint32_t handle);
 
+/* drm_lease.c */
+int drm_lease_init(void);
+void drm_lease_cleanup(void);
+
 /* drm_managed.c */
 void drm_managed_release(struct drm_device *dev);
 void drmm_add_final_kfree(struct drm_device *dev, void *container);
@@ -171,6 +175,7 @@ void drm_sysfs_connector_remove_early(struct drm_connector 
*connector);
 void drm_sysfs_connector_remove(struct drm_connector *connector);
 
 void drm_sysfs_lease_event(struct drm_device *dev);
+void drm_lease_uevent(struct drm_device *dev, char *envp[]);
 
 /* drm_gem.c */
 int drm_gem_init(struct drm_device *dev);
diff --git a/drivers/gpu/drm/drm_lease.c b/drivers/gpu/drm/drm_lease.c
index 5d2cf724cbd7..fdd706530d7c 100644
--- a/drivers/gpu/drm/drm_lease.c
+++ b/drivers/gpu/drm/drm_lease.c
@@ -2,8 +2,11 @@
 /*
  * Copyright © 2017 Keith Packard <[email protected]>
  */
+#include <linux/device.h>
 #include <linux/file.h>
+#include <linux/fs.h>
 #include <linux/uaccess.h>
+#include <linux/xarray.h>
 
 #include <drm/drm_auth.h>
 #include <drm/drm_crtc.h>
@@ -71,6 +74,11 @@
 
 static uint64_t drm_lease_idr_object;
 
+#define DRM_EXPOSED_LEASE_MINORS 256
+
+static int drm_exposed_lease_major;
+static DEFINE_XARRAY_ALLOC(drm_exposed_lease_minors_xa);
+
 struct drm_master *drm_lease_owner(struct drm_master *master)
 {
        while (master->lessor != NULL)
@@ -263,10 +271,144 @@ static struct drm_master *drm_lease_create(struct 
drm_master *lessor, struct idr
        return ERR_PTR(error);
 }
 
+struct drm_exposed_lease {
+       struct file *lessee_file;
+       struct drm_master *lessee;
+       struct file_operations fops;
+       struct device kdev;
+};
+
+static const struct device_type drm_exposed_lease_device_type = {
+       .name = "drm_lease",
+};
+
+void drm_lease_uevent(struct drm_device *dev, char *envp[])
+{
+       struct device *kdev;
+       unsigned long minor = 0;
+
+       for (;;) {
+               struct drm_exposed_lease *exposed;
+
+               kdev = NULL;
+               xa_lock(&drm_exposed_lease_minors_xa);
+               while ((exposed = xa_find(&drm_exposed_lease_minors_xa, &minor,
+                                         DRM_EXPOSED_LEASE_MINORS - 1,
+                                         XA_PRESENT))) {
+                       minor++;
+                       if (exposed->lessee->dev == dev) {
+                               kdev = get_device(&exposed->kdev);
+                               break;
+                       }
+               }
+               xa_unlock(&drm_exposed_lease_minors_xa);
+
+               if (!kdev)
+                       return;
+
+               kobject_uevent_env(&kdev->kobj, KOBJ_CHANGE, envp);
+               put_device(kdev);
+       }
+}
+
+static void drm_exposed_device_release(struct device *dev)
+{
+       kfree(container_of(dev, struct drm_exposed_lease, kdev));
+}
+
+static int drm_exposed_lease_release(struct inode *inode, struct file *filp)
+{
+       struct drm_exposed_lease *exposed;
+       struct file *lessee_file = NULL;
+
+       xa_lock(&drm_exposed_lease_minors_xa);
+       exposed = xa_load(&drm_exposed_lease_minors_xa, iminor(inode));
+       if (exposed && exposed->lessee_file)
+               lessee_file = exposed->lessee_file;
+       xa_unlock(&drm_exposed_lease_minors_xa);
+
+       if (lessee_file)
+               fput(lessee_file);
+
+       return 0;
+}
+
+static int drm_expose_lease(struct file *lessee_file,
+                           struct drm_master *lessee)
+{
+       int ret = 0;
+       u32 minor;
+       struct drm_exposed_lease *exposed;
+       struct device *drm_kdev;
+       struct device *kdev;
+
+       exposed = kzalloc_obj(*exposed);
+       if (!exposed)
+               return -ENOMEM;
+
+       exposed->lessee_file = lessee_file;
+       exposed->lessee = lessee;
+       exposed->fops = *lessee_file->f_op;
+       exposed->fops.release = drm_exposed_lease_release;
+
+       drm_kdev = lessee->dev->primary->kdev;
+       kdev = &exposed->kdev;
+
+       device_initialize(kdev);
+       kdev->class = drm_kdev->class;
+       kdev->type = &drm_exposed_lease_device_type;
+       kdev->parent = drm_kdev->parent;
+       kdev->release = drm_exposed_device_release;
+       ret = dev_set_name(kdev, "%s-lessee-%d", dev_name(drm_kdev), 
lessee->lessee_id);
+       if (ret < 0)
+               goto device_put;
+
+       ret = xa_alloc(&drm_exposed_lease_minors_xa, &minor, exposed,
+                      XA_LIMIT(0, DRM_EXPOSED_LEASE_MINORS - 1), GFP_KERNEL);
+       if (ret < 0)
+               goto device_put;
+
+       kdev->devt = MKDEV(drm_exposed_lease_major, minor);
+       ret = device_add(kdev);
+       if (ret < 0)
+               goto minor_free;
+
+       return 0;
+
+minor_free:
+       xa_erase(&drm_exposed_lease_minors_xa, minor);
+
+device_put:
+       put_device(kdev);
+
+       return ret;
+}
+
+static void drm_hide_lease(struct drm_master *master)
+{
+       unsigned long minor;
+       struct drm_exposed_lease *exposed = NULL;
+
+       xa_lock(&drm_exposed_lease_minors_xa);
+       xa_for_each(&drm_exposed_lease_minors_xa, minor, exposed) {
+               if (exposed->lessee == master)
+                       break;
+       }
+       if (exposed)
+               __xa_erase(&drm_exposed_lease_minors_xa, minor);
+       xa_unlock(&drm_exposed_lease_minors_xa);
+
+       if (exposed)
+               device_unregister(&exposed->kdev);
+}
+
 void drm_lease_destroy(struct drm_master *master)
 {
        struct drm_device *dev = master->dev;
 
+       if (master->lessee_id != 0)
+               drm_hide_lease(master);
+
        mutex_lock(&dev->mode_config.idr_mutex);
 
        drm_dbg_lease(dev, "drm_lease_destroy %d\n", master->lessee_id);
@@ -491,7 +633,7 @@ int drm_mode_create_lease_ioctl(struct drm_device *dev,
        if (!drm_core_check_feature(dev, DRIVER_MODESET))
                return -EOPNOTSUPP;
 
-       if (cl->flags && (cl->flags & ~(O_CLOEXEC | O_NONBLOCK))) {
+       if (cl->flags && (cl->flags & ~(O_CLOEXEC | O_NONBLOCK | O_CREAT))) {
                drm_dbg_lease(dev, "invalid flags\n");
                return -EINVAL;
        }
@@ -566,6 +708,14 @@ int drm_mode_create_lease_ioctl(struct drm_device *dev,
        cl->fd = fd;
        cl->lessee_id = lessee->lessee_id;
 
+       if (cl->flags & O_CREAT) {
+               ret = drm_expose_lease(lessee_file, lessee);
+               if (ret) {
+                       fput(lessee_file);
+                       goto out_leases;
+               }
+       }
+
        /* Hook up the fd */
        fd_install(fd, lessee_file);
 
@@ -730,3 +880,56 @@ int drm_mode_revoke_lease_ioctl(struct drm_device *dev,
 
        return ret;
 }
+
+static int drm_lease_open(struct inode *inode, struct file *filp)
+{
+       struct drm_exposed_lease *exposed;
+       struct file *lessee_file = NULL;
+       const struct file_operations *fops = NULL;
+
+       xa_lock(&drm_exposed_lease_minors_xa);
+       exposed = xa_load(&drm_exposed_lease_minors_xa, iminor(inode));
+       if (exposed && exposed->lessee_file) {
+               lessee_file = get_file_active(&exposed->lessee_file);
+               if (lessee_file)
+                       fops = fops_get(&exposed->fops);
+       }
+       xa_unlock(&drm_exposed_lease_minors_xa);
+
+       if (!lessee_file)
+               return -ENODEV;
+       if (!fops) {
+               fput(lessee_file);
+               return -ENODEV;
+       }
+
+       replace_fops(filp, fops);
+       filp->f_mapping = lessee_file->f_mapping;
+       filp->private_data = lessee_file->private_data;
+
+       return 0;
+}
+
+static const struct file_operations drm_lease_fops = {
+       .owner = THIS_MODULE,
+       .open = drm_lease_open,
+};
+
+int drm_lease_init(void)
+{
+       int ret;
+
+       ret = register_chrdev(0, "drm_lease", &drm_lease_fops);
+       if (ret < 0)
+               return ret;
+
+       drm_exposed_lease_major = ret;
+
+       return 0;
+}
+
+void drm_lease_cleanup(void)
+{
+       if (drm_exposed_lease_major > 0)
+               unregister_chrdev(drm_exposed_lease_major, "drm_lease");
+}
diff --git a/drivers/gpu/drm/drm_sysfs.c b/drivers/gpu/drm/drm_sysfs.c
index ef4e923a8728..63a2c6c035b2 100644
--- a/drivers/gpu/drm/drm_sysfs.c
+++ b/drivers/gpu/drm/drm_sysfs.c
@@ -449,6 +449,7 @@ void drm_sysfs_hotplug_event(struct drm_device *dev)
        drm_dbg_kms(dev, "generating hotplug event\n");
 
        kobject_uevent_env(&dev->primary->kdev->kobj, KOBJ_CHANGE, envp);
+       drm_lease_uevent(dev, envp);
 }
 EXPORT_SYMBOL(drm_sysfs_hotplug_event);
 
@@ -474,6 +475,7 @@ void drm_sysfs_connector_hotplug_event(struct drm_connector 
*connector)
                    connector->base.id, connector->name);
 
        kobject_uevent_env(&dev->primary->kdev->kobj, KOBJ_CHANGE, envp);
+       drm_lease_uevent(dev, envp);
 }
 EXPORT_SYMBOL(drm_sysfs_connector_hotplug_event);
 
diff --git a/include/uapi/drm/drm_mode.h b/include/uapi/drm/drm_mode.h
index bd435effdcee..9bc69bca891c 100644
--- a/include/uapi/drm/drm_mode.h
+++ b/include/uapi/drm/drm_mode.h
@@ -1436,7 +1436,12 @@ struct drm_mode_create_lease {
        __u64 object_ids;
        /** @object_count: Number of object ids */
        __u32 object_count;
-       /** @flags: flags for new FD (O_CLOEXEC, etc) */
+       /**
+        * @flags: Flags for the new file descriptor.
+        *
+        * O_CLOEXEC and O_NONBLOCK control the returned file descriptor.
+        * O_CREAT additionally exposes the lease as a DRM class device.
+        */
        __u32 flags;
 
        /** @lessee_id: Return: unique identifier for lessee. */

base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
-- 
2.55.0

Reply via email to