* [PATCH 1/5] module/dups: Fix use-after-free in kmod_dup_req lifetime handling
2026-07-20 12:15 [PATCH 0/5] module/dups: Fix a use-after-free and improve the code Petr Pavlu
@ 2026-07-20 12:15 ` Petr Pavlu
2026-07-20 12:15 ` [PATCH 2/5] module/dups: Inform duplicate requests about the result directly Petr Pavlu
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Petr Pavlu @ 2026-07-20 12:15 UTC (permalink / raw)
To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen
Cc: Aaron Tomlin, linux-modules, linux-kernel
The kmod dups code uses RCU to ensure that a kmod_dup_req instance is freed
only after it is no longer referenced. When releasing an instance, the
kmod_dup_request_delete() function removes the kmod_dup_req from the
dup_kmod_reqs list, waits via synchronize_rcu() and finally frees it.
However, this doesn't work correctly because parallel users referencing the
instance in kmod_dup_request_exists_wait() don't enter an RCU read-side
critical section. This can result in a use-after-free.
The kmod_dup_request_exists_wait() function may need to hold a valid
reference to a kmod_dup_req instance across a blocking wait until the
corresponding modprobe command completes. This makes it unsuitable for RCU.
Fix the issue by changing the lifecycle management of kmod_dup_req to use
reference counting.
Fixes: 8660484ed1cf ("module: add debugging auto-load duplicate module support")
Signed-off-by: Petr Pavlu <petr.pavlu@suse.com>
---
kernel/module/dups.c | 43 ++++++++++++++++++++++++++++++++-----------
1 file changed, 32 insertions(+), 11 deletions(-)
diff --git a/kernel/module/dups.c b/kernel/module/dups.c
index 1d720a5311ba..e1828e865edd 100644
--- a/kernel/module/dups.c
+++ b/kernel/module/dups.c
@@ -30,6 +30,7 @@
#include <linux/ptrace.h>
#include <linux/async.h>
#include <linux/uaccess.h>
+#include <linux/refcount.h>
#include "internal.h"
@@ -38,13 +39,12 @@
static bool enable_dups_trace = IS_ENABLED(CONFIG_MODULE_DEBUG_AUTOLOAD_DUPS_TRACE);
module_param(enable_dups_trace, bool_enable_only, 0644);
-/*
- * Protects dup_kmod_reqs list, adds / removals with RCU.
- */
+/* A mutex-protected list of active kmod requests. */
static DEFINE_MUTEX(kmod_dup_mutex);
static LIST_HEAD(dup_kmod_reqs);
struct kmod_dup_req {
+ refcount_t refcount;
struct list_head list;
char name[MODULE_NAME_LEN];
struct completion first_req_done;
@@ -53,12 +53,24 @@ struct kmod_dup_req {
int dup_ret;
};
+static void get_kmod_req(struct kmod_dup_req *kmod_req)
+{
+ refcount_inc(&kmod_req->refcount);
+}
+
+static void put_kmod_req(struct kmod_dup_req *kmod_req)
+{
+ if (refcount_dec_and_test(&kmod_req->refcount))
+ kfree(kmod_req);
+}
+
static struct kmod_dup_req *kmod_dup_request_lookup(char *module_name)
{
struct kmod_dup_req *kmod_req;
- list_for_each_entry_rcu(kmod_req, &dup_kmod_reqs, list,
- lockdep_is_held(&kmod_dup_mutex)) {
+ lockdep_assert_held(&kmod_dup_mutex);
+
+ list_for_each_entry(kmod_req, &dup_kmod_reqs, list) {
if (strlen(kmod_req->name) == strlen(module_name) &&
!memcmp(kmod_req->name, module_name, strlen(module_name))) {
return kmod_req;
@@ -87,10 +99,10 @@ static void kmod_dup_request_delete(struct work_struct *work)
* just returning 0.
*/
mutex_lock(&kmod_dup_mutex);
- list_del_rcu(&kmod_req->list);
- synchronize_rcu();
+ list_del(&kmod_req->list);
mutex_unlock(&kmod_dup_mutex);
- kfree(kmod_req);
+
+ put_kmod_req(kmod_req);
}
static void kmod_dup_request_complete(struct work_struct *work)
@@ -129,6 +141,7 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
if (!new_kmod_req)
return false;
+ refcount_set(&new_kmod_req->refcount, 1);
memcpy(new_kmod_req->name, module_name, strlen(module_name));
INIT_WORK(&new_kmod_req->complete_work, kmod_dup_request_complete);
INIT_DELAYED_WORK(&new_kmod_req->delete_work, kmod_dup_request_delete);
@@ -161,10 +174,12 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
* keep tab on duplicates later.
*/
pr_debug("New request_module() for %s\n", module_name);
- list_add_rcu(&new_kmod_req->list, &dup_kmod_reqs);
+ list_add(&new_kmod_req->list, &dup_kmod_reqs);
mutex_unlock(&kmod_dup_mutex);
return false;
}
+
+ get_kmod_req(kmod_req);
mutex_unlock(&kmod_dup_mutex);
/* We are dealing with a duplicate request now */
@@ -194,7 +209,7 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
* calls bail out right away.
*/
*dup_ret = 0;
- return true;
+ goto out;
}
/*
@@ -209,12 +224,14 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
TASK_KILLABLE);
if (ret) {
*dup_ret = ret;
- return true;
+ goto out;
}
/* Now the duplicate request has the same exact return value as the first request */
*dup_ret = kmod_req->dup_ret;
+out:
+ put_kmod_req(kmod_req);
return true;
}
@@ -224,6 +241,10 @@ void kmod_dup_request_announce(char *module_name, int ret)
mutex_lock(&kmod_dup_mutex);
+ /*
+ * Find the entry previously added in kmod_dup_request_exists_wait()
+ * that is owned by the current task.
+ */
kmod_req = kmod_dup_request_lookup(module_name);
if (!kmod_req)
goto out;
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 2/5] module/dups: Inform duplicate requests about the result directly
2026-07-20 12:15 [PATCH 0/5] module/dups: Fix a use-after-free and improve the code Petr Pavlu
2026-07-20 12:15 ` [PATCH 1/5] module/dups: Fix use-after-free in kmod_dup_req lifetime handling Petr Pavlu
@ 2026-07-20 12:15 ` Petr Pavlu
2026-07-20 12:15 ` [PATCH 3/5] module/dups: Use scoped guards for kmod_dup_mutex Petr Pavlu
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Petr Pavlu @ 2026-07-20 12:15 UTC (permalink / raw)
To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen
Cc: Aaron Tomlin, linux-modules, linux-kernel
When kmod_dup_request_announce() announces the completion of
a request_module() call to duplicate waiters, it queues a work item to
invoke kmod_dup_request_complete(), and only that function calls
complete_all().
This adds an arbitrary delay that is unnecessary and provides little
benefit. Call complete_all() directly from kmod_dup_request_announce()
instead.
Signed-off-by: Petr Pavlu <petr.pavlu@suse.com>
---
kernel/module/dups.c | 50 +++++++++++---------------------------------
1 file changed, 12 insertions(+), 38 deletions(-)
diff --git a/kernel/module/dups.c b/kernel/module/dups.c
index e1828e865edd..f0bdd7560d7e 100644
--- a/kernel/module/dups.c
+++ b/kernel/module/dups.c
@@ -48,7 +48,6 @@ struct kmod_dup_req {
struct list_head list;
char name[MODULE_NAME_LEN];
struct completion first_req_done;
- struct work_struct complete_work;
struct delayed_work delete_work;
int dup_ret;
};
@@ -105,29 +104,6 @@ static void kmod_dup_request_delete(struct work_struct *work)
put_kmod_req(kmod_req);
}
-static void kmod_dup_request_complete(struct work_struct *work)
-{
- struct kmod_dup_req *kmod_req;
-
- kmod_req = container_of(work, struct kmod_dup_req, complete_work);
-
- /*
- * This will ensure that the kernel will let all the waiters get
- * informed its time to check the return value. It's time to
- * go home.
- */
- complete_all(&kmod_req->first_req_done);
-
- /*
- * Now that we have allowed prior request_module() calls to go on
- * with life, let's schedule deleting this entry. We don't have
- * to do it right away, but we *eventually* want to do it so to not
- * let this linger forever as this is just a boot optimization for
- * possible abuses of vmalloc() incurred by finit_module() thrashing.
- */
- queue_delayed_work(system_dfl_wq, &kmod_req->delete_work, 60 * HZ);
-}
-
bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
{
struct kmod_dup_req *kmod_req, *new_kmod_req;
@@ -143,7 +119,6 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
refcount_set(&new_kmod_req->refcount, 1);
memcpy(new_kmod_req->name, module_name, strlen(module_name));
- INIT_WORK(&new_kmod_req->complete_work, kmod_dup_request_complete);
INIT_DELAYED_WORK(&new_kmod_req->delete_work, kmod_dup_request_delete);
init_completion(&new_kmod_req->first_req_done);
@@ -246,23 +221,22 @@ void kmod_dup_request_announce(char *module_name, int ret)
* that is owned by the current task.
*/
kmod_req = kmod_dup_request_lookup(module_name);
+ mutex_unlock(&kmod_dup_mutex);
+
if (!kmod_req)
- goto out;
+ return;
kmod_req->dup_ret = ret;
+ /* Inform all the duplicate waiters to check the return value. */
+ complete_all(&kmod_req->first_req_done);
+
/*
- * If we complete() here we may allow duplicate threads
- * to continue before the first one that submitted the
- * request. We're in no rush also, given that each and
- * every bounce back to userspace is slow we avoid that
- * with a slight delay here. So queueue up the completion
- * and let duplicates suffer, just wait a tad bit longer.
- * There is no rush. But we also don't want to hold the
- * caller up forever or introduce any boot delays.
+ * Now that we have allowed prior request_module() calls to go on
+ * with life, let's schedule deleting this entry. We don't have
+ * to do it right away, but we *eventually* want to do it so to not
+ * let this linger forever as this is just a boot optimization for
+ * possible abuses of vmalloc() incurred by finit_module() thrashing.
*/
- queue_work(system_dfl_wq, &kmod_req->complete_work);
-
-out:
- mutex_unlock(&kmod_dup_mutex);
+ queue_delayed_work(system_dfl_wq, &kmod_req->delete_work, 60 * HZ);
}
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 3/5] module/dups: Use scoped guards for kmod_dup_mutex
2026-07-20 12:15 [PATCH 0/5] module/dups: Fix a use-after-free and improve the code Petr Pavlu
2026-07-20 12:15 ` [PATCH 1/5] module/dups: Fix use-after-free in kmod_dup_req lifetime handling Petr Pavlu
2026-07-20 12:15 ` [PATCH 2/5] module/dups: Inform duplicate requests about the result directly Petr Pavlu
@ 2026-07-20 12:15 ` Petr Pavlu
2026-07-20 12:15 ` [PATCH 4/5] module/dups: Use strcmp() to compare module names Petr Pavlu
2026-07-20 12:15 ` [PATCH 5/5] module/dups: Clean up includes Petr Pavlu
4 siblings, 0 replies; 6+ messages in thread
From: Petr Pavlu @ 2026-07-20 12:15 UTC (permalink / raw)
To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen
Cc: Aaron Tomlin, linux-modules, linux-kernel
Using scoped guards for kmod_dup_mutex shortens the code and clarifies
where the lock is taken in kmod_dup_request_exists_wait().
Signed-off-by: Petr Pavlu <petr.pavlu@suse.com>
---
kernel/module/dups.c | 26 +++++++++++---------------
1 file changed, 11 insertions(+), 15 deletions(-)
diff --git a/kernel/module/dups.c b/kernel/module/dups.c
index f0bdd7560d7e..a62619ec057e 100644
--- a/kernel/module/dups.c
+++ b/kernel/module/dups.c
@@ -97,9 +97,8 @@ static void kmod_dup_request_delete(struct work_struct *work)
* kmod. The inneficies there are a call to modprobe and modprobe
* just returning 0.
*/
- mutex_lock(&kmod_dup_mutex);
- list_del(&kmod_req->list);
- mutex_unlock(&kmod_dup_mutex);
+ scoped_guard(mutex, &kmod_dup_mutex)
+ list_del(&kmod_req->list);
put_kmod_req(kmod_req);
}
@@ -122,10 +121,13 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
INIT_DELAYED_WORK(&new_kmod_req->delete_work, kmod_dup_request_delete);
init_completion(&new_kmod_req->first_req_done);
- mutex_lock(&kmod_dup_mutex);
+ scoped_guard(mutex, &kmod_dup_mutex) {
+ kmod_req = kmod_dup_request_lookup(module_name);
+ if (kmod_req) {
+ get_kmod_req(kmod_req);
+ goto dup_req;
+ }
- kmod_req = kmod_dup_request_lookup(module_name);
- if (!kmod_req) {
/*
* If the first request that came through for a module
* was with request_module_nowait() we cannot wait for it
@@ -140,7 +142,6 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
if (!wait) {
kfree(new_kmod_req);
pr_debug("New request_module_nowait() for %s -- cannot track duplicates for this request\n", module_name);
- mutex_unlock(&kmod_dup_mutex);
return false;
}
@@ -150,13 +151,10 @@ bool kmod_dup_request_exists_wait(char *module_name, bool wait, int *dup_ret)
*/
pr_debug("New request_module() for %s\n", module_name);
list_add(&new_kmod_req->list, &dup_kmod_reqs);
- mutex_unlock(&kmod_dup_mutex);
return false;
}
- get_kmod_req(kmod_req);
- mutex_unlock(&kmod_dup_mutex);
-
+dup_req:
/* We are dealing with a duplicate request now */
kfree(new_kmod_req);
@@ -214,14 +212,12 @@ void kmod_dup_request_announce(char *module_name, int ret)
{
struct kmod_dup_req *kmod_req;
- mutex_lock(&kmod_dup_mutex);
-
/*
* Find the entry previously added in kmod_dup_request_exists_wait()
* that is owned by the current task.
*/
- kmod_req = kmod_dup_request_lookup(module_name);
- mutex_unlock(&kmod_dup_mutex);
+ scoped_guard(mutex, &kmod_dup_mutex)
+ kmod_req = kmod_dup_request_lookup(module_name);
if (!kmod_req)
return;
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 4/5] module/dups: Use strcmp() to compare module names
2026-07-20 12:15 [PATCH 0/5] module/dups: Fix a use-after-free and improve the code Petr Pavlu
` (2 preceding siblings ...)
2026-07-20 12:15 ` [PATCH 3/5] module/dups: Use scoped guards for kmod_dup_mutex Petr Pavlu
@ 2026-07-20 12:15 ` Petr Pavlu
2026-07-20 12:15 ` [PATCH 5/5] module/dups: Clean up includes Petr Pavlu
4 siblings, 0 replies; 6+ messages in thread
From: Petr Pavlu @ 2026-07-20 12:15 UTC (permalink / raw)
To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen
Cc: Aaron Tomlin, linux-modules, linux-kernel
Use strcmp() instead of strlen()+memcmp() to compare module names in
kmod_dup_request_lookup(), since all strings are NUL-terminated.
Signed-off-by: Petr Pavlu <petr.pavlu@suse.com>
---
kernel/module/dups.c | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/kernel/module/dups.c b/kernel/module/dups.c
index a62619ec057e..43cec4cec4fd 100644
--- a/kernel/module/dups.c
+++ b/kernel/module/dups.c
@@ -70,11 +70,9 @@ static struct kmod_dup_req *kmod_dup_request_lookup(char *module_name)
lockdep_assert_held(&kmod_dup_mutex);
list_for_each_entry(kmod_req, &dup_kmod_reqs, list) {
- if (strlen(kmod_req->name) == strlen(module_name) &&
- !memcmp(kmod_req->name, module_name, strlen(module_name))) {
+ if (!strcmp(kmod_req->name, module_name))
return kmod_req;
- }
- }
+ }
return NULL;
}
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 5/5] module/dups: Clean up includes
2026-07-20 12:15 [PATCH 0/5] module/dups: Fix a use-after-free and improve the code Petr Pavlu
` (3 preceding siblings ...)
2026-07-20 12:15 ` [PATCH 4/5] module/dups: Use strcmp() to compare module names Petr Pavlu
@ 2026-07-20 12:15 ` Petr Pavlu
4 siblings, 0 replies; 6+ messages in thread
From: Petr Pavlu @ 2026-07-20 12:15 UTC (permalink / raw)
To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen
Cc: Aaron Tomlin, linux-modules, linux-kernel
The kernel/module/dups.c file relies on the following definitions and
associated functions:
* module_param() -> linux/moduleparam.h,
* DEFINE_MUTEX() -> linux/mutex.h,
* LIST_HEAD(), list_for_each_entry(), ... -> linux/list.h,
* refcount_t, refcount_inc(), ... -> linux/refcount.h,
* MODULE_NAME_LEN -> linux/module.h,
* completion, complete_all(), ... -> linux/completion.h,
* delayed_work, work_struct, ... -> linux/workqueue.h,
* lockdep_assert_held() -> linux/lockdep.h,
* strcmp(), memcpy() -> linux/string.h,
* container_of() -> linux/container_of.h,
* scoped_guard() -> linux/cleanup.h,
* kzalloc_obj(), kfree() -> linux/slab.h,
* pr_debug(), pr_warn() -> linux/printk.h,
* WARN() -> linux/bug.h,
* TASK_KILLABLE -> linux/sched.h,
* HZ -> linux/param.h.
Update the file's include list accordingly.
Signed-off-by: Petr Pavlu <petr.pavlu@suse.com>
---
kernel/module/dups.c | 32 ++++++++++++--------------------
1 file changed, 12 insertions(+), 20 deletions(-)
diff --git a/kernel/module/dups.c b/kernel/module/dups.c
index 43cec4cec4fd..76faa662f14d 100644
--- a/kernel/module/dups.c
+++ b/kernel/module/dups.c
@@ -7,30 +7,22 @@
#define pr_fmt(fmt) "module: " fmt
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/list.h>
+#include <linux/lockdep.h>
#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/mutex.h>
+#include <linux/param.h>
+#include <linux/printk.h>
+#include <linux/refcount.h>
#include <linux/sched.h>
-#include <linux/sched/task.h>
-#include <linux/binfmts.h>
-#include <linux/syscalls.h>
-#include <linux/unistd.h>
-#include <linux/kmod.h>
#include <linux/slab.h>
-#include <linux/completion.h>
-#include <linux/cred.h>
-#include <linux/file.h>
+#include <linux/string.h>
#include <linux/workqueue.h>
-#include <linux/security.h>
-#include <linux/mount.h>
-#include <linux/kernel.h>
-#include <linux/init.h>
-#include <linux/resource.h>
-#include <linux/notifier.h>
-#include <linux/suspend.h>
-#include <linux/rwsem.h>
-#include <linux/ptrace.h>
-#include <linux/async.h>
-#include <linux/uaccess.h>
-#include <linux/refcount.h>
#include "internal.h"
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread