GPU drivers sometimes need to read a value from a userspace VA when a
dma-fence signals, compare it with an expected value, and only perform
the deferred work (e.g. eventfd_signal) if the comparison passes. This
is the per-signal filtering pattern used in AMDGPU's EOP eventfd path.

Add optional compare fields to drm_user_fence and a new helper
drm_user_fence_set_compare() to configure them. Supported operators are
==, !=, >=.

When cmp_op is set, drm_user_fence reads the value from userspace via
copy_from_user_nofault() and calls ops->worker() with mm_ok=true only if
the comparison passes. If the process MM is gone or the read fails, the
worker is called with mm_ok=false to allow mandatory housekeeping (e.g.
wake_up()).

Drivers that do not need filtering (e.g. XE) leave cmp_op unset and the
worker is called unconditionally — no behavioral change.

Suggested-by: Christian König <[email protected]>
Cc: Alex Deucher <[email protected]>
Cc: Matthew Brost <[email protected]>
Cc: Thomas Hellström <[email protected]>
Cc: [email protected]
Cc: [email protected]
Signed-off-by: Srinivasan Shanmugam <[email protected]>
Change-Id: I200dd4bf32286ba9bdd2ecb68fffb3c205f9fd52
---
 drivers/gpu/drm/drm_user_fence.c | 97 +++++++++++++++++++++++++++++---
 include/drm/drm_user_fence.h     | 47 +++++++++++++++-
 2 files changed, 135 insertions(+), 9 deletions(-)

diff --git a/drivers/gpu/drm/drm_user_fence.c b/drivers/gpu/drm/drm_user_fence.c
index 0f229b7210a9..06a5683db10d 100644
--- a/drivers/gpu/drm/drm_user_fence.c
+++ b/drivers/gpu/drm/drm_user_fence.c
@@ -2,16 +2,25 @@
 /*
  * Copyright © 2024 The Linux Foundation
  *
- * DRM user fence — extends drm_work_fence with kthread_use_mm() support.
+ * DRM user fence helper.
  *
- * Use this when a GPU fence signals and work needs to access userspace
- * memory (copy_to_user, fault-able operations) from a kthread context.
- * For work that does not require userspace memory access, use
- * drm_work_fence directly.
+ * Extends drm_work_fence with the ability to access userspace memory
+ * from workqueue context by borrowing the process MM via kthread_use_mm().
+ *
+ * Drivers that need to write completion status to userspace (e.g., user
+ * fences, signaling eventfds) embed drm_user_fence and implement
+ * ops->worker() to do the actual write.
+ *
+ * Optionally, drivers may configure a per-signal compare via
+ * drm_user_fence_set_compare(): work is skipped unless the value at a
+ * userspace address matches the expected value at signal time.
  */
 
 #include <linux/kthread.h>
+#include <linux/mm.h>
 #include <linux/sched/mm.h>
+#include <linux/uaccess.h>
+#include <linux/workqueue.h>
 
 #include <drm/drm_user_fence.h>
 
@@ -30,13 +39,46 @@ static void drm_user_fence_do_work(struct drm_work_fence 
*wfence)
        struct drm_user_fence *ufence =
                container_of(wfence, struct drm_user_fence, base);
        struct mm_struct *mm = NULL;
+       bool call_worker = true;
 
        if (mmget_not_zero(ufence->mm)) {
                mm = ufence->mm;
                kthread_use_mm(mm);
        }
 
