Introduce revocable_replace_fops() to simplify the use of the revocable API with file_operations.
The function, should be called from a driver's ->open(), replaces the fops with a wrapper that automatically handles the `try_access` and `withdraw_access`.
When the file is closed, the wrapper's ->release() restores the original fops and cleanups. This centralizes the revocable logic, making drivers cleaner and easier to maintain.
Signed-off-by: Tzung-Bi Shih tzungbi@kernel.org --- PoC patch.
Known issues: - All file operations are serialized in a mutex unless we want to embed the required context to struct file. - All file operations (for current version) call revocable_try_access() for guaranteeing the resource even if it may be unused in the fops. - To support multiple revocable resources, extend to arrays.
v4: - New in the series.
drivers/base/revocable.c | 141 ++++++++++++++++++++++++++++++++++++++ include/linux/revocable.h | 7 ++ 2 files changed, 148 insertions(+)
diff --git a/drivers/base/revocable.c b/drivers/base/revocable.c index f8dd4363a87b..ebb91231adf5 100644 --- a/drivers/base/revocable.c +++ b/drivers/base/revocable.c @@ -5,8 +5,10 @@ * Revocable resource management */
+#include <linux/cleanup.h> #include <linux/device.h> #include <linux/kref.h> +#include <linux/poll.h> #include <linux/revocable.h> #include <linux/slab.h> #include <linux/srcu.h> @@ -231,3 +233,142 @@ void revocable_withdraw_access(struct revocable *rev) __releases(&rev->rp->srcu) srcu_read_unlock(&rp->srcu, rev->idx); } EXPORT_SYMBOL_GPL(revocable_withdraw_access); + +struct fops_replacement { + struct file *filp; + const struct revocable_operations *rops; + const struct file_operations *orig_fops; + struct revocable *rev; + struct file_operations fops; + struct list_head list; +}; + +/* + * Use a list and linear search for PoC for now. A hash table should be more + * practical. + */ +static LIST_HEAD(fops_replacement_list); +static DEFINE_MUTEX(fops_replacement_mutex); + +/* + * Use `filp` as the search key. + */ +static struct fops_replacement *find_fops_replacement(struct file *filp) +{ + struct fops_replacement *fr; + + guard(mutex)(&fops_replacement_mutex); + list_for_each_entry(fr, &fops_replacement_list, list) { + if (fr->filp == filp) + return fr; + } + + return NULL; +} + +#define GET_FOPS_REPLACEMENT(filp) ({ \ + struct fops_replacement *_fr; \ + int _ret; \ + \ + _fr = find_fops_replacement(filp); \ + if (!_fr) \ + return -ENODEV; \ + \ + _ret = _fr->rops->try_access(_fr->rev, filp->private_data); \ + if (_ret) \ + return _ret; \ + \ + _fr; \ +}) + +static void put_fops_replacement(struct fops_replacement *fr) +{ + revocable_withdraw_access(fr->rev); +} + +DEFINE_FREE(fops_replacement, struct fops_replacement *, if (_T) put_fops_replacement(_T)) + +static ssize_t revocable_fr_read(struct file *filp, char __user *buffer, + size_t length, loff_t *offset) +{ + struct fops_replacement *fr __free(fops_replacement) = + GET_FOPS_REPLACEMENT(filp); + return fr->orig_fops->read(filp, buffer, length, offset); +} + +static __poll_t revocable_fr_poll(struct file *filp, poll_table *wait) +{ + struct fops_replacement *fr __free(fops_replacement) = + GET_FOPS_REPLACEMENT(filp); + return fr->orig_fops->poll(filp, wait); +} + +static long revocable_fr_unlocked_ioctl(struct file *filp, unsigned int cmd, + unsigned long arg) +{ + struct fops_replacement *fr __free(fops_replacement) = + GET_FOPS_REPLACEMENT(filp); + return fr->orig_fops->unlocked_ioctl(filp, cmd, arg); +} + +static void revocable_recover_fops(struct file *filp, + struct fops_replacement *fr) +{ + filp->f_op = fr->orig_fops; + + revocable_free(fr->rev); + scoped_guard(mutex, &fops_replacement_mutex) + list_del(&fr->list); + kfree(fr); +} + +static int revocable_fr_release(struct inode *inode, struct file *filp) +{ + int ret = 0; + struct fops_replacement *fr = GET_FOPS_REPLACEMENT(filp); + + if (fr->orig_fops->release) + ret = fr->orig_fops->release(inode, filp); + put_fops_replacement(fr); + + revocable_recover_fops(filp, fr); + return ret; +} + +/** + * revocable_replace_fops() - Replace the file operations to be revocable-aware. + * + * Should be used only from ->open() instances. + */ +int revocable_replace_fops(struct file *filp, struct revocable_provider *rp, + const struct revocable_operations *rops) +{ + struct fops_replacement *fr; + + fr = kzalloc(sizeof(*fr), GFP_KERNEL); + if (!fr) + return -ENOMEM; + + fr->filp = filp; + fr->rops = rops; + fr->orig_fops = filp->f_op; + fr->rev = revocable_alloc(rp); + if (!fr->rev) + return -ENOMEM; + memcpy(&fr->fops, filp->f_op, sizeof(struct file_operations)); + scoped_guard(mutex, &fops_replacement_mutex) + list_add(&fr->list, &fops_replacement_list); + + fr->fops.release = revocable_fr_release; + + if (filp->f_op->read) + fr->fops.read = revocable_fr_read; + if (filp->f_op->poll) + fr->fops.poll = revocable_fr_poll; + if (filp->f_op->unlocked_ioctl) + fr->fops.unlocked_ioctl = revocable_fr_unlocked_ioctl; + + filp->f_op = &fr->fops; + return 0; +} +EXPORT_SYMBOL_GPL(revocable_replace_fops); diff --git a/include/linux/revocable.h b/include/linux/revocable.h index 7177bf045d9c..9ad1d0198a1e 100644 --- a/include/linux/revocable.h +++ b/include/linux/revocable.h @@ -50,4 +50,11 @@ _label: \ #define REVOCABLE_TRY_ACCESS_WITH(_rev, _res) \ _REVOCABLE_TRY_ACCESS_WITH(_rev, __UNIQUE_ID(label), _res)
+struct revocable_operations { + int (*try_access)(struct revocable *rev, void *data); +}; + +int revocable_replace_fops(struct file *filp, struct revocable_provider *rp, + const struct revocable_operations *rops); + #endif /* __LINUX_REVOCABLE_H */