From d9e4142e7635f6f7173854667c0695ce5b836bbc Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 16 Mar 2026 04:54:31 -0700 Subject: kho: add size parameter to kho_add_subtree() Patch series "kho: history: track previous kernel version and kexec boot count", v9. Use Kexec Handover (KHO) to pass the previous kernel's version string and the number of kexec reboots since the last cold boot to the next kernel, and print it at boot time. Example ======= [ 0.000000] Linux version 6.19.0-rc3-upstream-00047-ge5d992347849 ... [ 0.000000] KHO: exec from: 6.19.0-rc4-next-20260107upstream-00004-g3071b0dc4498 (count 1) Motivation ========== Bugs that only reproduce when kexecing from specific kernel versions are difficult to diagnose. These issues occur when a buggy kernel kexecs into a new kernel, with the bug manifesting only in the second kernel. Recent examples include: * eb2266312507 ("x86/boot: Fix page table access in 5-level to 4-level paging transition") * 77d48d39e991 ("efistub/tpm: Use ACPI reclaim memory for event log to avoid corruption") * 64b45dd46e15 ("x86/efi: skip memattr table on kexec boot") As kexec-based reboots become more common, these version-dependent bugs are appearing more frequently. At scale, correlating crashes to the previous kernel version is challenging, especially when issues only occur in specific transition scenarios. Some bugs manifest only after multiple consecutive kexec reboots. Tracking the kexec count helps identify these cases (this metric is already used by live update sub-system). KHO provides a reliable mechanism to pass information between kernels. By carrying the previous kernel's release string and kexec count forward, we can print this context at boot time to aid debugging. The goal of this feature is to have this information being printed in early boot, so, users can trace back kernel releases in kexec. Systemd is not helpful because we cannot assume that the previous kernel has systemd or even write access to the disk (common when using Linux as bootloaders) This patch (of 6): kho_add_subtree() assumes the fdt argument is always an FDT and calls fdt_totalsize() on it in the debugfs code path. This assumption will break if a caller passes arbitrary data instead of an FDT. When CONFIG_KEXEC_HANDOVER_DEBUGFS is enabled, kho_debugfs_fdt_add() calls __kho_debugfs_fdt_add(), which executes: f->wrapper.size = fdt_totalsize(fdt); Fix this by adding an explicit size parameter to kho_add_subtree() so callers specify the blob size. This allows subtrees to contain arbitrary data formats, not just FDTs. Update all callers: - memblock.c: use fdt_totalsize(fdt) - luo_core.c: use fdt_totalsize(fdt_out) - test_kho.c: use fdt_totalsize() - kexec_handover.c (root fdt): use fdt_totalsize(kho_out.fdt) Also update __kho_debugfs_fdt_add() to receive the size explicitly instead of computing it internally via fdt_totalsize(). In kho_in_debugfs_init(), pass fdt_totalsize() for the root FDT and sub-blobs since all current users are FDTs. A subsequent patch will persist the size in the KHO FDT so the incoming side can handle non-FDT blobs correctly. Link: https://lore.kernel.org/20260323110747.193569-1-duanchenghao@kylinos.cn Link: https://lore.kernel.org/20260316-kho-v9-1-ed6dcd951988@debian.org Signed-off-by: Breno Leitao Suggested-by: Pratyush Yadav Reviewed-by: Mike Rapoport (Microsoft) Reviewed-by: Pratyush Yadav Cc: Alexander Graf Cc: David Hildenbrand Cc: Jonathan Corbet Cc: "Liam R. Howlett" Cc: Lorenzo Stoakes Cc: Michal Hocko Cc: Pasha Tatashin Cc: SeongJae Park Cc: Shuah Khan Cc: Suren Baghdasaryan Cc: Vlastimil Babka Signed-off-by: Andrew Morton --- kernel/liveupdate/kexec_handover.c | 8 +++++--- kernel/liveupdate/kexec_handover_debugfs.c | 15 +++++++++------ kernel/liveupdate/kexec_handover_internal.h | 5 +++-- kernel/liveupdate/luo_core.c | 3 ++- 4 files changed, 19 insertions(+), 12 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c index 532f455c5d4f..8cc25e29ff91 100644 --- a/kernel/liveupdate/kexec_handover.c +++ b/kernel/liveupdate/kexec_handover.c @@ -727,6 +727,7 @@ err_disable_kho: * kho_add_subtree - record the physical address of a sub FDT in KHO root tree. * @name: name of the sub tree. * @fdt: the sub tree blob. + * @size: size of the blob in bytes. * * Creates a new child node named @name in KHO root FDT and records * the physical address of @fdt. The pages of @fdt must also be preserved @@ -738,7 +739,7 @@ err_disable_kho: * * Return: 0 on success, error code on failure */ -int kho_add_subtree(const char *name, void *fdt) +int kho_add_subtree(const char *name, void *fdt, size_t size) { phys_addr_t phys = virt_to_phys(fdt); void *root_fdt = kho_out.fdt; @@ -763,7 +764,7 @@ int kho_add_subtree(const char *name, void *fdt) if (err < 0) goto out_pack; - WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, false)); + WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, size, false)); out_pack: fdt_pack(root_fdt); @@ -1431,7 +1432,8 @@ static __init int kho_init(void) } WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, "fdt", - kho_out.fdt, true)); + kho_out.fdt, + fdt_totalsize(kho_out.fdt), true)); return 0; diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c index acf368222682..ca0153736af1 100644 --- a/kernel/liveupdate/kexec_handover_debugfs.c +++ b/kernel/liveupdate/kexec_handover_debugfs.c @@ -25,7 +25,7 @@ struct fdt_debugfs { }; static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, - const char *name, const void *fdt) + const char *name, const void *fdt, size_t size) { struct fdt_debugfs *f; struct dentry *file; @@ -35,7 +35,7 @@ static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, return -ENOMEM; f->wrapper.data = (void *)fdt; - f->wrapper.size = fdt_totalsize(fdt); + f->wrapper.size = size; file = debugfs_create_blob(name, 0400, dir, &f->wrapper); if (IS_ERR(file)) { @@ -50,7 +50,7 @@ static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, } int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, bool root) + const void *fdt, size_t size, bool root) { struct dentry *dir; @@ -59,7 +59,7 @@ int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, else dir = dbg->sub_fdt_dir; - return __kho_debugfs_fdt_add(&dbg->fdt_list, dir, name, fdt); + return __kho_debugfs_fdt_add(&dbg->fdt_list, dir, name, fdt, size); } void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, void *fdt) @@ -113,7 +113,8 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) goto err_rmdir; } - err = __kho_debugfs_fdt_add(&dbg->fdt_list, dir, "fdt", fdt); + err = __kho_debugfs_fdt_add(&dbg->fdt_list, dir, "fdt", fdt, + fdt_totalsize(fdt)); if (err) goto err_rmdir; @@ -121,6 +122,7 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) int len = 0; const char *name = fdt_get_name(fdt, child, NULL); const u64 *fdt_phys; + void *sub_fdt; fdt_phys = fdt_getprop(fdt, child, KHO_FDT_SUB_TREE_PROP_NAME, &len); if (!fdt_phys) @@ -130,8 +132,9 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) name, len); continue; } + sub_fdt = phys_to_virt(*fdt_phys); err = __kho_debugfs_fdt_add(&dbg->fdt_list, sub_fdt_dir, name, - phys_to_virt(*fdt_phys)); + sub_fdt, fdt_totalsize(sub_fdt)); if (err) { pr_warn("failed to add fdt %s to debugfs: %pe\n", name, ERR_PTR(err)); diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h index 9a832a35254c..2a28cb8db9b0 100644 --- a/kernel/liveupdate/kexec_handover_internal.h +++ b/kernel/liveupdate/kexec_handover_internal.h @@ -27,7 +27,7 @@ int kho_debugfs_init(void); void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt); int kho_out_debugfs_init(struct kho_debugfs *dbg); int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, bool root); + const void *fdt, size_t size, bool root); void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, void *fdt); #else static inline int kho_debugfs_init(void) { return 0; } @@ -35,7 +35,8 @@ static inline void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) { } static inline int kho_out_debugfs_init(struct kho_debugfs *dbg) { return 0; } static inline int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, bool root) { return 0; } + const void *fdt, size_t size, + bool root) { return 0; } static inline void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, void *fdt) { } #endif /* CONFIG_KEXEC_HANDOVER_DEBUGFS */ diff --git a/kernel/liveupdate/luo_core.c b/kernel/liveupdate/luo_core.c index 84ac728d63ba..04d06a0906c0 100644 --- a/kernel/liveupdate/luo_core.c +++ b/kernel/liveupdate/luo_core.c @@ -172,7 +172,8 @@ static int __init luo_fdt_setup(void) if (err) goto exit_free; - err = kho_add_subtree(LUO_FDT_KHO_ENTRY_NAME, fdt_out); + err = kho_add_subtree(LUO_FDT_KHO_ENTRY_NAME, fdt_out, + fdt_totalsize(fdt_out)); if (err) goto exit_free; luo_global.fdt_out = fdt_out; -- cgit v1.2.3 From 4916ae386760ad666eafa8afc075957bf479afbc Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 16 Mar 2026 04:54:32 -0700 Subject: kho: rename fdt parameter to blob in kho_add/remove_subtree() Since kho_add_subtree() now accepts arbitrary data blobs (not just FDTs), rename the parameter from 'fdt' to 'blob' to better reflect its purpose. Apply the same rename to kho_remove_subtree() for consistency. Also rename kho_debugfs_fdt_add() and kho_debugfs_fdt_remove() to kho_debugfs_blob_add() and kho_debugfs_blob_remove() respectively, with the same parameter rename from 'fdt' to 'blob'. Link: https://lore.kernel.org/20260316-kho-v9-2-ed6dcd951988@debian.org Signed-off-by: Breno Leitao Reviewed-by: Mike Rapoport (Microsoft) Reviewed-by: Pratyush Yadav Cc: Alexander Graf Cc: David Hildenbrand Cc: Jonathan Corbet Cc: "Liam R. Howlett" Cc: Lorenzo Stoakes Cc: Michal Hocko Cc: Pasha Tatashin Cc: SeongJae Park Cc: Shuah Khan Cc: Suren Baghdasaryan Cc: Vlastimil Babka Signed-off-by: Andrew Morton --- kernel/liveupdate/kexec_handover.c | 33 +++++++++++++++-------------- kernel/liveupdate/kexec_handover_debugfs.c | 25 +++++++++++----------- kernel/liveupdate/kexec_handover_internal.h | 16 +++++++------- 3 files changed, 38 insertions(+), 36 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c index 8cc25e29ff91..711b6c3376e7 100644 --- a/kernel/liveupdate/kexec_handover.c +++ b/kernel/liveupdate/kexec_handover.c @@ -724,13 +724,13 @@ err_disable_kho: } /** - * kho_add_subtree - record the physical address of a sub FDT in KHO root tree. + * kho_add_subtree - record the physical address of a sub blob in KHO root tree. * @name: name of the sub tree. - * @fdt: the sub tree blob. + * @blob: the sub tree blob. * @size: size of the blob in bytes. * * Creates a new child node named @name in KHO root FDT and records - * the physical address of @fdt. The pages of @fdt must also be preserved + * the physical address of @blob. The pages of @blob must also be preserved * by KHO for the new kernel to retrieve it after kexec. * * A debugfs blob entry is also created at @@ -739,9 +739,9 @@ err_disable_kho: * * Return: 0 on success, error code on failure */ -int kho_add_subtree(const char *name, void *fdt, size_t size) +int kho_add_subtree(const char *name, void *blob, size_t size) { - phys_addr_t phys = virt_to_phys(fdt); + phys_addr_t phys = virt_to_phys(blob); void *root_fdt = kho_out.fdt; int err = -ENOMEM; int off, fdt_err; @@ -764,7 +764,8 @@ int kho_add_subtree(const char *name, void *fdt, size_t size) if (err < 0) goto out_pack; - WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, size, false)); + WARN_ON_ONCE(kho_debugfs_blob_add(&kho_out.dbg, name, blob, + size, false)); out_pack: fdt_pack(root_fdt); @@ -773,9 +774,9 @@ out_pack: } EXPORT_SYMBOL_GPL(kho_add_subtree); -void kho_remove_subtree(void *fdt) +void kho_remove_subtree(void *blob) { - phys_addr_t target_phys = virt_to_phys(fdt); + phys_addr_t target_phys = virt_to_phys(blob); void *root_fdt = kho_out.fdt; int off; int err; @@ -797,7 +798,7 @@ void kho_remove_subtree(void *fdt) if ((phys_addr_t)*val == target_phys) { fdt_del_node(root_fdt, off); - kho_debugfs_fdt_remove(&kho_out.dbg, fdt); + kho_debugfs_blob_remove(&kho_out.dbg, blob); break; } } @@ -1293,11 +1294,11 @@ bool is_kho_boot(void) EXPORT_SYMBOL_GPL(is_kho_boot); /** - * kho_retrieve_subtree - retrieve a preserved sub FDT by its name. - * @name: the name of the sub FDT passed to kho_add_subtree(). - * @phys: if found, the physical address of the sub FDT is stored in @phys. + * kho_retrieve_subtree - retrieve a preserved sub blob by its name. + * @name: the name of the sub blob passed to kho_add_subtree(). + * @phys: if found, the physical address of the sub blob is stored in @phys. * - * Retrieve a preserved sub FDT named @name and store its physical + * Retrieve a preserved sub blob named @name and store its physical * address in @phys. * * Return: 0 on success, error code on failure @@ -1431,9 +1432,9 @@ static __init int kho_init(void) init_cma_reserved_pageblock(pfn_to_page(pfn)); } - WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, "fdt", - kho_out.fdt, - fdt_totalsize(kho_out.fdt), true)); + WARN_ON_ONCE(kho_debugfs_blob_add(&kho_out.dbg, "fdt", + kho_out.fdt, + fdt_totalsize(kho_out.fdt), true)); return 0; diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c index ca0153736af1..cab923e4f5c8 100644 --- a/kernel/liveupdate/kexec_handover_debugfs.c +++ b/kernel/liveupdate/kexec_handover_debugfs.c @@ -24,8 +24,9 @@ struct fdt_debugfs { struct dentry *file; }; -static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, - const char *name, const void *fdt, size_t size) +static int __kho_debugfs_blob_add(struct list_head *list, struct dentry *dir, + const char *name, const void *blob, + size_t size) { struct fdt_debugfs *f; struct dentry *file; @@ -34,7 +35,7 @@ static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, if (!f) return -ENOMEM; - f->wrapper.data = (void *)fdt; + f->wrapper.data = (void *)blob; f->wrapper.size = size; file = debugfs_create_blob(name, 0400, dir, &f->wrapper); @@ -49,8 +50,8 @@ static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir, return 0; } -int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, size_t size, bool root) +int kho_debugfs_blob_add(struct kho_debugfs *dbg, const char *name, + const void *blob, size_t size, bool root) { struct dentry *dir; @@ -59,15 +60,15 @@ int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, else dir = dbg->sub_fdt_dir; - return __kho_debugfs_fdt_add(&dbg->fdt_list, dir, name, fdt, size); + return __kho_debugfs_blob_add(&dbg->fdt_list, dir, name, blob, size); } -void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, void *fdt) +void kho_debugfs_blob_remove(struct kho_debugfs *dbg, void *blob) { struct fdt_debugfs *ff; list_for_each_entry(ff, &dbg->fdt_list, list) { - if (ff->wrapper.data == fdt) { + if (ff->wrapper.data == blob) { debugfs_remove(ff->file); list_del(&ff->list); kfree(ff); @@ -113,8 +114,8 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) goto err_rmdir; } - err = __kho_debugfs_fdt_add(&dbg->fdt_list, dir, "fdt", fdt, - fdt_totalsize(fdt)); + err = __kho_debugfs_blob_add(&dbg->fdt_list, dir, "fdt", fdt, + fdt_totalsize(fdt)); if (err) goto err_rmdir; @@ -133,8 +134,8 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) continue; } sub_fdt = phys_to_virt(*fdt_phys); - err = __kho_debugfs_fdt_add(&dbg->fdt_list, sub_fdt_dir, name, - sub_fdt, fdt_totalsize(sub_fdt)); + err = __kho_debugfs_blob_add(&dbg->fdt_list, sub_fdt_dir, name, + sub_fdt, fdt_totalsize(sub_fdt)); if (err) { pr_warn("failed to add fdt %s to debugfs: %pe\n", name, ERR_PTR(err)); diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h index 2a28cb8db9b0..0399ff107775 100644 --- a/kernel/liveupdate/kexec_handover_internal.h +++ b/kernel/liveupdate/kexec_handover_internal.h @@ -26,19 +26,19 @@ extern unsigned int kho_scratch_cnt; int kho_debugfs_init(void); void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt); int kho_out_debugfs_init(struct kho_debugfs *dbg); -int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, size_t size, bool root); -void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, void *fdt); +int kho_debugfs_blob_add(struct kho_debugfs *dbg, const char *name, + const void *blob, size_t size, bool root); +void kho_debugfs_blob_remove(struct kho_debugfs *dbg, void *blob); #else static inline int kho_debugfs_init(void) { return 0; } static inline void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) { } static inline int kho_out_debugfs_init(struct kho_debugfs *dbg) { return 0; } -static inline int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name, - const void *fdt, size_t size, - bool root) { return 0; } -static inline void kho_debugfs_fdt_remove(struct kho_debugfs *dbg, - void *fdt) { } +static inline int kho_debugfs_blob_add(struct kho_debugfs *dbg, + const char *name, const void *blob, + size_t size, bool root) { return 0; } +static inline void kho_debugfs_blob_remove(struct kho_debugfs *dbg, + void *blob) { } #endif /* CONFIG_KEXEC_HANDOVER_DEBUGFS */ #ifdef CONFIG_KEXEC_HANDOVER_DEBUG -- cgit v1.2.3 From 85e41392820fcf0f7a3f9784cea907905f921358 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 16 Mar 2026 04:54:33 -0700 Subject: kho: persist blob size in KHO FDT kho_add_subtree() accepts a size parameter but only forwards it to debugfs. The size is not persisted in the KHO FDT, so it is lost across kexec. This makes it impossible for the incoming kernel to determine the blob size without understanding the blob format. Store the blob size as a "blob-size" property in the KHO FDT alongside the "preserved-data" physical address. This allows the receiving kernel to recover the size for any blob regardless of format. Also extend kho_retrieve_subtree() with an optional size output parameter so callers can learn the blob size without needing to understand the blob format. Update all callers to pass NULL for the new parameter. Link: https://lore.kernel.org/20260316-kho-v9-3-ed6dcd951988@debian.org Signed-off-by: Breno Leitao Reviewed-by: Mike Rapoport (Microsoft) Reviewed-by: Pratyush Yadav Cc: Alexander Graf Cc: David Hildenbrand Cc: Jonathan Corbet Cc: "Liam R. Howlett" Cc: Lorenzo Stoakes Cc: Michal Hocko Cc: Pasha Tatashin Cc: SeongJae Park Cc: Shuah Khan Cc: Suren Baghdasaryan Cc: Vlastimil Babka Signed-off-by: Andrew Morton --- kernel/liveupdate/kexec_handover.c | 27 ++++++++++++++++++++++----- kernel/liveupdate/kexec_handover_debugfs.c | 3 ++- kernel/liveupdate/luo_core.c | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c index 711b6c3376e7..adf6541f70f9 100644 --- a/kernel/liveupdate/kexec_handover.c +++ b/kernel/liveupdate/kexec_handover.c @@ -743,6 +743,7 @@ int kho_add_subtree(const char *name, void *blob, size_t size) { phys_addr_t phys = virt_to_phys(blob); void *root_fdt = kho_out.fdt; + u64 size_u64 = size; int err = -ENOMEM; int off, fdt_err; @@ -759,11 +760,16 @@ int kho_add_subtree(const char *name, void *blob, size_t size) goto out_pack; } - err = fdt_setprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME, + err = fdt_setprop(root_fdt, off, KHO_SUB_TREE_PROP_NAME, &phys, sizeof(phys)); if (err < 0) goto out_pack; + err = fdt_setprop(root_fdt, off, KHO_SUB_TREE_SIZE_PROP_NAME, + &size_u64, sizeof(size_u64)); + if (err < 0) + goto out_pack; + WARN_ON_ONCE(kho_debugfs_blob_add(&kho_out.dbg, name, blob, size, false)); @@ -792,7 +798,7 @@ void kho_remove_subtree(void *blob) const u64 *val; int len; - val = fdt_getprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME, &len); + val = fdt_getprop(root_fdt, off, KHO_SUB_TREE_PROP_NAME, &len); if (!val || len != sizeof(phys_addr_t)) continue; @@ -1297,13 +1303,14 @@ EXPORT_SYMBOL_GPL(is_kho_boot); * kho_retrieve_subtree - retrieve a preserved sub blob by its name. * @name: the name of the sub blob passed to kho_add_subtree(). * @phys: if found, the physical address of the sub blob is stored in @phys. + * @size: if not NULL and found, the size of the sub blob is stored in @size. * * Retrieve a preserved sub blob named @name and store its physical - * address in @phys. + * address in @phys and optionally its size in @size. * * Return: 0 on success, error code on failure */ -int kho_retrieve_subtree(const char *name, phys_addr_t *phys) +int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size) { const void *fdt = kho_get_fdt(); const u64 *val; @@ -1319,12 +1326,22 @@ int kho_retrieve_subtree(const char *name, phys_addr_t *phys) if (offset < 0) return -ENOENT; - val = fdt_getprop(fdt, offset, KHO_FDT_SUB_TREE_PROP_NAME, &len); + val = fdt_getprop(fdt, offset, KHO_SUB_TREE_PROP_NAME, &len); if (!val || len != sizeof(*val)) return -EINVAL; *phys = (phys_addr_t)*val; + val = fdt_getprop(fdt, offset, KHO_SUB_TREE_SIZE_PROP_NAME, &len); + if (!val || len != sizeof(*val)) { + pr_warn("broken KHO subnode '%s': missing or invalid blob-size property\n", + name); + return -EINVAL; + } + + if (size) + *size = (size_t)*val; + return 0; } EXPORT_SYMBOL_GPL(kho_retrieve_subtree); diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c index cab923e4f5c8..b416846810d7 100644 --- a/kernel/liveupdate/kexec_handover_debugfs.c +++ b/kernel/liveupdate/kexec_handover_debugfs.c @@ -125,7 +125,8 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) const u64 *fdt_phys; void *sub_fdt; - fdt_phys = fdt_getprop(fdt, child, KHO_FDT_SUB_TREE_PROP_NAME, &len); + fdt_phys = fdt_getprop(fdt, child, + KHO_SUB_TREE_PROP_NAME, &len); if (!fdt_phys) continue; if (len != sizeof(*fdt_phys)) { diff --git a/kernel/liveupdate/luo_core.c b/kernel/liveupdate/luo_core.c index 04d06a0906c0..48b25c9abeda 100644 --- a/kernel/liveupdate/luo_core.c +++ b/kernel/liveupdate/luo_core.c @@ -88,7 +88,7 @@ static int __init luo_early_startup(void) } /* Retrieve LUO subtree, and verify its format. */ - err = kho_retrieve_subtree(LUO_FDT_KHO_ENTRY_NAME, &fdt_phys); + err = kho_retrieve_subtree(LUO_FDT_KHO_ENTRY_NAME, &fdt_phys, NULL); if (err) { if (err != -ENOENT) { pr_err("failed to retrieve FDT '%s' from KHO: %pe\n", -- cgit v1.2.3 From 062dd306d99cc2e02f761124e064e6a3735e27b0 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 16 Mar 2026 04:54:34 -0700 Subject: kho: fix kho_in_debugfs_init() to handle non-FDT blobs kho_in_debugfs_init() calls fdt_totalsize() to determine blob sizes, which assumes all blobs are FDTs. This breaks for non-FDT blobs like struct kho_kexec_metadata. Fix this by reading the "blob-size" property from the FDT (persisted by kho_add_subtree()) instead of calling fdt_totalsize(). Also rename local variables from fdt_phys/sub_fdt to blob_phys/blob for consistency with the non-FDT-specific naming. Link: https://lore.kernel.org/20260316-kho-v9-4-ed6dcd951988@debian.org Signed-off-by: Breno Leitao Reviewed-by: Pratyush Yadav Cc: Alexander Graf Cc: David Hildenbrand Cc: Jonathan Corbet Cc: "Liam R. Howlett" Cc: Lorenzo Stoakes Cc: Michal Hocko Cc: Mike Rapoport (Microsoft) Cc: Pasha Tatashin Cc: SeongJae Park Cc: Shuah Khan Cc: Suren Baghdasaryan Cc: Vlastimil Babka Signed-off-by: Andrew Morton --- kernel/liveupdate/kexec_handover_debugfs.c | 32 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c index b416846810d7..257ee8a52be6 100644 --- a/kernel/liveupdate/kexec_handover_debugfs.c +++ b/kernel/liveupdate/kexec_handover_debugfs.c @@ -122,24 +122,34 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt) fdt_for_each_subnode(child, fdt, 0) { int len = 0; const char *name = fdt_get_name(fdt, child, NULL); - const u64 *fdt_phys; - void *sub_fdt; + const u64 *blob_phys; + const u64 *blob_size; + void *blob; - fdt_phys = fdt_getprop(fdt, child, + blob_phys = fdt_getprop(fdt, child, KHO_SUB_TREE_PROP_NAME, &len); - if (!fdt_phys) + if (!blob_phys) continue; - if (len != sizeof(*fdt_phys)) { - pr_warn("node %s prop fdt has invalid length: %d\n", - name, len); + if (len != sizeof(*blob_phys)) { + pr_warn("node %s prop %s has invalid length: %d\n", + name, KHO_SUB_TREE_PROP_NAME, len); continue; } - sub_fdt = phys_to_virt(*fdt_phys); + + blob_size = fdt_getprop(fdt, child, + KHO_SUB_TREE_SIZE_PROP_NAME, &len); + if (!blob_size || len != sizeof(*blob_size)) { + pr_warn("node %s missing or invalid %s property\n", + name, KHO_SUB_TREE_SIZE_PROP_NAME); + continue; + } + + blob = phys_to_virt(*blob_phys); err = __kho_debugfs_blob_add(&dbg->fdt_list, sub_fdt_dir, name, - sub_fdt, fdt_totalsize(sub_fdt)); + blob, *blob_size); if (err) { - pr_warn("failed to add fdt %s to debugfs: %pe\n", name, - ERR_PTR(err)); + pr_warn("failed to add blob %s to debugfs: %pe\n", + name, ERR_PTR(err)); continue; } } -- cgit v1.2.3 From 76aa46b9e4049247858309c6e3527d477da2b2fe Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 16 Mar 2026 04:54:35 -0700 Subject: kho: kexec-metadata: track previous kernel chain Use Kexec Handover (KHO) to pass the previous kernel's version string and the number of kexec reboots since the last cold boot to the next kernel, and print it at boot time. Example output: [ 0.000000] KHO: exec from: 6.19.0-rc4-next-20260107 (count 1) Motivation ========== Bugs that only reproduce when kexecing from specific kernel versions are difficult to diagnose. These issues occur when a buggy kernel kexecs into a new kernel, with the bug manifesting only in the second kernel. Recent examples include the following commits: * commit eb2266312507 ("x86/boot: Fix page table access in 5-level to 4-level paging transition") * commit 77d48d39e991 ("efistub/tpm: Use ACPI reclaim memory for event log to avoid corruption") * commit 64b45dd46e15 ("x86/efi: skip memattr table on kexec boot") As kexec-based reboots become more common, these version-dependent bugs are appearing more frequently. At scale, correlating crashes to the previous kernel version is challenging, especially when issues only occur in specific transition scenarios. Implementation ============== The kexec metadata is stored as a plain C struct (struct kho_kexec_metadata) rather than FDT format, for simplicity and direct field access. It is registered via kho_add_subtree() as a separate subtree, keeping it independent from the core KHO ABI. This design choice: - Keeps the core KHO ABI minimal and stable - Allows the metadata format to evolve independently - Avoids requiring version bumps for all KHO consumers (LUO, etc.) when the metadata format changes The struct kho_kexec_metadata contains two fields: - previous_release: The kernel version that initiated the kexec - kexec_count: Number of kexec boots since last cold boot On cold boot, kexec_count starts at 0 and increments with each kexec. The count helps identify issues that only manifest after multiple consecutive kexec reboots. [leitao@debian.org: call kho_kexec_metadata_init() for both boot paths] Link: https://lore.kernel.org/all/20260309-kho-v8-5-c3abcf4ac750@debian.org/ [1] Link: https://lore.kernel.org/20260409-kho_fix_merge_issue-v1-1-710c84ceaa85@debian.org Link: https://lore.kernel.org/20260316-kho-v9-5-ed6dcd951988@debian.org Signed-off-by: Breno Leitao Acked-by: SeongJae Park Reviewed-by: Mike Rapoport (Microsoft) Reviewed-by: Pratyush Yadav Cc: Alexander Graf Cc: David Hildenbrand Cc: Jonathan Corbet Cc: "Liam R. Howlett" Cc: Lorenzo Stoakes Cc: Michal Hocko Cc: Pasha Tatashin Cc: Shuah Khan Cc: Suren Baghdasaryan Cc: Vlastimil Babka Signed-off-by: Andrew Morton --- kernel/liveupdate/kexec_handover.c | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c index adf6541f70f9..94762de1fe5f 100644 --- a/kernel/liveupdate/kexec_handover.c +++ b/kernel/liveupdate/kexec_handover.c @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1268,6 +1270,8 @@ EXPORT_SYMBOL_GPL(kho_restore_free); struct kho_in { phys_addr_t fdt_phys; phys_addr_t scratch_phys; + char previous_release[__NEW_UTS_LEN + 1]; + u32 kexec_count; struct kho_debugfs dbg; }; @@ -1392,6 +1396,96 @@ static __init int kho_out_fdt_setup(void) return err; } +static void __init kho_in_kexec_metadata(void) +{ + struct kho_kexec_metadata *metadata; + phys_addr_t metadata_phys; + size_t blob_size; + int err; + + err = kho_retrieve_subtree(KHO_METADATA_NODE_NAME, &metadata_phys, + &blob_size); + if (err) + /* This is fine, previous kernel didn't export metadata */ + return; + + /* Check that, at least, "version" is present */ + if (blob_size < sizeof(u32)) { + pr_warn("kexec-metadata blob too small (%zu bytes)\n", + blob_size); + return; + } + + metadata = phys_to_virt(metadata_phys); + + if (metadata->version != KHO_KEXEC_METADATA_VERSION) { + pr_warn("kexec-metadata version %u not supported (expected %u)\n", + metadata->version, KHO_KEXEC_METADATA_VERSION); + return; + } + + if (blob_size < sizeof(*metadata)) { + pr_warn("kexec-metadata blob too small for v%u (%zu < %zu)\n", + metadata->version, blob_size, sizeof(*metadata)); + return; + } + + /* + * Copy data to the kernel structure that will persist during + * kernel lifetime. + */ + kho_in.kexec_count = metadata->kexec_count; + strscpy(kho_in.previous_release, metadata->previous_release, + sizeof(kho_in.previous_release)); + + pr_info("exec from: %s (count %u)\n", + kho_in.previous_release, kho_in.kexec_count); +} + +/* + * Create kexec metadata to pass kernel version and boot count to the + * next kernel. This keeps the core KHO ABI minimal and allows the + * metadata format to evolve independently. + */ +static __init int kho_out_kexec_metadata(void) +{ + struct kho_kexec_metadata *metadata; + int err; + + metadata = kho_alloc_preserve(sizeof(*metadata)); + if (IS_ERR(metadata)) + return PTR_ERR(metadata); + + metadata->version = KHO_KEXEC_METADATA_VERSION; + strscpy(metadata->previous_release, init_uts_ns.name.release, + sizeof(metadata->previous_release)); + /* kho_in.kexec_count is set to 0 on cold boot */ + metadata->kexec_count = kho_in.kexec_count + 1; + + err = kho_add_subtree(KHO_METADATA_NODE_NAME, metadata, + sizeof(*metadata)); + if (err) + kho_unpreserve_free(metadata); + + return err; +} + +static int __init kho_kexec_metadata_init(const void *fdt) +{ + int err; + + if (fdt) + kho_in_kexec_metadata(); + + /* Populate kexec metadata for the possible next kexec */ + err = kho_out_kexec_metadata(); + if (err) + pr_warn("failed to initialize kexec-metadata subtree: %d\n", + err); + + return err; +} + static __init int kho_init(void) { struct kho_radix_tree *tree = &kho_out.radix_tree; @@ -1425,6 +1519,10 @@ static __init int kho_init(void) if (err) goto err_free_fdt; + err = kho_kexec_metadata_init(fdt); + if (err) + goto err_free_fdt; + if (fdt) { kho_in_debugfs_init(&kho_in.dbg, fdt); return 0; -- cgit v1.2.3 From 00d0b372374f2528394aabf7b1f53f8dafe294de Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Thu, 26 Mar 2026 16:39:41 +0000 Subject: liveupdate: prevent double management of files Patch series "liveupdate: prevent double preservation", v4. Currently, LUO does not prevent the same file from being managed twice across different active sessions. Because LUO preserves files of absolutely different types: memfd, and upcoming vfiofd [1], iommufd [2], guestmefd (and possible kvmfd/cpufd). There is no common private data or guarantee on how to prevent that the same file is not preserved twice beside using inode or some slower and expensive method like hashtables. This patch (of 4) Currently, LUO does not prevent the same file from being managed twice across different active sessions. Use a global xarray luo_preserved_files to keep track of file identifiers being preserved by LUO. Update luo_preserve_file() to check and insert the file identifier into this xarray when it is preserved, and erase it in luo_file_unpreserve_files() when it is released. To allow handlers to define what constitutes a "unique" file (e.g., different struct file objects pointing to the same hardware resource), add a get_id() callback to struct liveupdate_file_ops. If not provided, the default identifier is the struct file pointer itself. This ensures that the same file (or resource) cannot be managed by multiple sessions. If another session attempts to preserve an already managed file, it will now fail with -EBUSY. Link: https://lore.kernel.org/20260326163943.574070-1-pasha.tatashin@soleen.com Link: https://lore.kernel.org/20260326163943.574070-2-pasha.tatashin@soleen.com Link: https://lore.kernel.org/all/20260129212510.967611-1-dmatlack@google.com [1] Link: https://lore.kernel.org/all/20260203220948.2176157-1-skhawaja@google.com [2] Signed-off-by: Pasha Tatashin Reviewed-by: Samiullah Khawaja Reviewed-by: Mike Rapoport (Microsoft) Cc: David Matlack Cc: Pratyush Yadav Cc: Shuah Khan Cc: Christian Brauner Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 5acee4174bf0..09103cf81107 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -108,12 +108,16 @@ #include #include #include +#include #include #include #include "luo_internal.h" static LIST_HEAD(luo_file_handler_list); +/* Keep track of files being preserved by LUO */ +static DEFINE_XARRAY(luo_preserved_files); + /* 2 4K pages, give space for 128 files per file_set */ #define LUO_FILE_PGCNT 2ul #define LUO_FILE_MAX \ @@ -203,6 +207,12 @@ static void luo_free_files_mem(struct luo_file_set *file_set) file_set->files = NULL; } +static unsigned long luo_get_id(struct liveupdate_file_handler *fh, + struct file *file) +{ + return fh->ops->get_id ? fh->ops->get_id(file) : (unsigned long)file; +} + static bool luo_token_is_used(struct luo_file_set *file_set, u64 token) { struct luo_file *iter; @@ -248,6 +258,7 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token) * Context: Can be called from an ioctl handler during normal system operation. * Return: 0 on success. Returns a negative errno on failure: * -EEXIST if the token is already used. + * -EBUSY if the file descriptor is already preserved by another session. * -EBADF if the file descriptor is invalid. * -ENOSPC if the file_set is full. * -ENOENT if no compatible handler is found. @@ -288,10 +299,15 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd) if (err) goto err_free_files_mem; - err = luo_flb_file_preserve(fh); + err = xa_insert(&luo_preserved_files, luo_get_id(fh, file), + file, GFP_KERNEL); if (err) goto err_free_files_mem; + err = luo_flb_file_preserve(fh); + if (err) + goto err_erase_xa; + luo_file = kzalloc_obj(*luo_file); if (!luo_file) { err = -ENOMEM; @@ -320,6 +336,8 @@ err_kfree: kfree(luo_file); err_flb_unpreserve: luo_flb_file_unpreserve(fh); +err_erase_xa: + xa_erase(&luo_preserved_files, luo_get_id(fh, file)); err_free_files_mem: luo_free_files_mem(file_set); err_fput: @@ -363,6 +381,8 @@ void luo_file_unpreserve_files(struct luo_file_set *file_set) luo_file->fh->ops->unpreserve(&args); luo_flb_file_unpreserve(luo_file->fh); + xa_erase(&luo_preserved_files, + luo_get_id(luo_file->fh, luo_file->file)); list_del(&luo_file->list); file_set->count--; @@ -606,6 +626,11 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token, luo_file->file = args.file; /* Get reference so we can keep this file in LUO until finish */ get_file(luo_file->file); + + WARN_ON(xa_insert(&luo_preserved_files, + luo_get_id(luo_file->fh, luo_file->file), + luo_file->file, GFP_KERNEL)); + *filep = luo_file->file; luo_file->retrieve_status = 1; @@ -701,8 +726,11 @@ int luo_file_finish(struct luo_file_set *file_set) luo_file_finish_one(file_set, luo_file); - if (luo_file->file) + if (luo_file->file) { + xa_erase(&luo_preserved_files, + luo_get_id(luo_file->fh, luo_file->file)); fput(luo_file->file); + } list_del(&luo_file->list); file_set->count--; mutex_destroy(&luo_file->mutex); -- cgit v1.2.3 From 277f4e5e398b8c59148ebc33dbee8f9821f087eb Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:25 +0000 Subject: liveupdate: safely print untrusted strings Patch series "liveupdate: Fix module unloading and unregister API", v3. This patch series addresses an issue with how LUO handles module reference counting and unregistration during a module unload (e.g., via rmmod). Currently, modules that register live update file handlers are pinned for the entire duration they are registered. This prevents the modules from being unloaded gracefully, even when no live update session is in progress. Furthermore, if a module is forcefully unloaded, the unregistration functions return an error (e.g. -EBUSY) if a session is active, which is ignored by the kernel's module unload path, leaving dangling pointers in the LUO global lists. To resolve these issues, this series introduces the following changes: 1. Adds a global read-write semaphore (luo_register_rwlock) to protect the registration lists for both file handlers and FLBs. 2. Reduces the scope of module reference counting for file handlers and FLBs. Instead of pinning modules indefinitely upon registration, references are now taken only when they are actively used in a live update session (e.g., during preservation, retrieval, or deserialization). 3. Removes the global luo_session_quiesce() mechanism since module unload behavior now handles active sessions implicitly. 4. Introduces auto-unregistration of FLBs during file handler unregistration to prevent leaving dangling resources. 5. Changes the unregistration functions to return void instead of an error code. 6. Fixes a data race in luo_flb_get_private() by introducing a spinlock for thread-safe lazy initialization. 7. Strengthens security by using %.*s when printing untrusted deserialized compatible strings and session names to prevent out-of-bounds reads. This patch (of 10): Deserialized strings from KHO data (such as file handler compatible strings and session names) are provided by the previous kernel and might not be null-terminated if the data is corrupted or maliciously crafted. When printing these strings in error messages, use the %.*s format specifier with the maximum buffer size to prevent out-of-bounds reads into adjacent kernel memory. Link: https://lore.kernel.org/20260327033335.696621-1-pasha.tatashin@soleen.com Link: https://lore.kernel.org/20260327033335.696621-2-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 3 ++- kernel/liveupdate/luo_session.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 09103cf81107..8fcf302c73b6 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -813,7 +813,8 @@ int luo_file_deserialize(struct luo_file_set *file_set, } if (!handler_found) { - pr_warn("No registered handler for compatible '%s'\n", + pr_warn("No registered handler for compatible '%.*s'\n", + (int)sizeof(file_ser[i].compatible), file_ser[i].compatible); return -ENOENT; } diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c index 783677295640..c68a0041bcf2 100644 --- a/kernel/liveupdate/luo_session.c +++ b/kernel/liveupdate/luo_session.c @@ -544,7 +544,8 @@ int luo_session_deserialize(void) session = luo_session_alloc(sh->ser[i].name); if (IS_ERR(session)) { - pr_warn("Failed to allocate session [%s] during deserialization %pe\n", + pr_warn("Failed to allocate session [%.*s] during deserialization %pe\n", + (int)sizeof(sh->ser[i].name), sh->ser[i].name, session); return PTR_ERR(session); } -- cgit v1.2.3 From 38fb71ace230bcf0106b6a09e7361c09255ba332 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:26 +0000 Subject: liveupdate: synchronize lazy initialization of FLB private state The luo_flb_get_private() function, which is responsible for lazily initializing the private state of FLB objects, can be called concurrently from multiple threads. This creates a data race on the 'initialized' flag and can lead to multiple executions of mutex_init() and INIT_LIST_HEAD() on the same memory. Introduce a static spinlock (luo_flb_init_lock) local to the function to synchronize the initialization path. Use smp_load_acquire() and smp_store_release() for memory ordering between the fast path and the slow path. Link: https://lore.kernel.org/20260327033335.696621-3-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_flb.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index f52e8114837e..cf4a8f854c83 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -89,13 +89,18 @@ struct luo_flb_link { static struct luo_flb_private *luo_flb_get_private(struct liveupdate_flb *flb) { struct luo_flb_private *private = &ACCESS_PRIVATE(flb, private); + static DEFINE_SPINLOCK(luo_flb_init_lock); + if (smp_load_acquire(&private->initialized)) + return private; + + guard(spinlock)(&luo_flb_init_lock); if (!private->initialized) { mutex_init(&private->incoming.lock); mutex_init(&private->outgoing.lock); INIT_LIST_HEAD(&private->list); private->users = 0; - private->initialized = true; + smp_store_release(&private->initialized, true); } return private; -- cgit v1.2.3 From 9e1e18584548e8ef8b37a2a7f5eb84b91e35a160 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:27 +0000 Subject: liveupdate: protect file handler list with rwsem Because liveupdate file handlers will no longer hold a module reference when registered, we must ensure that the access to the handler list is protected against concurrent module unloading. Utilize the global luo_register_rwlock to protect the global registry of file handlers. Read locks are taken during list traversals in luo_preserve_file() and luo_file_deserialize(). Write locks are taken during registration and unregistration. Link: https://lore.kernel.org/20260327033335.696621-4-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_core.c | 6 ++++++ kernel/liveupdate/luo_file.c | 22 +++++++++++++++++----- kernel/liveupdate/luo_internal.h | 2 ++ 3 files changed, 25 insertions(+), 5 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_core.c b/kernel/liveupdate/luo_core.c index 48b25c9abeda..803f51c84275 100644 --- a/kernel/liveupdate/luo_core.c +++ b/kernel/liveupdate/luo_core.c @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,11 @@ static struct { u64 liveupdate_num; } luo_global; +/* + * luo_register_rwlock - Protects registration of file handlers and FLBs. + */ +DECLARE_RWSEM(luo_register_rwlock); + static int __init early_liveupdate_param(char *buf) { return kstrtobool(buf, &luo_global.enabled); diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 8fcf302c73b6..91edbf4e44ac 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -288,12 +288,14 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd) goto err_fput; err = -ENOENT; + down_read(&luo_register_rwlock); list_private_for_each_entry(fh, &luo_file_handler_list, list) { if (fh->ops->can_preserve(fh, file)) { err = 0; break; } } + up_read(&luo_register_rwlock); /* err is still -ENOENT if no handler was found */ if (err) @@ -805,12 +807,14 @@ int luo_file_deserialize(struct luo_file_set *file_set, bool handler_found = false; struct luo_file *luo_file; + down_read(&luo_register_rwlock); list_private_for_each_entry(fh, &luo_file_handler_list, list) { if (!strcmp(fh->compatible, file_ser[i].compatible)) { handler_found = true; break; } } + up_read(&luo_register_rwlock); if (!handler_found) { pr_warn("No registered handler for compatible '%.*s'\n", @@ -879,32 +883,36 @@ int liveupdate_register_file_handler(struct liveupdate_file_handler *fh) if (!luo_session_quiesce()) return -EBUSY; + down_write(&luo_register_rwlock); /* Check for duplicate compatible strings */ list_private_for_each_entry(fh_iter, &luo_file_handler_list, list) { if (!strcmp(fh_iter->compatible, fh->compatible)) { pr_err("File handler registration failed: Compatible string '%s' already registered.\n", fh->compatible); err = -EEXIST; - goto err_resume; + goto err_unlock; } } /* Pin the module implementing the handler */ if (!try_module_get(fh->ops->owner)) { err = -EAGAIN; - goto err_resume; + goto err_unlock; } INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, flb_list)); INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, list)); list_add_tail(&ACCESS_PRIVATE(fh, list), &luo_file_handler_list); + up_write(&luo_register_rwlock); + luo_session_resume(); liveupdate_test_register(fh); return 0; -err_resume: +err_unlock: + up_write(&luo_register_rwlock); luo_session_resume(); return err; } @@ -938,16 +946,20 @@ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) if (!luo_session_quiesce()) goto err_register; + down_write(&luo_register_rwlock); if (!list_empty(&ACCESS_PRIVATE(fh, flb_list))) - goto err_resume; + goto err_unlock; list_del(&ACCESS_PRIVATE(fh, list)); + up_write(&luo_register_rwlock); + module_put(fh->ops->owner); luo_session_resume(); return 0; -err_resume: +err_unlock: + up_write(&luo_register_rwlock); luo_session_resume(); err_register: liveupdate_test_register(fh); diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h index 8083d8739b09..4bfe00ac8866 100644 --- a/kernel/liveupdate/luo_internal.h +++ b/kernel/liveupdate/luo_internal.h @@ -77,6 +77,8 @@ struct luo_session { struct mutex mutex; }; +extern struct rw_semaphore luo_register_rwlock; + int luo_session_create(const char *name, struct file **filep); int luo_session_retrieve(const char *name, struct file **filep); int __init luo_session_setup_outgoing(void *fdt); -- cgit v1.2.3 From 6b2b22f7c8cf1596490beaac96a989cbafdfea57 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:28 +0000 Subject: liveupdate: protect FLB lists with luo_register_rwlock Because liveupdate FLB objects will soon drop their persistent module references when registered, list traversals must be protected against concurrent module unloading. To provide this protection, utilize the global luo_register_rwlock. It protects the global registry of FLBs and the handler's specific list of FLB dependencies. Read locks are used during concurrent list traversals (e.g., during preservation and serialization). Write locks are taken during registration and unregistration. Link: https://lore.kernel.org/20260327033335.696621-5-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_flb.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index cf4a8f854c83..fdb274410e8f 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -245,17 +245,20 @@ int luo_flb_file_preserve(struct liveupdate_file_handler *fh) struct luo_flb_link *iter; int err = 0; + down_read(&luo_register_rwlock); list_for_each_entry(iter, flb_list, list) { err = luo_flb_file_preserve_one(iter->flb); if (err) goto exit_err; } + up_read(&luo_register_rwlock); return 0; exit_err: list_for_each_entry_continue_reverse(iter, flb_list, list) luo_flb_file_unpreserve_one(iter->flb); + up_read(&luo_register_rwlock); return err; } @@ -277,6 +280,7 @@ void luo_flb_file_unpreserve(struct liveupdate_file_handler *fh) struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list); struct luo_flb_link *iter; + guard(rwsem_read)(&luo_register_rwlock); list_for_each_entry_reverse(iter, flb_list, list) luo_flb_file_unpreserve_one(iter->flb); } @@ -297,6 +301,7 @@ void luo_flb_file_finish(struct liveupdate_file_handler *fh) struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list); struct luo_flb_link *iter; + guard(rwsem_read)(&luo_register_rwlock); list_for_each_entry_reverse(iter, flb_list, list) luo_flb_file_finish_one(iter->flb); } @@ -360,6 +365,8 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, if (!luo_session_quiesce()) return -EBUSY; + down_write(&luo_register_rwlock); + /* Check that this FLB is not already linked to this file handler */ err = -EEXIST; list_for_each_entry(iter, flb_list, list) { @@ -401,11 +408,13 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, private->users++; link->flb = flb; list_add_tail(&no_free_ptr(link)->list, flb_list); + up_write(&luo_register_rwlock); luo_session_resume(); return 0; err_resume: + up_write(&luo_register_rwlock); luo_session_resume(); return err; } @@ -449,6 +458,8 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, if (!luo_session_quiesce()) return -EBUSY; + down_write(&luo_register_rwlock); + /* Find and remove the link from the file handler's list */ list_for_each_entry(iter, flb_list, list) { if (iter->flb == flb) { @@ -473,11 +484,13 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, module_put(flb->ops->owner); } + up_write(&luo_register_rwlock); luo_session_resume(); return 0; err_resume: + up_write(&luo_register_rwlock); luo_session_resume(); return err; } @@ -643,6 +656,7 @@ void luo_flb_serialize(void) struct liveupdate_flb *gflb; int i = 0; + guard(rwsem_read)(&luo_register_rwlock); list_private_for_each_entry(gflb, &luo_flb_global.list, private.list) { struct luo_flb_private *private = luo_flb_get_private(gflb); -- cgit v1.2.3 From 76be9983df33aebd69716edaa8204ed90e72fef1 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:29 +0000 Subject: liveupdate: defer FLB module refcounting to active sessions Stop pinning modules indefinitely upon FLB registration. Instead, dynamically take a module reference when the FLB is actively used in a session (e.g., during preserve and retrieve) and release it when the session concludes. This allows modules providing FLB operations to be cleanly unloaded when not in active use by the live update orchestrator. Link: https://lore.kernel.org/20260327033335.696621-6-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Samiullah Khawaja Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_flb.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index fdb274410e8f..3d439d1c8ff1 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -115,10 +115,15 @@ static int luo_flb_file_preserve_one(struct liveupdate_flb *flb) struct liveupdate_flb_op_args args = {0}; int err; + if (!try_module_get(flb->ops->owner)) + return -ENODEV; + args.flb = flb; err = flb->ops->preserve(&args); - if (err) + if (err) { + module_put(flb->ops->owner); return err; + } private->outgoing.data = args.data; private->outgoing.obj = args.obj; } @@ -146,6 +151,7 @@ static void luo_flb_file_unpreserve_one(struct liveupdate_flb *flb) private->outgoing.data = 0; private->outgoing.obj = NULL; + module_put(flb->ops->owner); } } } @@ -181,12 +187,17 @@ static int luo_flb_retrieve_one(struct liveupdate_flb *flb) if (!found) return -ENOENT; + if (!try_module_get(flb->ops->owner)) + return -ENODEV; + args.flb = flb; args.data = private->incoming.data; err = flb->ops->retrieve(&args); - if (err) + if (err) { + module_put(flb->ops->owner); return err; + } private->incoming.obj = args.obj; private->incoming.retrieved = true; @@ -220,6 +231,7 @@ static void luo_flb_file_finish_one(struct liveupdate_flb *flb) private->incoming.data = 0; private->incoming.obj = NULL; private->incoming.finished = true; + module_put(flb->ops->owner); } } } @@ -395,11 +407,6 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, goto err_resume; } - if (!try_module_get(flb->ops->owner)) { - err = -EAGAIN; - goto err_resume; - } - list_add_tail(&private->list, &luo_flb_global.list); luo_flb_global.count++; } @@ -476,12 +483,11 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, private->users--; /* * If this is the last file-handler with which we are registred, remove - * from the global list, and relese module reference. + * from the global list. */ if (!private->users) { list_del_init(&private->list); luo_flb_global.count--; - module_put(flb->ops->owner); } up_write(&luo_register_rwlock); @@ -510,7 +516,8 @@ err_resume: * * Return: 0 on success, or a negative errno on failure. -ENODATA means no * incoming FLB data, -ENOENT means specific flb not found in the incoming - * data, and -EOPNOTSUPP when live update is disabled or not configured. + * data, -ENODEV if the FLB's module is unloading, and -EOPNOTSUPP when + * live update is disabled or not configured. */ int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp) { -- cgit v1.2.3 From 118c3908242076c6e281c7010d29c2d0607c3190 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:30 +0000 Subject: liveupdate: remove luo_session_quiesce() Now that FLB module references are handled dynamically during active sessions, we can safely remove the luo_session_quiesce() and luo_session_resume() mechanism. Link: https://lore.kernel.org/20260327033335.696621-7-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 21 +------------- kernel/liveupdate/luo_flb.c | 59 +++++++--------------------------------- kernel/liveupdate/luo_internal.h | 2 -- kernel/liveupdate/luo_session.c | 43 ----------------------------- 4 files changed, 11 insertions(+), 114 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 91edbf4e44ac..97342b8b8b69 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -875,14 +875,6 @@ int liveupdate_register_file_handler(struct liveupdate_file_handler *fh) return -EINVAL; } - /* - * Ensure the system is quiescent (no active sessions). - * This prevents registering new handlers while sessions are active or - * while deserialization is in progress. - */ - if (!luo_session_quiesce()) - return -EBUSY; - down_write(&luo_register_rwlock); /* Check for duplicate compatible strings */ list_private_for_each_entry(fh_iter, &luo_file_handler_list, list) { @@ -905,15 +897,12 @@ int liveupdate_register_file_handler(struct liveupdate_file_handler *fh) list_add_tail(&ACCESS_PRIVATE(fh, list), &luo_file_handler_list); up_write(&luo_register_rwlock); - luo_session_resume(); - liveupdate_test_register(fh); return 0; err_unlock: up_write(&luo_register_rwlock); - luo_session_resume(); return err; } @@ -925,14 +914,12 @@ err_unlock: * reverses the operations of liveupdate_register_file_handler(). * * It ensures safe removal by checking that: - * No live update session is currently in progress. * No FLB registered with this file handler. * * If the unregistration fails, the internal test state is reverted. * * Return: 0 Success. -EOPNOTSUPP when live update is not enabled. -EBUSY A live - * update is in progress, can't quiesce live update or FLB is registred with - * this file handler. + * update is in progress, FLB is registred with this file handler. */ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) { @@ -943,9 +930,6 @@ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) liveupdate_test_unregister(fh); - if (!luo_session_quiesce()) - goto err_register; - down_write(&luo_register_rwlock); if (!list_empty(&ACCESS_PRIVATE(fh, flb_list))) goto err_unlock; @@ -954,14 +938,11 @@ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) up_write(&luo_register_rwlock); module_put(fh->ops->owner); - luo_session_resume(); return 0; err_unlock: up_write(&luo_register_rwlock); - luo_session_resume(); -err_register: liveupdate_test_register(fh); return err; } diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index 3d439d1c8ff1..13f96d11ecc9 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -348,7 +348,6 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, struct luo_flb_link *link __free(kfree) = NULL; struct liveupdate_flb *gflb; struct luo_flb_link *iter; - int err; if (!liveupdate_enabled()) return -EOPNOTSUPP; @@ -369,21 +368,12 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, if (!link) return -ENOMEM; - /* - * Ensure the system is quiescent (no active sessions). - * This acts as a global lock for registration: no other thread can - * be in this section, and no sessions can be creating/using FDs. - */ - if (!luo_session_quiesce()) - return -EBUSY; - - down_write(&luo_register_rwlock); + guard(rwsem_write)(&luo_register_rwlock); /* Check that this FLB is not already linked to this file handler */ - err = -EEXIST; list_for_each_entry(iter, flb_list, list) { if (iter->flb == flb) - goto err_resume; + return -EEXIST; } /* @@ -391,20 +381,16 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, * is registered */ if (!private->users) { - if (WARN_ON(!list_empty(&private->list))) { - err = -EINVAL; - goto err_resume; - } + if (WARN_ON(!list_empty(&private->list))) + return -EINVAL; - if (luo_flb_global.count == LUO_FLB_MAX) { - err = -ENOSPC; - goto err_resume; - } + if (luo_flb_global.count == LUO_FLB_MAX) + return -ENOSPC; /* Check that compatible string is unique in global list */ list_private_for_each_entry(gflb, &luo_flb_global.list, private.list) { if (!strcmp(gflb->compatible, flb->compatible)) - goto err_resume; + return -EEXIST; } list_add_tail(&private->list, &luo_flb_global.list); @@ -415,15 +401,8 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, private->users++; link->flb = flb; list_add_tail(&no_free_ptr(link)->list, flb_list); - up_write(&luo_register_rwlock); - luo_session_resume(); return 0; - -err_resume: - up_write(&luo_register_rwlock); - luo_session_resume(); - return err; } /** @@ -439,12 +418,9 @@ err_resume: * the FLB is removed from the global registry and the reference to its * owner module (acquired during registration) is released. * - * Context: This function ensures the session is quiesced (no active FDs - * being created) during the update. It is typically called from a - * subsystem's module exit function. + * Context: It is typically called from a subsystem's module exit function. * Return: 0 on success. * -EOPNOTSUPP if live update is disabled. - * -EBUSY if the live update session is active and cannot be quiesced. * -ENOENT if the FLB was not found in the file handler's list. */ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, @@ -458,14 +434,7 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, if (!liveupdate_enabled()) return -EOPNOTSUPP; - /* - * Ensure the system is quiescent (no active sessions). - * This acts as a global lock for unregistration. - */ - if (!luo_session_quiesce()) - return -EBUSY; - - down_write(&luo_register_rwlock); + guard(rwsem_write)(&luo_register_rwlock); /* Find and remove the link from the file handler's list */ list_for_each_entry(iter, flb_list, list) { @@ -478,7 +447,7 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, } if (err) - goto err_resume; + return err; private->users--; /* @@ -490,15 +459,7 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, luo_flb_global.count--; } - up_write(&luo_register_rwlock); - luo_session_resume(); - return 0; - -err_resume: - up_write(&luo_register_rwlock); - luo_session_resume(); - return err; } /** diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h index 4bfe00ac8866..40a011bdfa55 100644 --- a/kernel/liveupdate/luo_internal.h +++ b/kernel/liveupdate/luo_internal.h @@ -85,8 +85,6 @@ int __init luo_session_setup_outgoing(void *fdt); int __init luo_session_setup_incoming(void *fdt); int luo_session_serialize(void); int luo_session_deserialize(void); -bool luo_session_quiesce(void); -void luo_session_resume(void); int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd); void luo_file_unpreserve_files(struct luo_file_set *file_set); diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c index c68a0041bcf2..e5d35e83ac3d 100644 --- a/kernel/liveupdate/luo_session.c +++ b/kernel/liveupdate/luo_session.c @@ -602,46 +602,3 @@ err_undo: return err; } -/** - * luo_session_quiesce - Ensure no active sessions exist and lock session lists. - * - * Acquires exclusive write locks on both incoming and outgoing session lists. - * It then validates no sessions exist in either list. - * - * This mechanism is used during file handler un/registration to ensure that no - * sessions are currently using the handler, and no new sessions can be created - * while un/registration is in progress. - * - * This prevents registering new handlers while sessions are active or - * while deserialization is in progress. - * - * Return: - * true - System is quiescent (0 sessions) and locked. - * false - Active sessions exist. The locks are released internally. - */ -bool luo_session_quiesce(void) -{ - down_write(&luo_session_global.incoming.rwsem); - down_write(&luo_session_global.outgoing.rwsem); - - if (luo_session_global.incoming.count || - luo_session_global.outgoing.count) { - up_write(&luo_session_global.outgoing.rwsem); - up_write(&luo_session_global.incoming.rwsem); - return false; - } - - return true; -} - -/** - * luo_session_resume - Unlock session lists and resume normal activity. - * - * Releases the exclusive locks acquired by a successful call to - * luo_session_quiesce(). - */ -void luo_session_resume(void) -{ - up_write(&luo_session_global.outgoing.rwsem); - up_write(&luo_session_global.incoming.rwsem); -} -- cgit v1.2.3 From 5ee1c7d6414a0b1cb7285bd4904b4969c0d9fab1 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:31 +0000 Subject: liveupdate: auto unregister FLBs on file handler unregistration To ensure that unregistration is always successful and doesn't leave dangling resources, introduce auto-unregistration of FLBs: when a file handler is unregistered, all FLBs associated with it are automatically unregistered. Introduce a new helper luo_flb_unregister_all() which unregisters all FLBs linked to the given file handler. Link: https://lore.kernel.org/20260327033335.696621-8-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 14 +------ kernel/liveupdate/luo_flb.c | 84 +++++++++++++++++++++++++++------------- kernel/liveupdate/luo_internal.h | 1 + 3 files changed, 60 insertions(+), 39 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 97342b8b8b69..9ba904c10425 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -923,26 +923,16 @@ err_unlock: */ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) { - int err = -EBUSY; - if (!liveupdate_enabled()) return -EOPNOTSUPP; liveupdate_test_unregister(fh); - down_write(&luo_register_rwlock); - if (!list_empty(&ACCESS_PRIVATE(fh, flb_list))) - goto err_unlock; - + guard(rwsem_write)(&luo_register_rwlock); + luo_flb_unregister_all(fh); list_del(&ACCESS_PRIVATE(fh, list)); - up_write(&luo_register_rwlock); module_put(fh->ops->owner); return 0; - -err_unlock: - up_write(&luo_register_rwlock); - liveupdate_test_register(fh); - return err; } diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index 13f96d11ecc9..e069d694163e 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -318,6 +318,62 @@ void luo_flb_file_finish(struct liveupdate_file_handler *fh) luo_flb_file_finish_one(iter->flb); } +static void luo_flb_unregister_one(struct liveupdate_file_handler *fh, + struct liveupdate_flb *flb) +{ + struct luo_flb_private *private = luo_flb_get_private(flb); + struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list); + struct luo_flb_link *iter; + bool found = false; + + /* Find and remove the link from the file handler's list */ + list_for_each_entry(iter, flb_list, list) { + if (iter->flb == flb) { + list_del(&iter->list); + kfree(iter); + found = true; + break; + } + } + + if (!found) { + pr_warn("Failed to unregister FLB '%s': not found in file handler '%s'\n", + flb->compatible, fh->compatible); + return; + } + + private->users--; + + /* + * If this is the last file-handler with which we are registred, remove + * from the global list. + */ + if (!private->users) { + list_del_init(&private->list); + luo_flb_global.count--; + } +} + +/** + * luo_flb_unregister_all - Unregister all FLBs associated with a file handler. + * @fh: The file handler whose FLBs should be unregistered. + * + * This function iterates through the list of FLBs associated with the given + * file handler and unregisters them all one by one. + */ +void luo_flb_unregister_all(struct liveupdate_file_handler *fh) +{ + struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list); + struct luo_flb_link *iter, *tmp; + + if (!liveupdate_enabled()) + return; + + lockdep_assert_held_write(&luo_register_rwlock); + list_for_each_entry_safe(iter, tmp, flb_list, list) + luo_flb_unregister_one(fh, iter->flb); +} + /** * liveupdate_register_flb - Associate an FLB with a file handler and register it globally. * @fh: The file handler that will now depend on the FLB. @@ -426,38 +482,12 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, struct liveupdate_flb *flb) { - struct luo_flb_private *private = luo_flb_get_private(flb); - struct list_head *flb_list = &ACCESS_PRIVATE(fh, flb_list); - struct luo_flb_link *iter; - int err = -ENOENT; - if (!liveupdate_enabled()) return -EOPNOTSUPP; guard(rwsem_write)(&luo_register_rwlock); - /* Find and remove the link from the file handler's list */ - list_for_each_entry(iter, flb_list, list) { - if (iter->flb == flb) { - list_del(&iter->list); - kfree(iter); - err = 0; - break; - } - } - - if (err) - return err; - - private->users--; - /* - * If this is the last file-handler with which we are registred, remove - * from the global list. - */ - if (!private->users) { - list_del_init(&private->list); - luo_flb_global.count--; - } + luo_flb_unregister_one(fh, flb); return 0; } diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h index 40a011bdfa55..22f6901f89ed 100644 --- a/kernel/liveupdate/luo_internal.h +++ b/kernel/liveupdate/luo_internal.h @@ -103,6 +103,7 @@ void luo_file_set_destroy(struct luo_file_set *file_set); int luo_flb_file_preserve(struct liveupdate_file_handler *fh); void luo_flb_file_unpreserve(struct liveupdate_file_handler *fh); void luo_flb_file_finish(struct liveupdate_file_handler *fh); +void luo_flb_unregister_all(struct liveupdate_file_handler *fh); int __init luo_flb_setup_outgoing(void *fdt); int __init luo_flb_setup_incoming(void *fdt); void luo_flb_serialize(void); -- cgit v1.2.3 From 074488008d6e745af067e968d6046f2c04b12537 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:32 +0000 Subject: liveupdate: remove liveupdate_test_unregister() Now that file handler unregistration automatically unregisters all associated file handlers (FLBs), the liveupdate_test_unregister() function is no longer needed. Remove it along with its usages and declarations. Link: https://lore.kernel.org/20260327033335.696621-9-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 2 -- kernel/liveupdate/luo_internal.h | 2 -- 2 files changed, 4 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 9ba904c10425..4060b6064248 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -926,8 +926,6 @@ int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) if (!liveupdate_enabled()) return -EOPNOTSUPP; - liveupdate_test_unregister(fh); - guard(rwsem_write)(&luo_register_rwlock); luo_flb_unregister_all(fh); list_del(&ACCESS_PRIVATE(fh, list)); diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h index 22f6901f89ed..875844d7a41d 100644 --- a/kernel/liveupdate/luo_internal.h +++ b/kernel/liveupdate/luo_internal.h @@ -110,10 +110,8 @@ void luo_flb_serialize(void); #ifdef CONFIG_LIVEUPDATE_TEST void liveupdate_test_register(struct liveupdate_file_handler *fh); -void liveupdate_test_unregister(struct liveupdate_file_handler *fh); #else static inline void liveupdate_test_register(struct liveupdate_file_handler *fh) { } -static inline void liveupdate_test_unregister(struct liveupdate_file_handler *fh) { } #endif #endif /* _LINUX_LUO_INTERNAL_H */ -- cgit v1.2.3 From 2ab7207e7ec6cd5af1912d9be5174f114633286b Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:33 +0000 Subject: liveupdate: make unregister functions return void Change liveupdate_unregister_file_handler and liveupdate_unregister_flb to return void instead of an error code. This follows the design principle that unregistration during module unload should not fail, as the unload cannot be stopped at that point. Link: https://lore.kernel.org/20260327033335.696621-10-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 14 ++------------ kernel/liveupdate/luo_flb.c | 11 +++-------- 2 files changed, 5 insertions(+), 20 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 4060b6064248..0730865711c1 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -912,25 +912,15 @@ err_unlock: * * Unregisters the file handler from the liveupdate core. This function * reverses the operations of liveupdate_register_file_handler(). - * - * It ensures safe removal by checking that: - * No FLB registered with this file handler. - * - * If the unregistration fails, the internal test state is reverted. - * - * Return: 0 Success. -EOPNOTSUPP when live update is not enabled. -EBUSY A live - * update is in progress, FLB is registred with this file handler. */ -int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) +void liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) { if (!liveupdate_enabled()) - return -EOPNOTSUPP; + return; guard(rwsem_write)(&luo_register_rwlock); luo_flb_unregister_all(fh); list_del(&ACCESS_PRIVATE(fh, list)); module_put(fh->ops->owner); - - return 0; } diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c index e069d694163e..00f5494812c4 100644 --- a/kernel/liveupdate/luo_flb.c +++ b/kernel/liveupdate/luo_flb.c @@ -475,21 +475,16 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh, * owner module (acquired during registration) is released. * * Context: It is typically called from a subsystem's module exit function. - * Return: 0 on success. - * -EOPNOTSUPP if live update is disabled. - * -ENOENT if the FLB was not found in the file handler's list. */ -int liveupdate_unregister_flb(struct liveupdate_file_handler *fh, - struct liveupdate_flb *flb) +void liveupdate_unregister_flb(struct liveupdate_file_handler *fh, + struct liveupdate_flb *flb) { if (!liveupdate_enabled()) - return -EOPNOTSUPP; + return; guard(rwsem_write)(&luo_register_rwlock); luo_flb_unregister_one(fh, flb); - - return 0; } /** -- cgit v1.2.3 From 68750e820bc4095d25cf70002782c284e5702415 Mon Sep 17 00:00:00 2001 From: Pasha Tatashin Date: Fri, 27 Mar 2026 03:33:34 +0000 Subject: liveupdate: defer file handler module refcounting to active sessions Stop pinning modules indefinitely upon file handler registration. Instead, dynamically increment the module reference count only when a live update session actively uses the file handler (e.g., during preservation or deserialization), and release it when the session ends. This allows modules providing live update handlers to be gracefully unloaded when no live update is in progress. Link: https://lore.kernel.org/20260327033335.696621-11-pasha.tatashin@soleen.com Signed-off-by: Pasha Tatashin Reviewed-by: Pratyush Yadav (Google) Cc: David Matlack Cc: Mike Rapoport Cc: Samiullah Khawaja Signed-off-by: Andrew Morton --- kernel/liveupdate/luo_file.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'kernel/liveupdate') diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c index 0730865711c1..a0a419085e28 100644 --- a/kernel/liveupdate/luo_file.c +++ b/kernel/liveupdate/luo_file.c @@ -291,7 +291,8 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd) down_read(&luo_register_rwlock); list_private_for_each_entry(fh, &luo_file_handler_list, list) { if (fh->ops->can_preserve(fh, file)) { - err = 0; + if (try_module_get(fh->ops->owner)) + err = 0; break; } } @@ -304,7 +305,7 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd) err = xa_insert(&luo_preserved_files, luo_get_id(fh, file), file, GFP_KERNEL); if (err) - goto err_free_files_mem; + goto err_module_put; err = luo_flb_file_preserve(fh); if (err) @@ -340,6 +341,8 @@ err_flb_unpreserve: luo_flb_file_unpreserve(fh); err_erase_xa: xa_erase(&luo_preserved_files, luo_get_id(fh, file)); +err_module_put: + module_put(fh->ops->owner); err_free_files_mem: luo_free_files_mem(file_set); err_fput: @@ -382,6 +385,7 @@ void luo_file_unpreserve_files(struct luo_file_set *file_set) args.private_data = luo_file->private_data; luo_file->fh->ops->unpreserve(&args); luo_flb_file_unpreserve(luo_file->fh); + module_put(luo_file->fh->ops->owner); xa_erase(&luo_preserved_files, luo_get_id(luo_file->fh, luo_file->file)); @@ -673,6 +677,7 @@ static void luo_file_finish_one(struct luo_file_set *file_set, luo_file->fh->ops->finish(&args); luo_flb_file_finish(luo_file->fh); + module_put(luo_file->fh->ops->owner); } /** @@ -810,7 +815,8 @@ int luo_file_deserialize(struct luo_file_set *file_set, down_read(&luo_register_rwlock); list_private_for_each_entry(fh, &luo_file_handler_list, list) { if (!strcmp(fh->compatible, file_ser[i].compatible)) { - handler_found = true; + if (try_module_get(fh->ops->owner)) + handler_found = true; break; } } @@ -824,8 +830,10 @@ int luo_file_deserialize(struct luo_file_set *file_set, } luo_file = kzalloc_obj(*luo_file); - if (!luo_file) + if (!luo_file) { + module_put(fh->ops->owner); return -ENOMEM; + } luo_file->fh = fh; luo_file->file = NULL; @@ -886,12 +894,6 @@ int liveupdate_register_file_handler(struct liveupdate_file_handler *fh) } } - /* Pin the module implementing the handler */ - if (!try_module_get(fh->ops->owner)) { - err = -EAGAIN; - goto err_unlock; - } - INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, flb_list)); INIT_LIST_HEAD(&ACCESS_PRIVATE(fh, list)); list_add_tail(&ACCESS_PRIVATE(fh, list), &luo_file_handler_list); @@ -921,6 +923,4 @@ void liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh) guard(rwsem_write)(&luo_register_rwlock); luo_flb_unregister_all(fh); list_del(&ACCESS_PRIVATE(fh, list)); - - module_put(fh->ops->owner); } -- cgit v1.2.3