[PATCH 3/8] char: misc: Introduce misc_sync_register()

Tzung-Bi Shih
2026-04-27T13:47:45Z

Introduce misc_sync_register() to support synchronous file operations
for misc devices. This aims to prevent Use-After-Free errors when a
device is deregistered while file operations are still in progress or
files are open.

It creates a synchronization context that wraps supported file
operations and ensures the device is still registered before invoking
the file operations.

The minor number is deferred from being freed immediately on
deregistration and is used as a primary key to search for the
synchronization context in `misc_sync_ctx_list` after the device is
unregistered.

Performance impact:
- All file operations are serialized by a global lock.
- All file operations perform a linear search to find the corresponding
miscdevice.

Signed-off-by: Tzung-Bi Shih <tzungbi@kernel.org>
---
drivers/char/misc.c | 123 ++++++++++++++++++++++++++++++++++++-
include/linux/miscdevice.h | 10 +++
2 files changed, 132 insertions(+), 1 deletion(-)

diff --git a/drivers/char/misc.c b/drivers/char/misc.c
index c26315577d65..87f47bdc7afb 100644
--- a/drivers/char/misc.c
+++ b/drivers/char/misc.c
@@ -56,6 +56,7 @@
* Head entry for the doubly linked miscdevice list
*/
static LIST_HEAD(misc_list);
+static LIST_HEAD(misc_sync_ctx_list);
static DEFINE_MUTEX(misc_mtx);

/*
@@ -130,6 +131,75 @@ static struct miscdevice *misc_find(int minor)
return NULL;
}

+#define DEFINE_SYNC_FOPS(member, ret_type, PROTO, ARGS) \
+ static ret_type misc_sync_##member PROTO \
+ { \
+ struct miscdevice *c; \
+ \
+ guard(mutex)(&misc_mtx); \
+ \
+ c = misc_find(iminor(filp->f_inode)); \
+ if (!c) \
+ return -ENODEV; \
+ \
+ return c->fops->member ARGS; \
+ }
+
+DEFINE_SYNC_FOPS(read, ssize_t,
+ (struct file *filp, char __user *buf, size_t len, loff_t *off),
+ (filp, buf, len, off))
+DEFINE_SYNC_FOPS(unlocked_ioctl, long,
+ (struct file *filp, unsigned int cmd, unsigned long arg),
+ (filp, cmd, arg))
+DEFINE_SYNC_FOPS(compat_ioctl, long,
+ (struct file *filp, unsigned int cmd, unsigned long arg),
+ (filp, cmd, arg))
+
+static void misc_sync_ctx_release(struct kref *kref)
+{
+ struct miscdevice_sync_ctx *ctx = container_of(kref, typeof(*ctx), kref);
+
+ misc_minor_free(ctx->minor);
+ list_del(&ctx->list);
+ kfree(ctx);
+}
+
+static int misc_sync_release(struct inode *inode, struct file *filp)
+{
+ int minor = iminor(filp->f_inode);
+ struct miscdevice *c;
+ struct miscdevice_sync_ctx *iter, *ctx = NULL;
+
+ guard(mutex)(&misc_mtx);
+
+ c = misc_find(minor);
+ if (c) {
+ /* The miscdevice is still registered. */
+ ctx = c->sync_ctx;
+ } else {
+ /* The miscdeivce is unregistered. Search in the list. */
+ list_for_each_entry(iter, &misc_sync_ctx_list, list) {
+ if (iter->minor == minor) {
+ ctx = iter;
+ break;
+ }
+ }
+ if (!ctx) {
+ pr_err("Cannot find miscdevice_sync_ctx\n");
+ return -ENOENT;
+ }
+ }
+
+ /* Restore it so that the corresponding fops_put() works. */
+ filp->f_op = ctx->orig_fops;
+ kref_put(&ctx->kref, misc_sync_ctx_release);
+
+ /* Call to the original .release() if any. */
+ if (filp->f_op->release)
+ return filp->f_op->release(inode, filp);
+ return 0;
+}
+
static int misc_open(struct inode *inode, struct file *file)
{
int minor = iminor(inode);
@@ -166,6 +236,10 @@ static int misc_open(struct inode *inode, struct file *file)

err = 0;
replace_fops(file, new_fops);
+ if (c->sync_ctx) {
+ file->f_op = &c->sync_ctx->fops;
+ kref_get(&c->sync_ctx->kref);
+ }
if (file->f_op->open)
err = file->f_op->open(inode, file);
fail:
@@ -280,12 +354,59 @@ void misc_deregister(struct miscdevice *misc)
guard(mutex)(&misc_mtx);
list_del_init(&misc->list);
device_destroy(&misc_class, MKDEV(MISC_MAJOR, misc->minor));
- misc_minor_free(misc->minor);
+
+ /* Defer to free the minor number for sync fops */
+ if (!misc->sync_ctx) {
+ misc_minor_free(misc->minor);
+ } else {
+ list_add(&misc->sync_ctx->list, &misc_sync_ctx_list);
+ kref_put(&misc->sync_ctx->kref, misc_sync_ctx_release);
+ }
+
if (misc->minor > MISC_DYNAMIC_MINOR)
misc->minor = MISC_DYNAMIC_MINOR;
}
EXPORT_SYMBOL(misc_deregister);

+int misc_sync_register(struct miscdevice *misc)
+{
+ struct miscdevice_sync_ctx *ctx;
+ int ret;
+
+ ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
+ if (!ctx)
+ return -ENOMEM;
+
+ ret = misc_register(misc);
+ if (ret) {
+ kfree(ctx);
+ return ret;
+ }
+
+ ctx->minor = misc->minor;
+ kref_init(&ctx->kref);
+ ctx->orig_fops = misc->fops;
+ INIT_LIST_HEAD(&ctx->list);
+
+ /* Use any fops as default in case the misc sync doesn't support them. */
+ memcpy(&ctx->fops, misc->fops, sizeof(struct file_operations));
+
+ /* Override fops that support sync. */
+ if (misc->fops->read)
+ ctx->fops.read = misc_sync_read;
+ if (misc->fops->unlocked_ioctl)
+ ctx->fops.unlocked_ioctl = misc_sync_unlocked_ioctl;
+ if (misc->fops->compat_ioctl)
+ ctx->fops.compat_ioctl = misc_sync_compat_ioctl;
+
+ /* .release() is used to drop the reference to the sync context. */
+ ctx->fops.release = misc_sync_release;
+
+ misc->sync_ctx = ctx;
+ return 0;
+}
+EXPORT_SYMBOL(misc_sync_register);
+
static int __init misc_init(void)
{
int err;
diff --git a/include/linux/miscdevice.h b/include/linux/miscdevice.h
index fa9000f68523..a60d8281e1dd 100644
--- a/include/linux/miscdevice.h
+++ b/include/linux/miscdevice.h
@@ -81,6 +81,14 @@
*/
#define MISC_DYNAMIC_MINOR 255

+struct miscdevice_sync_ctx {
+ int minor;
+ struct kref kref;
+ const struct file_operations *orig_fops;
+ struct list_head list;
+ struct file_operations fops;
+};
+
struct miscdevice {
int minor;
const char *name;
@@ -91,10 +99,12 @@ struct miscdevice {
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
+ struct miscdevice_sync_ctx *sync_ctx;
};

extern int misc_register(struct miscdevice *misc);
extern void misc_deregister(struct miscdevice *misc);
+extern int misc_sync_register(struct miscdevice *misc);

/*
* Helper macro for drivers that don't do anything special in the initcall.
--
2.51.0