diff --git a/MAINTAINERS b/MAINTAINERS
index 222b7fc2c3aa..06c82b4a7614 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11905,6 +11905,12 @@ L: linux-input@vger.kernel.org
S: Supported
F: drivers/hid/hid-playstation.c
+HID PLUGINS
+M: Rong Zhang <i@rong.moe>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-plugin.*
+
HID PULSAR DRIVER
M: Nikolas Koesling <nikolas@koesling.info>
L: linux-input@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2481bc01c435..604683b4a4a9 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -92,6 +92,20 @@ config HID_GENERIC
If unsure, say Y.
+menuconfig HID_PLUGIN
+ bool "HID plugins"
+ help
+ Support for HID plugins.
+
+ Say Y here if you want to enhance HID drivers with additional
+ functionalities provided by corresponding plugins.
+
+ Plugins can be either builtin or modules. A plugin compiled as a module
+ is made available to HID drivers by automatically loading it when
+ requested.
+
+ If unsure, say N.
+
config HID_HAPTIC
bool "Haptic touchpad support"
default n
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 43a8eb0f389b..b41aa3f52a94 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -4,6 +4,8 @@
#
hid-y := hid-core.o hid-input.o hid-quirks.o
hid-$(CONFIG_DEBUG_FS) += hid-debug.o
+hid-$(CONFIG_HID_PLUGIN) += hid-plugin.o
+
hid-$(CONFIG_HID_HAPTIC) += hid-haptic.o
obj-$(CONFIG_HID_BPF) += bpf/
diff --git a/drivers/hid/hid-plugin.c b/drivers/hid/hid-plugin.c
new file mode 100644
index 000000000000..a83275df038c
--- /dev/null
+++ b/drivers/hid/hid-plugin.c
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID plugin helpers
+ *
+ * Copyright (c) 2026 Rong Zhang <i@rong.moe>
+ */
+
+#define pr_fmt(fmt) "hid-plugin: " fmt
+
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+#include <linux/compiler.h>
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/printk.h>
+#include <linux/rcupdate.h>
+
+#include "hid-plugin.h"
+
+static const char * const hid_plugin_mod_names[NR_HID_PLUGINS] = {
+};
+
+static struct hid_plugin __rcu *hid_plugins[NR_HID_PLUGINS];
+
+static struct hid_plugin *hid_plugin_try_module_get(enum hid_plugin_id id)
+{
+ guard(rcu)();
+
+ struct hid_plugin *plugin = rcu_dereference(hid_plugins[id]);
+
+ if (!plugin)
+ return NULL;
+
+ /* Is the module unloading? */
+ return try_module_get(plugin->owner) ? plugin : ERR_PTR(-ENXIO);
+}
+
+struct hid_plugin *__hid_plugin_get(enum hid_plugin_id id)
+{
+ struct hid_plugin *plugin;
+ const char *mod_name;
+ int ret;
+
+ /*
+ * Compile the function as a stub if plugin support is enabled but no
+ * plugin is actually enabled. Utilizing dead code elimination is the
+ * best thing we can do as it's impossible to evaluate an enumeration in
+ * macros.
+ */
+ if (NR_HID_PLUGINS <= 0)
+ return ERR_PTR(-ENOENT);
+
+ plugin = hid_plugin_try_module_get(id);
+ if (plugin)
+ return plugin;
+
+ mod_name = hid_plugin_mod_names[id];
+ if (WARN_ON(!mod_name)) /* Huh? Builtin plugins can't reach here. */
+ return ERR_PTR(-ENXIO);
+
+ ret = request_module(mod_name);
+ if (ret) {
+ pr_warn("failed to load [%d]%s: %d\n", id, mod_name, ret);
+ return ERR_PTR(-ENXIO); /* Don't return positive error code. */
+ }
+
+ plugin = hid_plugin_try_module_get(id);
+ return plugin ?: ERR_PTR(-ENXIO);
+}
+EXPORT_SYMBOL_GPL(__hid_plugin_get);
+
+int __hid_plugin_register(enum hid_plugin_id id, struct hid_plugin *plugin)
+{
+ if (NR_HID_PLUGINS <= 0)
+ return -ENOENT;
+
+ /*
+ * The corresponding plugin is the only writer of hid_plugins[id], so
+ * there is no need to protect the read access with RCU or to protect
+ * the write access with an extra lock.
+ *
+ * However, try our best to do some sanity checks.
+ */
+ if (WARN_ON(rcu_access_pointer(hid_plugins[id])))
+ return -EEXIST;
+
+ rcu_assign_pointer(hid_plugins[id], plugin);
+ synchronize_rcu();
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(__hid_plugin_register);
+
+void __hid_plugin_unregister(enum hid_plugin_id id, struct hid_plugin *plugin)
+{
+ if (NR_HID_PLUGINS <= 0)
+ return;
+
+ if (WARN_ON(rcu_access_pointer(hid_plugins[id]) != plugin))
+ return;
+
+ rcu_assign_pointer(hid_plugins[id], NULL);
+ synchronize_rcu();
+}
+EXPORT_SYMBOL_GPL(__hid_plugin_unregister);diff --git a/drivers/hid/hid-plugin.h b/drivers/hid/hid-plugin.h
new file mode 100644
index 000000000000..4038325c51a0
--- /dev/null
+++ b/drivers/hid/hid-plugin.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * HID plugin helpers
+ *
+ * Copyright (c) 2026 Rong Zhang <i@rong.moe>
+ */
+
+#ifndef __HID_PLUGIN_H
+#define __HID_PLUGIN_H
+
+#include <linux/bug.h>
+#include <linux/build_bug.h>
+#include <linux/compiler.h>
+#include <linux/err.h>
+#include <linux/kconfig.h>
+#include <linux/init.h>
+#include <linux/module.h>
+
+enum hid_plugin_id {
+ HID_PLUGIN_DISABLED = -1,
+
+ NR_HID_PLUGINS,
+};
+
+#if !IS_ENABLED(CONFIG_HID_PLUGIN)
+static_assert(NR_HID_PLUGINS == 0);
+#endif
+
+struct hid_plugin {
+ struct module *owner;
+};
+
+struct hid_plugin *__hid_plugin_get(enum hid_plugin_id id);
+int __hid_plugin_register(enum hid_plugin_id id, struct hid_plugin *plugin);
+void __hid_plugin_unregister(enum hid_plugin_id id, struct hid_plugin *plugin);
+
+/**
+ * hid_plugin_enabled() - Check if an HID plugin is enabled.
+ * @id: Unique ID of the desired HID plugin
+ *
+ * Return: boolean.
+ */
+static __always_inline bool hid_plugin_enabled(enum hid_plugin_id id)
+{
+ BUILD_BUG_ON(!__builtin_constant_p(id) || id >= NR_HID_PLUGINS || id < HID_PLUGIN_DISABLED);
+
+ return id != HID_PLUGIN_DISABLED;
+}
+
+/**
+ * hid_plugin_get() - Get an HID plugin and increment its refcount.
+ * @id: Unique ID of the desired HID plugin
+ *
+ * If the plugin is builtin or loaded, a pointer to the plugin is returned with
+ * the plugin's module refcount incremented. If the plugin is unloading during
+ * the process, ERR_PTR(-ENXIO) is returned.
+ *
+ * If the plugin is disabled, ERR_PTR(-ENOENT) is returned.
+ *
+ * Otherwise, try loading the module with request_module() and then increment
+ * its refcount. If the plugin fails to load, ERR_PTR(-ENXIO) is returned.
+ *
+ * Return: The HID plugin corresponding to @id on success, or an ERR_PTR().
+ * Never NULL.
+ */
+static __always_inline struct hid_plugin *hid_plugin_get(enum hid_plugin_id id)
+{
+ if (hid_plugin_enabled(id))
+ return __hid_plugin_get(id);
+
+ return ERR_PTR(-ENOENT);
+}
+
+/**
+ * hid_plugin_put() - Decrement an HID plugin's refcount.
+ * @id: Unique ID of @plugin
+ * @plugin: Pointer to the HID plugin
+ *
+ * Decrement @plugin's refcount, unless it's disabled.
+ */
+static __always_inline void hid_plugin_put(enum hid_plugin_id id, struct hid_plugin *plugin)
+{
+ if (hid_plugin_enabled(id) && !IS_ERR_OR_NULL(plugin))
+ module_put(plugin->owner);
+}
+
+/**
+ * hid_plugin_register() - Register an HID plugin.
+ * @id: Unique ID of @plugin
+ * @plugin: Pointer to the HID plugin
+ */
+static __always_inline int hid_plugin_register(enum hid_plugin_id id, struct hid_plugin *plugin)
+{
+ BUILD_BUG_ON(!hid_plugin_enabled(id));
+
+ return __hid_plugin_register(id, plugin);
+}
+
+/**
+ * hid_plugin_unregister() - Unregister an HID plugin.
+ * @id: Unique ID of @plugin
+ * @plugin: Pointer to the HID plugin
+ */
+static __always_inline void hid_plugin_unregister(enum hid_plugin_id id, struct hid_plugin *plugin)
+{
+ BUILD_BUG_ON(!hid_plugin_enabled(id));
+
+ __hid_plugin_unregister(id, plugin);
+}
+
+/*
+ * Usage:
+ *
+ * struct foo_plugin {
+ * struct hid_plugin hp;
+ * void (*bar)(struct hid_device *hdev);
+ * };
+ *
+ * static struct foo_plugin foo_plugin = {
+ * .hp = {
+ * .owner = THIS_MODULE,
+ * },
+ * .bar = hid_foo_bar,
+ * };
+ * module_hid_plugin(foo, HID_PLUGIN_FOO, &foo_plugin.hp);
+ */
+#define module_hid_plugin(_name, _id, _plugin) \
+static int __init hid_plugin_##_name##_init(void) \
+{ \
+ return hid_plugin_register(_id, _plugin); \
+} \
+subsys_initcall(hid_plugin_##_name##_init); \
+static void __exit hid_plugin_##_name##_exit(void) \
+{ \
+ hid_plugin_unregister(_id, _plugin); \
+} \
+module_exit(hid_plugin_##_name##_exit)
+
+#endif /* __HID_PLUGIN_H */
--
2.55.0