-       ufence->ops->worker(ufence, !!mm);
+       if (ufence->cmp_op != DRM_USER_FENCE_CMP_NONE &&
+           !(wfence->fence && wfence->fence->error)) {
+               if (!mm) {
+                       call_worker = false;
+               } else {
+                       __le64 raw;
+
+                       /*
+                        * Use copy_from_user_nofault() to prevent a
+                        * userfaultfd-registered page from blocking this
+                        * workqueue thread indefinitely (DoS).
+                        */
+                       if (copy_from_user_nofault(&raw, ufence->cmp_addr,
+                                                  sizeof(raw))) {
+                               call_worker = false;
+                       } else {
+                               /* GPU writes LE; convert before comparing. */
+                               u64 cur_val = le64_to_cpu(raw);
+
+                               if (!drm_user_fence_cmp_match(cur_val,
+                                                             ufence->cmp_value,
+                                                             ufence->cmp_op))
+                                       call_worker = false;
+                       }
+               }
+       }
+
+       /*
+        * Always invoke the worker so drivers can perform mandatory
+        * housekeeping (e.g. wake_up()). Pass false if the compare
+        * filter suppressed the write.
+        */
+       ufence->ops->worker(ufence, call_worker ? !!mm : false);
 
        if (mm) {
                kthread_unuse_mm(mm);
@@ -63,8 +105,47 @@ void drm_user_fence_init(struct drm_user_fence *ufence,
                         const struct drm_user_fence_ops *ops)
 {
        drm_work_fence_init(&ufence->base, wq, &drm_user_fence_wfence_ops);
-       ufence->mm = current->mm;
+       ufence->mm        = current->mm;
        mmgrab(ufence->mm);
-       ufence->ops = ops;
+       ufence->ops       = ops;
+       ufence->cmp_op    = DRM_USER_FENCE_CMP_NONE;
+       ufence->cmp_addr  = NULL;
+       ufence->cmp_value = 0;
 }
 EXPORT_SYMBOL_GPL(drm_user_fence_init);
+
+/**
+ * drm_user_fence_set_compare - Set per-signal compare filter
+ * @ufence: user fence
+ * @addr: 8-byte-aligned userspace address to read from at signal time
+ * @value: expected value to compare against
+ * @op: comparison operator; pass %DRM_USER_FENCE_CMP_NONE to disable
+ *
+ * When @op is not %DRM_USER_FENCE_CMP_NONE, the worker is only called
+ * with mm_ok=true if the value at @addr matches @value according to @op.
+ * The worker is always called for mandatory housekeeping.
+ * If the MM is gone or the read fails, mm_ok is passed as false.
+ *
+ * Must only be called before drm_work_fence_add_callback().
+ */
+void drm_user_fence_set_compare(struct drm_user_fence *ufence,
+                               u64 __user *addr, u64 value,
+                               enum drm_user_fence_cmp op)
+{
+       /*
+        * get_user() of u64 is not atomic on 32-bit — caller should not
+        * reach here on non-64-bit kernels.
+        */
+       if (WARN_ON_ONCE(!IS_ENABLED(CONFIG_64BIT)))
+               return;
+
+       if (op != DRM_USER_FENCE_CMP_NONE) {
+               if (!addr || !IS_ALIGNED((unsigned long)addr, sizeof(u64)))
+                       return;
+       }
+
+       ufence->cmp_addr  = addr;
+       ufence->cmp_value = value;
+       ufence->cmp_op    = op;
+}
+EXPORT_SYMBOL_GPL(drm_user_fence_set_compare);
diff --git a/include/drm/drm_user_fence.h b/include/drm/drm_user_fence.h
index d35438eaa9e2..65287052370f 100644
--- a/include/drm/drm_user_fence.h
+++ b/include/drm/drm_user_fence.h
@@ -40,11 +40,23 @@ struct drm_user_fence_ops {
        void (*destroy)(struct drm_user_fence *ufence);
 };
 
+/**
+ * enum drm_user_fence_cmp - compare operator for per-signal filtering
+ */
+enum drm_user_fence_cmp {
+       DRM_USER_FENCE_CMP_NONE = 0,
+       DRM_USER_FENCE_CMP_EQ,
+       DRM_USER_FENCE_CMP_NEQ,
+       DRM_USER_FENCE_CMP_GTE,
+};
+
 /**
  * struct drm_user_fence - DRM user fence with MM borrowing
  *
  * Extends drm_work_fence with kthread_use_mm() support for drivers
  * that need to access userspace memory when a GPU fence signals.
+ * For work that does not need userspace memory access, use
+ * drm_work_fence directly.
  *
  * Call drm_user_fence_init() at creation and drm_user_fence_add_callback()
  * to arm on a dma-fence. Call drm_user_fence_cancel_sync() before teardown.
@@ -56,11 +68,20 @@ struct drm_user_fence {
        struct mm_struct *mm;
        /** @ops: Driver operations. */
        const struct drm_user_fence_ops *ops;
+       /** @cmp_addr: Userspace address to read for per-signal compare. */
+       u64 __user *cmp_addr;
+       /** @cmp_value: Expected value for per-signal compare. */
+       u64 cmp_value;
+       /** @cmp_op: Compare operator; DRM_USER_FENCE_CMP_NONE disables. */
+       enum drm_user_fence_cmp cmp_op;
 };
 
 void drm_user_fence_init(struct drm_user_fence *ufence,
                         struct workqueue_struct *wq,
                         const struct drm_user_fence_ops *ops);
+void drm_user_fence_set_compare(struct drm_user_fence *ufence,
+                               u64 __user *addr, u64 value,
+                               enum drm_user_fence_cmp op);
 
 /**
  * drm_user_fence_get - Acquire a reference to a user fence
@@ -112,11 +133,35 @@ static inline bool drm_user_fence_cancel(struct 
drm_user_fence *ufence)
  * drm_user_fence_cancel_sync - Cancel callback and wait for worker to finish
  * @ufence: user fence
  *
- * Must be called during teardown before freeing resources. May sleep.
+ * Must be called during teardown before freeing any resources accessed
+ * by ops->worker(). May sleep.
  */
 static inline void drm_user_fence_cancel_sync(struct drm_user_fence *ufence)
 {
        drm_work_fence_cancel_sync(&ufence->base);
 }
 
+/**
+ * drm_user_fence_cmp_match - Test a value against the compare filter
+ * @cur_val: value read from userspace (already converted from LE)
+ * @cmp_value: expected value
+ * @op: comparison operator
+ *
+ * Return: true if the comparison passes, false otherwise.
+ */
+static inline bool drm_user_fence_cmp_match(u64 cur_val, u64 cmp_value,
+                                           enum drm_user_fence_cmp op)
+{
+       switch (op) {
+       case DRM_USER_FENCE_CMP_EQ:
+               return cur_val == cmp_value;
+       case DRM_USER_FENCE_CMP_NEQ:
+               return cur_val != cmp_value;
+       case DRM_USER_FENCE_CMP_GTE:
+               return cur_val >= cmp_value;
+       default:
+               return false;
+       }
+}
+
 #endif /* __DRM_USER_FENCE_H__ */
-- 
2.34.1

Reply via email to