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
