From 7f4ca0d0b40ac1ccd82ebaf1514c2d26c3db71c4 Mon Sep 17 00:00:00 2001 From: Yafang Shao Date: Sun, 30 Aug 2026 13:48:56 +0800 Subject: [PATCH 1/4] livepatch: Fix UAF of unregistered patch kobjects The kobjects of a livepatch are released via kobject_put(). When CONFIG_DEBUG_KOBJECT_RELEASE is 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). klp_free_patch_finish() only waits for the release of the patch kobject: klp_free_patch_finish(): kobject_put(&patch->kobj); wait_for_completion(&patch->finish); patch->finish is completed by the patch kobject's release callback. If the patch kobject was never added to sysfs, or if some child kobjects were initialized but never added to sysfs (e.g. when klp_enable_patch() fails after klp_init_patch_early()), those un-added children do not hold a reference on the patch kobject. kobject_add() is what takes the parent reference, so the patch kobject can be released first, completing patch->finish while the child releases are still pending. The caller then unloads the livepatch module, which destroys the static klp_object and klp_func structures. The delayed child release callbacks later access this freed memory, causing a use-after-free. Fix it by making every child kobject hold an explicit reference on its parent from the moment the object is initialized: klp_init_object_early() takes a reference on the patch kobject and klp_init_func_early() takes a reference on the object kobject. Unlike the reference taken by kobject_add(), these references also exist for objects that are never added to sysfs, and the release callbacks drop them unconditionally. This guarantees the patch kobject is released only after all child kobjects have been released, so patch->finish cannot be completed before the static structures are safe to free. Because kobj->parent is set only by kobject_add(), add explicit back-pointers, obj->patch and func->obj, so the release callbacks can find the parent. Dynamic objects and nop functions are freed by their release callbacks; save the parent pointer before freeing and drop the parent reference afterwards. Reported-by: sashiko-bot Closes: https://lore.kernel.org/all/20260809094046.50ED31F000E9@smtp.kernel.org/ Suggested-by: Petr Mladek Signed-off-by: Yafang Shao Acked-by: Song Liu Reviewed-by: Petr Mladek Tested-by: Petr Mladek Link: https://patch.msgid.link/20260830054857.64758-2-laoar.shao@gmail.com Signed-off-by: Petr Mladek --- include/linux/livepatch.h | 4 ++++ kernel/livepatch/core.c | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/include/linux/livepatch.h b/include/linux/livepatch.h index ba9e3988c07c..5f74f79c22b4 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; diff --git a/kernel/livepatch/core.c b/kernel/livepatch/core.c index a240d1144e89..517fe427ff92 100644 --- a/kernel/livepatch/core.c +++ b/kernel/livepatch/core.c @@ -646,12 +646,15 @@ 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); + patch = obj->patch; if (obj->dynamic) klp_free_object_dynamic(obj); + kobject_put(&patch->kobj); } static const struct kobj_type klp_ktype_object = { @@ -662,12 +665,15 @@ static const struct kobj_type klp_ktype_object = { static void klp_kobj_release_func(struct kobject *kobj) { + struct klp_object *obj; struct klp_func *func; func = container_of(kobj, struct klp_func, kobj); + obj = func->obj; if (func->nop) klp_free_func_nop(func); + kobject_put(&obj->kobj); } static const struct kobj_type klp_ktype_func = { @@ -943,7 +949,9 @@ static void klp_init_func_early(struct klp_object *obj, struct klp_func *func) { kobject_init(&func->kobj, &klp_ktype_func); + kobject_get(&obj->kobj); list_add_tail(&func->node, &obj->func_list); + func->obj = obj; } static void klp_init_object_early(struct klp_patch *patch, @@ -951,7 +959,9 @@ static void klp_init_object_early(struct klp_patch *patch, { INIT_LIST_HEAD(&obj->func_list); kobject_init(&obj->kobj, &klp_ktype_object); + kobject_get(&patch->kobj); list_add_tail(&obj->node, &patch->obj_list); + obj->patch = patch; } static void klp_init_patch_early(struct klp_patch *patch) From 5d791d3396ca4c9e5fd9d20b49386c20d8179eb8 Mon Sep 17 00:00:00 2001 From: Yafang Shao Date: Sun, 30 Aug 2026 13:48:57 +0800 Subject: [PATCH 2/4] selftests/livepatch: filter debug messages in check_result() CONFIG_DEBUG_KOBJECT makes kobject_add_internal(), kobject_uevent_env(), fill_kobj_path() and friends emit pr_debug() messages, and CONFIG_DEBUG_KOBJECT_RELEASE makes kobject_release() emit a pr_info() for every delayed kobject free. All of these carry the "kobject:" prefix via pr_fmt(), e.g.: # --- expected # +++ result # @@ -1,7 +1,13 @@ # % insmod test_modules/test_klp_livepatch.ko # +kobject: 'test_klp_livepatch' (000000009dbf565e): kobject_add_internal: parent: 'module', set: 'module' # +kobject: 'holders' (000000002856f0ae): kobject_add_internal: parent: 'test_klp_livepatch', set: '' # +kobject: 'test_klp_livepatch' (00000000dcae1113): kobject_add_internal: parent: 'livepatch', set: '' # +kobject: 'vmlinux' (000000007b8837e6): kobject_add_internal: parent: 'test_klp_livepatch', set: '' # livepatch: enabling patch 'test_klp_livepatch' # livepatch: 'test_klp_livepatch': initializing patching transition # livepatch: 'test_klp_livepatch': starting patching transition # +kobject: 'test_klp_livepatch' (000000009dbf565e): kobject_uevent_env # +kobject: 'test_klp_livepatch' (000000009dbf565e): fill_kobj_path: path = '/module/test_klp_livepatch' # livepatch: 'test_klp_livepatch': completing patching transition # livepatch: 'test_klp_livepatch': patching complete # % echo 0 > /sys/kernel/livepatch/test_klp_livepatch/enabled # @@ -9,4 +15,17 @@ livepatch: 'test_klp_livepatch': initial # livepatch: 'test_klp_livepatch': starting unpatching transition # livepatch: 'test_klp_livepatch': completing unpatching transition # livepatch: 'test_klp_livepatch': unpatching complete # +kobject: 'test_klp_livepatch' (00000000dcae1113): kobject_release, parent 00000000f8785d63 (delayed 2000) # +kobject: 'test_klp_livepatch' (00000000dcae1113): kobject_cleanup, parent 00000000f8785d63 # +kobject: 'test_klp_livepatch' (00000000dcae1113): auto cleanup kobject_del # +kobject: 'test_klp_livepatch' (00000000dcae1113): calling ktype release # +kobject: 'test_klp_livepatch': free name # % rmmod test_klp_livepatch # +kobject: 'test_klp_livepatch' (000000009dbf565e): kobject_release, parent 0000000052e5c022 (delayed 3000) # +kobject: 'test_klp_livepatch' (000000009dbf565e): kobject_cleanup, parent 0000000052e5c022 # +kobject: 'test_klp_livepatch' (000000009dbf565e): auto cleanup kobject_del # +kobject: 'test_klp_livepatch' (000000009dbf565e): auto cleanup 'remove' event # +kobject: 'test_klp_livepatch' (000000009dbf565e): kobject_uevent_env # +kobject: 'test_klp_livepatch' (000000009dbf565e): fill_kobj_path: path = '/module/test_klp_livepatch' # +kobject: 'test_klp_livepatch' (000000009dbf565e): calling ktype release # +kobject: 'test_klp_livepatch': free name # # ERROR: livepatch kselftest(s) failed not ok 1 selftests: livepatch: test-livepatch.sh # exit=1 The livepatch test modules' kobjects are named "test_klp_*", so these lines match the check_result() grep for "test_klp" and leak into the result. The extra lines no longer match the expected output, so the selftests fail when either debug config is enabled. Filtering out every "kobject:" line also hides real WARN()s, e.g. the one kobject_get() emits for an object whose refcount was not initialized. Filtering with "dmesg --level=..." is not enough either, since the tests enable livepatch pr_debug() through dynamic_debug/control and expect its messages. Read the log with "dmesg --raw" and drop only the debug-level messages without the "livepatch:" prefix, plus the specific delayed-release info message. Suggested-by: Petr Mladek Signed-off-by: Yafang Shao Acked-by: Song Liu Reviewed-by: Petr Mladek Tested-by: Petr Mladek Link: https://patch.msgid.link/20260830054857.64758-3-laoar.shao@gmail.com Signed-off-by: Petr Mladek --- tools/testing/selftests/livepatch/functions.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/livepatch/functions.sh b/tools/testing/selftests/livepatch/functions.sh index a65b7b1ac8ad..429fce1f3143 100644 --- a/tools/testing/selftests/livepatch/functions.sh +++ b/tools/testing/selftests/livepatch/functions.sh @@ -320,9 +320,9 @@ function start_test { # find new kernel messages since the test started. local last_dmesg_msg="livepatch kselftest timestamp: $(date --rfc-3339=ns)" log "$last_dmesg_msg" - loop_until 'dmesg | grep -q "$last_dmesg_msg"' || + loop_until 'dmesg -r | grep -q "$last_dmesg_msg"' || die "buffer busy? can't find canary dmesg message: $last_dmesg_msg" - LAST_DMESG=$(dmesg | grep "$last_dmesg_msg") + LAST_DMESG=$(dmesg -r | grep "$last_dmesg_msg") echo -n "TEST: $test ... " log "===== TEST: $test =====" @@ -335,12 +335,18 @@ function check_result { local result # Test results include any new dmesg entry since LAST_DMESG, then: + # - exclude debug messages except with "livepatch:" prefix # - include lines matching keywords # - exclude lines matching keywords # - filter out dmesg timestamp prefixes - result=$(dmesg | awk -v last_dmesg="$LAST_DMESG" 'p; $0 == last_dmesg { p=1 }' | \ + # - exclude the delayed kobject release messages when CONFIG_DEBUG_KOBJECT_RELEASE is on + result=$(dmesg -r | \ + awk -v last_dmesg="$LAST_DMESG" \ + 'p { if ($0 !~ /^<7>/ || $0 ~ /livepatch:/) print; next } $0 == last_dmesg { p = 1 }' | \ grep -e 'livepatch:' -e 'test_klp' | \ grep -v '\(tainting\|taints\) kernel' | \ + grep -v 'kobject: .*parent.*(delayed' | \ + sed 's/^<[0-9]*>//' | \ sed 's/^\[[ 0-9.]*\] //' | \ sed 's/^\[[ ]*[CT][0-9]*\] //') From e0c1c9cad4afbb4099cb488a7e2ba4531993cd3a Mon Sep 17 00:00:00 2001 From: Jiri Kosina Date: Thu, 8 Oct 2026 17:19:24 +0200 Subject: [PATCH 3/4] livepatch: remove myself from MAINTAINERS As discussed at LPC 2026 with Josh -- I am not an active kernel livepatching maintainer any more, so let's properly reflect that. Signed-off-by: Jiri Kosina Link: https://patch.msgid.link/6pq4rr41-0p23-2oq5-o750-0259410q9680@xreary.bet Signed-off-by: Petr Mladek --- MAINTAINERS | 1 - 1 file changed, 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index 335d20584754..f6f481e70129 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15072,7 +15072,6 @@ N: litex LIVE PATCHING M: Josh Poimboeuf -M: Jiri Kosina M: Miroslav Benes M: Petr Mladek R: Joe Lawrence From 9f0deab7285811d13b61bb441653de9e5ce0a1d0 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Sat, 10 Oct 2026 13:31:49 -0700 Subject: [PATCH 4/4] livepatch: Add Joe and Song as maintainers Joe and Song have both been key contributors to livepatch for several years. They have graciously agreed to become maintainers; add them. Signed-off-by: Josh Poimboeuf Acked-by: Song Liu Acked-by: Miroslav Benes Acked-by: Petr Mladek Link: https://patch.msgid.link/55b68f507686cadfcb1fa4f8524ae042c7657be8.1791664234.git.jpoimboe@kernel.org Signed-off-by: Petr Mladek --- MAINTAINERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index f6f481e70129..269ea8b9d7c9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15074,7 +15074,8 @@ LIVE PATCHING M: Josh Poimboeuf M: Miroslav Benes M: Petr Mladek -R: Joe Lawrence +M: Joe Lawrence +M: Song Liu L: live-patching@vger.kernel.org S: Maintained T: git git://git.kernel.org/pub/scm/linux/kernel/git/livepatching/livepatching.git