[PATCH v2 2/2] livepatch: Fix UAF of unregistered patch kobjects
From: Yafang Shao <hidden>
Date: 2026-08-16 09:05:02
Subsystem:
live patching, the rest · Maintainers:
Josh Poimboeuf, Jiri Kosina, Miroslav Benes, Petr Mladek, Linus Torvalds
When klp_enable_patch() fails after klp_init_patch_early() has run,
the error path calls klp_free_patch_start() and klp_free_patch_finish().
The former drops the references of all object and function kobjects via
klp_free_objects(), the latter drops the patch kobject reference and
waits for the patch kobject release only:
klp_free_patch_finish():
kobject_put(&patch->kobj);
wait_for_completion(&patch->finish);
With CONFIG_DEBUG_KOBJECT_RELEASE enabled, kobject_put() does not
release the kobject synchronously but schedules a delayed release with
a random delay of up to 4 seconds (see kobject_release() in
lib/kobject.c). Because klp_free_patch_finish() only waits for the
patch kobject release, it may return while object and function kobject
releases are still pending. The caller can then unload the livepatch
module, which frees the klp_object and klp_func structures. The delayed
kobject release callbacks later access this freed memory in
kobject_cleanup(), resulting in a use-after-free.
This issue can occur in two scenarios:
1. The patch kobject was never added to sysfs (e.g., klp_init_patch()
failed at kobject_add()). All child kobjects were only initialized
via kobject_init() but never added to sysfs. They do not hold
references to the patch kobject, so the patch kobject can be
released independently, unblocking patch->finish before the child
releases complete.
2. The patch kobject was added to sysfs, but a subsequent operation
such as klp_add_nops() or klp_init_object() failed. Some child
kobjects were initialized but not yet added to sysfs. These
un-added children do not hold references to the patch kobject
either, so the same race can occur.
Fix this by tracking all static kobject releases with a per-patch
atomic counter (kobj_pending). klp_free_patch_start() counts the
patch kobject plus all static object and function kobjects.
klp_free_patch_finish() waits until kobj_pending reaches zero,
ensuring all kobject releases have completed before the module is
unloaded.
Dynamic objects and nop functions are excluded from the count because
they are freed by their release callbacks (which call kfree) and only
exist when the patch kobject was successfully added to sysfs. For
patches fully added to sysfs, the kobject parent-child reference chain
ensures the patch kobject is released last, so kobj_pending naturally
reaches zero when the patch kobject is released. For patches with
partial initialization failures, kobj_pending ensures all un-added
kobject releases are tracked regardless of release order.
The release callbacks use early return after kfree() for dynamic/nop
kobjects to avoid dereferencing freed memory. Static kobjects are not
freed by their release callbacks, so accessing obj->patch or
func->obj->patch is safe. Furthermore, static functions always belong
to static objects, so func->obj is never freed by a release callback.
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/live-patching/20260809091954.22930-1-laoar.shao@gmail.com/ (local)
Signed-off-by: Yafang Shao <redacted>
---
include/linux/livepatch.h | 6 +++++
kernel/livepatch/core.c | 50 ++++++++++++++++++++++++++++++++++++---
2 files changed, 53 insertions(+), 3 deletions(-)
diff --git a/include/linux/livepatch.h b/include/linux/livepatch.h
index ba9e3988c07c..6375150c2369 100644
--- a/include/linux/livepatch.h
+++ b/include/linux/livepatch.h@@ -33,6 +33,7 @@ * @kobj: kobject for sysfs resources * @node: list node for klp_object func_list * @stack_node: list node for klp_ops func_stack list + * @obj: back pointer to the owning object * @old_size: size of the old function * @new_size: size of the new function * @nop: temporary patch to use the original code again; dyn. allocated
@@ -72,6 +73,7 @@ struct klp_func { struct kobject kobj; struct list_head node; struct list_head stack_node; + struct klp_object *obj; unsigned long old_size, new_size; bool nop; bool patched;
@@ -86,6 +88,7 @@ struct klp_func { * @kobj: kobject for sysfs resources * @func_list: dynamic list of the function entries * @node: list node for klp_patch obj_list + * @patch: back pointer to the owning patch * @mod: kernel module associated with the patched object * (NULL for vmlinux) * @dynamic: temporary object for nop functions; dynamically allocated
@@ -101,6 +104,7 @@ struct klp_object { struct kobject kobj; struct list_head func_list; struct list_head node; + struct klp_patch *patch; struct module *mod; bool dynamic; bool patched;
@@ -127,6 +131,7 @@ struct klp_state { * @list: list node for global list of actively used patches * @kobj: kobject for sysfs resources * @obj_list: dynamic list of the object entries + * @kobj_pending: number of kobjects awaiting release * @enabled: the patch is enabled (but operation may be incomplete) * @forced: was involved in a forced transition * @free_work: patch cleanup from workqueue-context
@@ -140,6 +145,7 @@ struct klp_patch { bool replace; /* internal */ + atomic_t kobj_pending; struct list_head list; struct kobject kobj; struct list_head obj_list;
diff --git a/kernel/livepatch/core.c b/kernel/livepatch/core.c
index a240d1144e89..b5e8242b0877 100644
--- a/kernel/livepatch/core.c
+++ b/kernel/livepatch/core.c@@ -635,7 +635,8 @@ static void klp_kobj_release_patch(struct kobject *kobj) struct klp_patch *patch; patch = container_of(kobj, struct klp_patch, kobj); - complete(&patch->finish); + if (atomic_dec_and_test(&patch->kobj_pending)) + complete(&patch->finish); } static const struct kobj_type klp_ktype_patch = {
@@ -646,12 +647,19 @@ static const struct kobj_type klp_ktype_patch = { static void klp_kobj_release_object(struct kobject *kobj) { + struct klp_patch *patch; struct klp_object *obj; obj = container_of(kobj, struct klp_object, kobj); - if (obj->dynamic) + if (obj->dynamic) { klp_free_object_dynamic(obj); + return; + } + + patch = obj->patch; + if (atomic_dec_and_test(&patch->kobj_pending)) + complete(&patch->finish); } static const struct kobj_type klp_ktype_object = {
@@ -662,12 +670,19 @@ static const struct kobj_type klp_ktype_object = { static void klp_kobj_release_func(struct kobject *kobj) { + struct klp_patch *patch; struct klp_func *func; func = container_of(kobj, struct klp_func, kobj); - if (func->nop) + if (func->nop) { klp_free_func_nop(func); + return; + } + + patch = func->obj->patch; + if (atomic_dec_and_test(&patch->kobj_pending)) + complete(&patch->finish); } static const struct kobj_type klp_ktype_func = {
@@ -737,9 +752,35 @@ static void klp_free_objects_dynamic(struct klp_patch *patch) */ static void klp_free_patch_start(struct klp_patch *patch) { + struct klp_object *obj; + struct klp_func *func; + if (!list_empty(&patch->list)) list_del(&patch->list); + /* + * Count all static kobjects (patch + objects + funcs) so that + * klp_free_patch_finish() can wait for all their releases. + * + * Dynamic objects and nop funcs are excluded because they are + * freed by their release callbacks and only exist when the patch + * kobject was successfully added to sysfs. Static kobjects may + * or may not have been added to sysfs (e.g. if klp_init_object() + * failed after kobject_add() for the patch succeeded). Counting + * all of them ensures delayed releases are tracked regardless. + */ + atomic_set(&patch->kobj_pending, 1); + klp_for_each_object(patch, obj) { + if (obj->dynamic) + continue; + atomic_inc(&patch->kobj_pending); + klp_for_each_func(obj, func) { + if (func->nop) + continue; + atomic_inc(&patch->kobj_pending); + } + } + klp_free_objects(patch); }
@@ -944,6 +985,7 @@ static void klp_init_func_early(struct klp_object *obj, { kobject_init(&func->kobj, &klp_ktype_func); list_add_tail(&func->node, &obj->func_list); + func->obj = obj; } static void klp_init_object_early(struct klp_patch *patch,
@@ -952,6 +994,7 @@ static void klp_init_object_early(struct klp_patch *patch, INIT_LIST_HEAD(&obj->func_list); kobject_init(&obj->kobj, &klp_ktype_object); list_add_tail(&obj->node, &patch->obj_list); + obj->patch = patch; } static void klp_init_patch_early(struct klp_patch *patch)
@@ -962,6 +1005,7 @@ static void klp_init_patch_early(struct klp_patch *patch) INIT_LIST_HEAD(&patch->list); INIT_LIST_HEAD(&patch->obj_list); kobject_init(&patch->kobj, &klp_ktype_patch); + atomic_set(&patch->kobj_pending, 0); patch->enabled = false; patch->forced = false; INIT_WORK(&patch->free_work, klp_free_patch_work_fn);
--
2.52.0