* [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
@ 2026-03-24 21:27 Oskar Gerlicz Kowalczuk
2026-03-24 21:27 ` [PATCH v4 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
2026-03-25 13:38 ` [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Pasha Tatashin
0 siblings, 2 replies; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:27 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
Hi Pasha,
this v4 keeps the simpler direction from your mail: outgoing handover is
still driven by a boolean rebooting gate, refcount pinning of outgoing
sessions during serialization, and session->mutex as the serialization
point for in-flight mutations. There is no return to the earlier closing
counter or a larger session state machine.
The main changes in this respin are:
- reshape the series into five commits, each building and standing on its
own
- keep incoming session origin immutable and use retrieved only as the
checked-out bit
- make FINISH and implicit close consume incoming sessions without
reopening races through retrieve-by-name
- route deserialize failures through explicit rollback paths for
sessions, files, and serialized memfd state
- validate KHO-preserved extents before walking serialized metadata
- harden incoming FLB lifetime and remaining teardown paths
Patches 1-4 keep the core session, kexec, deserialize and validation work
separate. Patch 5 carries the remaining FLB and teardown fixes needed to
match the final tree.
Oskar Gerlicz Kowalczuk (5):
liveupdate: block outgoing session updates during reboot
kexec: abort liveupdate handover on kernel_kexec() unwind
liveupdate: fail session restore on file deserialization errors
liveupdate: validate handover metadata before using it
liveupdate: harden FLB lifetime and remaining teardown paths
include/linux/kexec_handover.h | 13 +
include/linux/liveupdate.h | 17 +-
kernel/kexec_core.c | 4 +
kernel/liveupdate/kexec_handover.c | 22 ++
kernel/liveupdate/luo_core.c | 16 +-
kernel/liveupdate/luo_file.c | 237 ++++++++++++--
kernel/liveupdate/luo_flb.c | 116 +++++--
kernel/liveupdate/luo_internal.h | 14 +-
kernel/liveupdate/luo_session.c | 500 ++++++++++++++++++++++++-----
lib/tests/liveupdate.c | 2 +
mm/memfd_luo.c | 160 +++++++--
11 files changed, 934 insertions(+), 167 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v4 1/5] liveupdate: block outgoing session updates during reboot
2026-03-24 21:27 [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Oskar Gerlicz Kowalczuk
@ 2026-03-24 21:27 ` Oskar Gerlicz Kowalczuk
2026-03-24 21:27 ` [PATCH v4 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
2026-03-25 13:38 ` [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Pasha Tatashin
1 sibling, 1 reply; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:27 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
Block new outgoing session mutations with the rebooting gate and serialize
in-flight operations with session->mutex. During serialization, pin the
outgoing sessions with temporary references instead of relying on an
auxiliary in-flight counter.
Also make incoming session origin immutable and use retrieved only as the
"checked out by userspace" bit. This keeps FINISH, close(), and
retrieve-by-name on a single lifetime model without reintroducing a larger
session state machine.
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
kernel/liveupdate/luo_internal.h | 9 +-
kernel/liveupdate/luo_session.c | 308 +++++++++++++++++++++++++------
2 files changed, 259 insertions(+), 58 deletions(-)
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index 8083d8739b09..e29e1a46b491 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -9,6 +9,7 @@
#define _LINUX_LUO_INTERNAL_H
#include <linux/liveupdate.h>
+#include <linux/refcount.h>
#include <linux/uaccess.h>
struct luo_ucmd {
@@ -63,8 +64,10 @@ struct luo_file_set {
* @list: A list_head member used to link this session into a global list
* of either outgoing (to be preserved) or incoming (restored from
* previous kernel) sessions.
- * @retrieved: A boolean flag indicating whether this session has been
- * retrieved by a consumer in the new kernel.
+ * @incoming: True for sessions restored from the previous kernel. This is
+ * immutable after publication and distinguishes incoming sessions
+ * from outgoing ones.
+ * @retrieved: True while an incoming session is checked out by userspace.
* @file_set: A set of files that belong to this session.
* @mutex: protects fields in the luo_session.
*/
@@ -72,8 +75,10 @@ struct luo_session {
char name[LIVEUPDATE_SESSION_NAME_LENGTH];
struct luo_session_ser *ser;
struct list_head list;
+ bool incoming;
bool retrieved;
struct luo_file_set file_set;
+ refcount_t refs;
struct mutex mutex;
};
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 783677295640..63aee9cc881d 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -66,6 +66,7 @@
#include <linux/rwsem.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
+#include <linux/wait.h>
#include <uapi/linux/liveupdate.h>
#include "luo_internal.h"
@@ -81,9 +82,12 @@
* @list: The head of the linked list of `struct luo_session` instances.
* @rwsem: A read-write semaphore providing synchronized access to the
* session list and other fields in this structure.
+ * @reboot_waitq: Wait queue used by close() and serialize() to wait for
+ * rebooting transitions.
* @header_ser: The header data of serialization array.
* @ser: The serialized session data (an array of
* `struct luo_session_ser`).
+ * @rebooting: True while outgoing sessions are frozen for kexec handover.
* @active: Set to true when first initialized. If previous kernel did not
* send session data, active stays false for incoming.
*/
@@ -91,8 +95,10 @@ struct luo_session_header {
long count;
struct list_head list;
struct rw_semaphore rwsem;
+ wait_queue_head_t reboot_waitq;
struct luo_session_header_ser *header_ser;
struct luo_session_ser *ser;
+ bool rebooting;
bool active;
};
@@ -110,13 +116,23 @@ static struct luo_session_global luo_session_global = {
.incoming = {
.list = LIST_HEAD_INIT(luo_session_global.incoming.list),
.rwsem = __RWSEM_INITIALIZER(luo_session_global.incoming.rwsem),
+ .reboot_waitq =
+ __WAIT_QUEUE_HEAD_INITIALIZER(luo_session_global.incoming.reboot_waitq),
},
.outgoing = {
.list = LIST_HEAD_INIT(luo_session_global.outgoing.list),
.rwsem = __RWSEM_INITIALIZER(luo_session_global.outgoing.rwsem),
+ .reboot_waitq =
+ __WAIT_QUEUE_HEAD_INITIALIZER(luo_session_global.outgoing.reboot_waitq),
},
};
+static void luo_session_reboot_done(struct luo_session_header *sh)
+{
+ WRITE_ONCE(sh->rebooting, false);
+ wake_up_all(&sh->reboot_waitq);
+}
+
static struct luo_session *luo_session_alloc(const char *name)
{
struct luo_session *session = kzalloc_obj(*session);
@@ -128,6 +144,7 @@ static struct luo_session *luo_session_alloc(const char *name)
INIT_LIST_HEAD(&session->file_set.files_list);
luo_file_set_init(&session->file_set);
INIT_LIST_HEAD(&session->list);
+ refcount_set(&session->refs, 1);
mutex_init(&session->mutex);
return session;
@@ -140,6 +157,17 @@ static void luo_session_free(struct luo_session *session)
kfree(session);
}
+static void luo_session_get(struct luo_session *session)
+{
+ refcount_inc(&session->refs);
+}
+
+static void luo_session_put(struct luo_session *session)
+{
+ if (refcount_dec_and_test(&session->refs))
+ luo_session_free(session);
+}
+
static int luo_session_insert(struct luo_session_header *sh,
struct luo_session *session)
{
@@ -152,6 +180,9 @@ static int luo_session_insert(struct luo_session_header *sh,
* for new session.
*/
if (sh == &luo_session_global.outgoing) {
+ if (READ_ONCE(sh->rebooting))
+ return -EBUSY;
+
if (sh->count == LUO_SESSION_MAX)
return -ENOMEM;
}
@@ -172,58 +203,157 @@ static int luo_session_insert(struct luo_session_header *sh,
return 0;
}
+static void __luo_session_remove(struct luo_session_header *sh,
+ struct luo_session *session)
+{
+ list_del_init(&session->list);
+ sh->count--;
+}
+
static void luo_session_remove(struct luo_session_header *sh,
struct luo_session *session)
{
guard(rwsem_write)(&sh->rwsem);
- list_del(&session->list);
- sh->count--;
+ __luo_session_remove(sh, session);
}
-static int luo_session_finish_one(struct luo_session *session)
+static int luo_session_outgoing_begin(struct luo_session *session,
+ struct luo_session_header **shp)
{
- guard(mutex)(&session->mutex);
- return luo_file_finish(&session->file_set);
+ struct luo_session_header *sh;
+
+ if (session->incoming)
+ return -EINVAL;
+
+ sh = &luo_session_global.outgoing;
+ down_read(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting)) {
+ up_read(&sh->rwsem);
+ return -EBUSY;
+ }
+
+ *shp = sh;
+ return 0;
}
-static void luo_session_unfreeze_one(struct luo_session *session,
- struct luo_session_ser *ser)
+static void luo_session_outgoing_end(struct luo_session_header *sh)
{
- guard(mutex)(&session->mutex);
- luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
+ if (sh)
+ up_read(&sh->rwsem);
}
-static int luo_session_freeze_one(struct luo_session *session,
- struct luo_session_ser *ser)
+static int luo_session_get_all_outgoing(struct luo_session **sessions)
{
- guard(mutex)(&session->mutex);
- return luo_file_freeze(&session->file_set, &ser->file_set_ser);
+ struct luo_session_header *sh = &luo_session_global.outgoing;
+ struct luo_session *session;
+ u64 nr = 0;
+
+ down_write(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting)) {
+ up_write(&sh->rwsem);
+ return -EBUSY;
+ }
+
+ WRITE_ONCE(sh->rebooting, true);
+ list_for_each_entry(session, &sh->list, list) {
+ luo_session_get(session);
+ sessions[nr++] = session;
+ }
+ up_write(&sh->rwsem);
+
+ return nr;
+}
+
+static void luo_session_put_all(struct luo_session **sessions, u64 nr)
+{
+ while (nr)
+ luo_session_put(sessions[--nr]);
+}
+
+static void luo_session_wait_reboot(struct luo_session_header *sh)
+{
+ DEFINE_WAIT(wait);
+
+ for (;;) {
+ prepare_to_wait(&sh->reboot_waitq, &wait,
+ TASK_UNINTERRUPTIBLE | TASK_FREEZABLE);
+ if (!READ_ONCE(sh->rebooting))
+ break;
+ schedule();
+ }
+
+ finish_wait(&sh->reboot_waitq, &wait);
+}
+
+static int luo_session_finish_retrieved(struct luo_session *session)
+{
+ lockdep_assert_held(&session->mutex);
+ if (!session->retrieved)
+ return -EINVAL;
+
+ return luo_file_finish(&session->file_set);
}
static int luo_session_release(struct inode *inodep, struct file *filep)
{
struct luo_session *session = filep->private_data;
- struct luo_session_header *sh;
+ struct luo_session_header *sh = &luo_session_global.incoming;
+ int ret = 0;
+ bool removed = false;
- /* If retrieved is set, it means this session is from incoming list */
- if (session->retrieved) {
- int err = luo_session_finish_one(session);
+ if (session->incoming) {
+ scoped_guard(mutex, &session->mutex) {
+ if (session->retrieved) {
+ ret = luo_session_finish_retrieved(session);
+ if (!ret) {
+ down_write(&sh->rwsem);
+ if (!list_empty(&session->list)) {
+ __luo_session_remove(sh, session);
+ removed = true;
+ }
+ up_write(&sh->rwsem);
+ }
+ WRITE_ONCE(session->retrieved, false);
+ }
+ }
- if (err) {
+ if (ret) {
pr_warn("Unable to finish session [%s] on release\n",
session->name);
- return err;
+ luo_session_put(session);
+ return ret;
+ }
+
+ if (removed)
+ luo_session_put(session);
+ luo_session_put(session);
+ return 0;
+ }
+
+ sh = &luo_session_global.outgoing;
+
+ for (;;) {
+ down_read(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting)) {
+ up_read(&sh->rwsem);
+ luo_session_wait_reboot(sh);
+ continue;
}
- sh = &luo_session_global.incoming;
- } else {
- scoped_guard(mutex, &session->mutex)
- luo_file_unpreserve_files(&session->file_set);
- sh = &luo_session_global.outgoing;
+
+ mutex_lock(&session->mutex);
+ up_read(&sh->rwsem);
+ break;
}
- luo_session_remove(sh, session);
- luo_session_free(session);
+ luo_file_unpreserve_files(&session->file_set);
+ down_write(&sh->rwsem);
+ __luo_session_remove(sh, session);
+ up_write(&sh->rwsem);
+ mutex_unlock(&session->mutex);
+
+ luo_session_put(session);
+ luo_session_put(session);
return 0;
}
@@ -231,10 +361,16 @@ static int luo_session_preserve_fd(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_preserve_fd *argp = ucmd->cmd;
+ struct luo_session_header *sh = NULL;
int err;
- guard(mutex)(&session->mutex);
- err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
+ err = luo_session_outgoing_begin(session, &sh);
+ if (err)
+ return err;
+
+ scoped_guard(mutex, &session->mutex)
+ err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
+ luo_session_outgoing_end(sh);
if (err)
return err;
@@ -256,8 +392,13 @@ static int luo_session_retrieve_fd(struct luo_session *session,
if (argp->fd < 0)
return argp->fd;
- guard(mutex)(&session->mutex);
- err = luo_retrieve_file(&session->file_set, argp->token, &file);
+ scoped_guard(mutex, &session->mutex) {
+ if (!session->retrieved)
+ err = -EINVAL;
+ else
+ err = luo_retrieve_file(&session->file_set,
+ argp->token, &file);
+ }
if (err < 0)
goto err_put_fd;
@@ -281,11 +422,28 @@ static int luo_session_finish(struct luo_session *session,
struct luo_ucmd *ucmd)
{
struct liveupdate_session_finish *argp = ucmd->cmd;
- int err = luo_session_finish_one(session);
+ struct luo_session_header *sh = &luo_session_global.incoming;
+ bool removed = false;
+ int err;
+ scoped_guard(mutex, &session->mutex) {
+ err = luo_session_finish_retrieved(session);
+ if (!err) {
+ down_write(&sh->rwsem);
+ if (!list_empty(&session->list)) {
+ __luo_session_remove(sh, session);
+ removed = true;
+ }
+ up_write(&sh->rwsem);
+ WRITE_ONCE(session->retrieved, false);
+ }
+ }
if (err)
return err;
+ if (removed)
+ luo_session_put(session);
+
return luo_ucmd_respond(ucmd, sizeof(*argp));
}
@@ -371,9 +529,12 @@ static int luo_session_getfile(struct luo_session *session, struct file **filep)
lockdep_assert_held(&session->mutex);
snprintf(name_buf, sizeof(name_buf), "[luo_session] %s", session->name);
+ luo_session_get(session);
file = anon_inode_getfile(name_buf, &luo_session_fops, session, O_RDWR);
- if (IS_ERR(file))
+ if (IS_ERR(file)) {
+ luo_session_put(session);
return PTR_ERR(file);
+ }
*filep = file;
@@ -401,9 +562,10 @@ int luo_session_create(const char *name, struct file **filep)
return 0;
err_remove:
- luo_session_remove(&luo_session_global.outgoing, session);
+ scoped_guard(mutex, &session->mutex)
+ luo_session_remove(&luo_session_global.outgoing, session);
err_free:
- luo_session_free(session);
+ luo_session_put(session);
return err;
}
@@ -418,6 +580,8 @@ int luo_session_retrieve(const char *name, struct file **filep)
scoped_guard(rwsem_read, &sh->rwsem) {
list_for_each_entry(it, &sh->list, list) {
if (!strncmp(it->name, name, sizeof(it->name))) {
+ /* Keep the session alive after dropping rwsem. */
+ luo_session_get(it);
session = it;
break;
}
@@ -428,12 +592,14 @@ int luo_session_retrieve(const char *name, struct file **filep)
return -ENOENT;
guard(mutex)(&session->mutex);
- if (session->retrieved)
- return -EINVAL;
+ if (session->retrieved || list_empty(&session->list))
+ err = -EINVAL;
+ else
+ err = luo_session_getfile(session, filep);
- err = luo_session_getfile(session, filep);
if (!err)
- session->retrieved = true;
+ WRITE_ONCE(session->retrieved, true);
+ luo_session_put(session);
return err;
}
@@ -548,6 +714,7 @@ int luo_session_deserialize(void)
sh->ser[i].name, session);
return PTR_ERR(session);
}
+ session->incoming = true;
err = luo_session_insert(sh, session);
if (err) {
@@ -573,32 +740,61 @@ int luo_session_deserialize(void)
int luo_session_serialize(void)
{
struct luo_session_header *sh = &luo_session_global.outgoing;
- struct luo_session *session;
- int i = 0;
+ struct luo_session **sessions;
+ u64 nr;
+ u64 i;
+ u64 frozen_nr = 0;
int err;
- guard(rwsem_write)(&sh->rwsem);
- list_for_each_entry(session, &sh->list, list) {
- err = luo_session_freeze_one(session, &sh->ser[i]);
- if (err)
- goto err_undo;
+ sessions = kcalloc(LUO_SESSION_MAX, sizeof(*sessions), GFP_KERNEL);
+ if (!sessions)
+ return -ENOMEM;
- strscpy(sh->ser[i].name, session->name,
- sizeof(sh->ser[i].name));
- i++;
- }
- sh->header_ser->count = sh->count;
+ err = luo_session_get_all_outgoing(sessions);
+ if (err < 0)
+ goto out_free_sessions;
- return 0;
+ nr = err;
+ for (i = 0; i < nr; i++) {
+ struct luo_session *session = sessions[i];
-err_undo:
- list_for_each_entry_continue_reverse(session, &sh->list, list) {
- i--;
- luo_session_unfreeze_one(session, &sh->ser[i]);
- memset(sh->ser[i].name, 0, sizeof(sh->ser[i].name));
+ scoped_guard(mutex, &session->mutex) {
+ if (list_empty(&session->list))
+ continue;
+
+ err = luo_file_freeze(&session->file_set,
+ &sh->ser[frozen_nr].file_set_ser);
+ if (err)
+ goto err_undo;
+
+ strscpy(sh->ser[frozen_nr].name, session->name,
+ sizeof(sh->ser[frozen_nr].name));
+ swap(sessions[frozen_nr], sessions[i]);
+ frozen_nr++;
+ }
}
+ sh->header_ser->count = frozen_nr;
+ err = 0;
+out_put_sessions:
+ luo_session_put_all(sessions, nr);
+out_free_sessions:
+ kfree(sessions);
return err;
+
+err_undo:
+ while (frozen_nr) {
+ struct luo_session *session = sessions[--frozen_nr];
+
+ scoped_guard(mutex, &session->mutex) {
+ luo_file_unfreeze(&session->file_set,
+ &sh->ser[frozen_nr].file_set_ser);
+ memset(&sh->ser[frozen_nr], 0, sizeof(sh->ser[frozen_nr]));
+ }
+ }
+ sh->header_ser->count = 0;
+ luo_session_reboot_done(sh);
+ goto out_put_sessions;
}
/**
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind
2026-03-24 21:27 ` [PATCH v4 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
@ 2026-03-24 21:27 ` Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
0 siblings, 1 reply; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:27 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
liveupdate_reboot() can freeze outgoing session state before the kexec
transition completes. v2 only unwound that state when kernel_kexec()
returned a non-zero error, which misses the preserve_context path where
machine_kexec() can return to the old kernel with error == 0.
If that return path skips the abort, outgoing sessions remain stuck in
the reboot state and later user space operations cannot make progress.
Treat every return to the original kernel after a prepared liveupdate
handover as an unwind point and call liveupdate_reboot_abort() there as
well.
Fixes: 8b32d3ced269 ("kexec: abort liveupdate handover on kernel_kexec() unwind")
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
include/linux/liveupdate.h | 5 +++++
kernel/kexec_core.c | 6 +++++-
kernel/liveupdate/luo_core.c | 11 ++++++++++-
kernel/liveupdate/luo_internal.h | 1 +
kernel/liveupdate/luo_session.c | 30 ++++++++++++++++++++++++++++++
5 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index dd11fdc76a5f..d93b043a0421 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -226,6 +226,7 @@ bool liveupdate_enabled(void);
/* Called during kexec to tell LUO that entered into reboot */
int liveupdate_reboot(void);
+void liveupdate_reboot_abort(void);
int liveupdate_register_file_handler(struct liveupdate_file_handler *fh);
int liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh);
@@ -250,6 +251,10 @@ static inline int liveupdate_reboot(void)
return 0;
}
+static inline void liveupdate_reboot_abort(void)
+{
+}
+
static inline int liveupdate_register_file_handler(struct liveupdate_file_handler *fh)
{
return -EOPNOTSUPP;
diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c
index 2fea396d29b9..95710a7d5e56 100644
--- a/kernel/kexec_core.c
+++ b/kernel/kexec_core.c
@@ -1139,6 +1139,7 @@ bool kexec_load_permitted(int kexec_image_type)
int kernel_kexec(void)
{
int error = 0;
+ bool liveupdate_prepared = false;
if (!kexec_trylock())
return -EBUSY;
@@ -1147,6 +1148,7 @@ int kernel_kexec(void)
goto Unlock;
}
+ liveupdate_prepared = true;
error = liveupdate_reboot();
if (error)
goto Unlock;
@@ -1230,7 +1232,9 @@ int kernel_kexec(void)
}
#endif
- Unlock:
+Unlock:
+ if (liveupdate_prepared)
+ liveupdate_reboot_abort();
kexec_unlock();
return error;
}
diff --git a/kernel/liveupdate/luo_core.c b/kernel/liveupdate/luo_core.c
index dda7bb57d421..95a0b81ce60d 100644
--- a/kernel/liveupdate/luo_core.c
+++ b/kernel/liveupdate/luo_core.c
@@ -233,8 +233,9 @@ int liveupdate_reboot(void)
err = kho_finalize();
if (err) {
pr_err("kho_finalize failed %d\n", err);
+ liveupdate_reboot_abort();
/*
- * kho_finalize() may return libfdt errors, to aboid passing to
+ * kho_finalize() may return libfdt errors, to avoid passing to
* userspace unknown errors, change this to EAGAIN.
*/
err = -EAGAIN;
@@ -243,6 +244,14 @@ int liveupdate_reboot(void)
return err;
}
+void liveupdate_reboot_abort(void)
+{
+ if (!liveupdate_enabled())
+ return;
+
+ luo_session_abort_reboot();
+}
+
/**
* liveupdate_enabled - Check if the live update feature is enabled.
*
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index e29e1a46b491..d6e9e85004b0 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -87,6 +87,7 @@ int luo_session_retrieve(const char *name, struct file **filep);
int __init luo_session_setup_outgoing(void *fdt);
int __init luo_session_setup_incoming(void *fdt);
int luo_session_serialize(void);
+void luo_session_abort_reboot(void);
int luo_session_deserialize(void);
bool luo_session_quiesce(void);
void luo_session_resume(void);
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 63aee9cc881d..cc03fc2bef97 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -294,6 +294,13 @@ static int luo_session_finish_retrieved(struct luo_session *session)
return luo_file_finish(&session->file_set);
}
+static void luo_session_unfreeze_one(struct luo_session *session,
+ struct luo_session_ser *ser)
+{
+ guard(mutex)(&session->mutex);
+ luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
+}
+
static int luo_session_release(struct inode *inodep, struct file *filep)
{
struct luo_session *session = filep->private_data;
@@ -797,6 +804,29 @@ int luo_session_serialize(void)
goto out_put_sessions;
}
+void luo_session_abort_reboot(void)
+{
+ struct luo_session_header *sh = &luo_session_global.outgoing;
+ struct luo_session *session;
+ int i = 0;
+
+ guard(rwsem_write)(&sh->rwsem);
+ if (!READ_ONCE(sh->rebooting))
+ return;
+
+ list_for_each_entry(session, &sh->list, list) {
+ if (i >= sh->header_ser->count)
+ break;
+
+ luo_session_unfreeze_one(session, &sh->ser[i]);
+ memset(&sh->ser[i], 0, sizeof(sh->ser[i]));
+ i++;
+ }
+
+ sh->header_ser->count = 0;
+ luo_session_reboot_done(sh);
+}
+
/**
* luo_session_quiesce - Ensure no active sessions exist and lock session lists.
*
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 3/5] liveupdate: fail session restore on file deserialization errors
2026-03-24 21:27 ` [PATCH v4 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
@ 2026-03-24 21:39 ` Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
0 siblings, 1 reply; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:39 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
Session restore can fail after inserting a partially constructed session
into the incoming list. The v2 cleanup also reused finish-style teardown
for that rollback path even though finish is the normal completion path
for a fully restored session.
That can leave stale incoming sessions behind after a deserialize error
and can run completion callbacks against state that was never fully
rebuilt.
Require an explicit abort callback for file handlers, use it to unwind
partially restored files, and route every deserialize failure through a
single cleanup path that drops restored sessions, frees serialized file
metadata and records the cached error code.
Fixes: 077fc48b59fc ("liveupdate: fail session restore on file deserialization errors")
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
include/linux/liveupdate.h | 3 ++
kernel/liveupdate/luo_file.c | 67 +++++++++++++++++++++++---------
kernel/liveupdate/luo_internal.h | 1 +
kernel/liveupdate/luo_session.c | 56 +++++++++++++++-----------
mm/memfd_luo.c | 24 +++++++-----
5 files changed, 100 insertions(+), 51 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index d93b043a0421..611907f57127 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -63,6 +63,8 @@ struct liveupdate_file_op_args {
* finish, in order to do successful finish calls for all
* resources in the session.
* @finish: Required. Final cleanup in the new kernel.
+ * @abort: Required. Discard preserved state in the new kernel without
+ * completing finish().
* @owner: Module reference
*
* All operations (except can_preserve) receive a pointer to a
@@ -78,6 +80,7 @@ struct liveupdate_file_ops {
int (*retrieve)(struct liveupdate_file_op_args *args);
bool (*can_finish)(struct liveupdate_file_op_args *args);
void (*finish)(struct liveupdate_file_op_args *args);
+ void (*abort)(struct liveupdate_file_op_args *args);
struct module *owner;
};
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 5acee4174bf0..939ef8d762ce 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -648,6 +648,20 @@ static void luo_file_finish_one(struct luo_file_set *file_set,
luo_flb_file_finish(luo_file->fh);
}
+static void luo_file_abort_one(struct luo_file *luo_file)
+{
+ struct liveupdate_file_op_args args = {0};
+
+ guard(mutex)(&luo_file->mutex);
+
+ args.handler = luo_file->fh;
+ args.file = luo_file->file;
+ args.serialized_data = luo_file->serialized_data;
+ args.retrieve_status = luo_file->retrieve_status;
+
+ luo_file->fh->ops->abort(&args);
+}
+
/**
* luo_file_finish - Completes the lifecycle for all files in a file_set.
* @file_set: The file_set to be finalized.
@@ -717,6 +731,28 @@ int luo_file_finish(struct luo_file_set *file_set)
return 0;
}
+void luo_file_abort_deserialized(struct luo_file_set *file_set)
+{
+ struct luo_file *luo_file;
+
+ while (!list_empty(&file_set->files_list)) {
+ luo_file = list_last_entry(&file_set->files_list,
+ struct luo_file, list);
+ luo_file_abort_one(luo_file);
+ if (luo_file->file)
+ fput(luo_file->file);
+ list_del(&luo_file->list);
+ file_set->count--;
+ mutex_destroy(&luo_file->mutex);
+ kfree(luo_file);
+ }
+
+ file_set->count = 0;
+ if (file_set->files)
+ kho_restore_free(file_set->files);
+ file_set->files = NULL;
+}
+
/**
* luo_file_deserialize - Reconstructs the list of preserved files in the new kernel.
* @file_set: The incoming file_set to fill with deserialized data.
@@ -747,6 +783,7 @@ int luo_file_deserialize(struct luo_file_set *file_set,
{
struct luo_file_ser *file_ser;
u64 i;
+ int err;
if (!file_set_ser->files) {
WARN_ON(file_set_ser->count);
@@ -756,21 +793,6 @@ int luo_file_deserialize(struct luo_file_set *file_set,
file_set->count = file_set_ser->count;
file_set->files = phys_to_virt(file_set_ser->files);
- /*
- * Note on error handling:
- *
- * If deserialization fails (e.g., allocation failure or corrupt data),
- * we intentionally skip cleanup of files that were already restored.
- *
- * A partial failure leaves the preserved state inconsistent.
- * Implementing a safe "undo" to unwind complex dependencies (sessions,
- * files, hardware state) is error-prone and provides little value, as
- * the system is effectively in a broken state.
- *
- * We treat these resources as leaked. The expected recovery path is for
- * userspace to detect the failure and trigger a reboot, which will
- * reliably reset devices and reclaim memory.
- */
file_ser = file_set->files;
for (i = 0; i < file_set->count; i++) {
struct liveupdate_file_handler *fh;
@@ -787,12 +809,15 @@ int luo_file_deserialize(struct luo_file_set *file_set,
if (!handler_found) {
pr_warn("No registered handler for compatible '%s'\n",
file_ser[i].compatible);
- return -ENOENT;
+ err = -ENOENT;
+ goto err_discard;
}
luo_file = kzalloc_obj(*luo_file);
- if (!luo_file)
- return -ENOMEM;
+ if (!luo_file) {
+ err = -ENOMEM;
+ goto err_discard;
+ }
luo_file->fh = fh;
luo_file->file = NULL;
@@ -803,6 +828,10 @@ int luo_file_deserialize(struct luo_file_set *file_set,
}
return 0;
+
+err_discard:
+ luo_file_abort_deserialized(file_set);
+ return err;
}
void luo_file_set_init(struct luo_file_set *file_set)
@@ -838,7 +867,7 @@ int liveupdate_register_file_handler(struct liveupdate_file_handler *fh)
/* Sanity check that all required callbacks are set */
if (!fh->ops->preserve || !fh->ops->unpreserve || !fh->ops->retrieve ||
- !fh->ops->finish || !fh->ops->can_preserve) {
+ !fh->ops->finish || !fh->ops->abort || !fh->ops->can_preserve) {
return -EINVAL;
}
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index d6e9e85004b0..94ca236cde21 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -101,6 +101,7 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
struct file **filep);
int luo_file_finish(struct luo_file_set *file_set);
+void luo_file_abort_deserialized(struct luo_file_set *file_set);
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
void luo_file_set_init(struct luo_file_set *file_set);
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index cc03fc2bef97..e35e53efb355 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -301,6 +301,20 @@ static void luo_session_unfreeze_one(struct luo_session *session,
luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
}
+static void luo_session_discard_deserialized(struct luo_session_header *sh)
+{
+ struct luo_session *session;
+
+ down_write(&sh->rwsem);
+ while (!list_empty(&sh->list)) {
+ session = list_last_entry(&sh->list, struct luo_session, list);
+ __luo_session_remove(sh, session);
+ luo_file_abort_deserialized(&session->file_set);
+ luo_session_put(session);
+ }
+ up_write(&sh->rwsem);
+}
+
static int luo_session_release(struct inode *inodep, struct file *filep)
{
struct luo_session *session = filep->private_data;
@@ -694,24 +708,10 @@ int luo_session_deserialize(void)
return err;
is_deserialized = true;
+ err = 0;
if (!sh->active)
- return 0;
+ return err;
- /*
- * Note on error handling:
- *
- * If deserialization fails (e.g., allocation failure or corrupt data),
- * we intentionally skip cleanup of sessions that were already restored.
- *
- * A partial failure leaves the preserved state inconsistent.
- * Implementing a safe "undo" to unwind complex dependencies (sessions,
- * files, hardware state) is error-prone and provides little value, as
- * the system is effectively in a broken state.
- *
- * We treat these resources as leaked. The expected recovery path is for
- * userspace to detect the failure and trigger a reboot, which will
- * reliably reset devices and reclaim memory.
- */
for (int i = 0; i < sh->header_ser->count; i++) {
struct luo_session *session;
@@ -719,7 +719,8 @@ int luo_session_deserialize(void)
if (IS_ERR(session)) {
pr_warn("Failed to allocate session [%s] during deserialization %pe\n",
sh->ser[i].name, session);
- return PTR_ERR(session);
+ err = PTR_ERR(session);
+ goto out_discard;
}
session->incoming = true;
@@ -727,21 +728,30 @@ int luo_session_deserialize(void)
if (err) {
pr_warn("Failed to insert session [%s] %pe\n",
session->name, ERR_PTR(err));
- luo_session_free(session);
- return err;
+ luo_session_put(session);
+ goto out_discard;
}
- scoped_guard(mutex, &session->mutex) {
- luo_file_deserialize(&session->file_set,
- &sh->ser[i].file_set_ser);
+ scoped_guard(mutex, &session->mutex)
+ err = luo_file_deserialize(&session->file_set,
+ &sh->ser[i].file_set_ser);
+ if (err) {
+ pr_warn("Failed to deserialize session [%s] files %pe\n",
+ session->name, ERR_PTR(err));
+ goto out_discard;
}
}
+out_free_header:
kho_restore_free(sh->header_ser);
sh->header_ser = NULL;
sh->ser = NULL;
- return 0;
+ return err;
+
+out_discard:
+ luo_session_discard_deserialized(sh);
+ goto out_free_header;
}
int luo_session_serialize(void)
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index b8edb9f981d7..8a453c8bfdf5 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -358,19 +358,11 @@ static void memfd_luo_discard_folios(const struct memfd_luo_folio_ser *folios_se
}
}
-static void memfd_luo_finish(struct liveupdate_file_op_args *args)
+static void memfd_luo_abort(struct liveupdate_file_op_args *args)
{
struct memfd_luo_folio_ser *folios_ser;
struct memfd_luo_ser *ser;
- /*
- * If retrieve was successful, nothing to do. If it failed, retrieve()
- * already cleaned up everything it could. So nothing to do there
- * either. Only need to clean up when retrieve was not called.
- */
- if (args->retrieve_status)
- return;
-
ser = phys_to_virt(args->serialized_data);
if (!ser)
return;
@@ -388,6 +380,19 @@ static void memfd_luo_finish(struct liveupdate_file_op_args *args)
kho_restore_free(ser);
}
+static void memfd_luo_finish(struct liveupdate_file_op_args *args)
+{
+ /*
+ * If retrieve was successful, nothing to do. If it failed, retrieve()
+ * already cleaned up everything it could. So nothing to do there
+ * either. Only need to clean up when retrieve was not called.
+ */
+ if (args->retrieve_status)
+ return;
+
+ memfd_luo_abort(args);
+}
+
static int memfd_luo_retrieve_folios(struct file *file,
struct memfd_luo_folio_ser *folios_ser,
u64 nr_folios)
@@ -532,6 +537,7 @@ static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
static const struct liveupdate_file_ops memfd_luo_file_ops = {
.freeze = memfd_luo_freeze,
.finish = memfd_luo_finish,
+ .abort = memfd_luo_abort,
.retrieve = memfd_luo_retrieve,
.preserve = memfd_luo_preserve,
.unpreserve = memfd_luo_unpreserve,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 4/5] liveupdate: validate handover metadata before using it
2026-03-24 21:39 ` [PATCH v4 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
@ 2026-03-24 21:39 ` Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 5/5] liveupdate: harden FLB lifetime and remaining teardown paths Oskar Gerlicz Kowalczuk
0 siblings, 1 reply; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:39 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
The incoming handover path was still trusting counts and preserved
addresses more than it should. Corrupted metadata could drive
phys_to_virt() or kho_restore_free() with invalid addresses, and memfd
restore could walk beyond the serialized folio array.
That turns malformed handover state into out-of-bounds iteration,
invalid restore frees, or partially restored incoming state.
Validate session, file and FLB metadata before consuming it, reject
non-restorable preserved addresses, and bound memfd folio restore by
both the serialized size and the restored vmalloc backing.
Fixes: 1c18e3e1efcd ("liveupdate: validate handover metadata before using it")
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
include/linux/kexec_handover.h | 5 +++
kernel/liveupdate/kexec_handover.c | 13 +++++++
kernel/liveupdate/luo_file.c | 22 +++++++++---
kernel/liveupdate/luo_flb.c | 33 ++++++++++++++++--
kernel/liveupdate/luo_session.c | 29 ++++++++++++++--
mm/memfd_luo.c | 56 ++++++++++++++++++++++++++++--
6 files changed, 147 insertions(+), 11 deletions(-)
diff --git a/include/linux/kexec_handover.h b/include/linux/kexec_handover.h
index ac4129d1d741..af8284a440bf 100644
--- a/include/linux/kexec_handover.h
+++ b/include/linux/kexec_handover.h
@@ -29,6 +29,7 @@ void kho_unpreserve_vmalloc(struct kho_vmalloc *preservation);
void *kho_alloc_preserve(size_t size);
void kho_unpreserve_free(void *mem);
void kho_restore_free(void *mem);
+bool kho_is_restorable_phys(phys_addr_t phys);
struct folio *kho_restore_folio(phys_addr_t phys);
struct page *kho_restore_pages(phys_addr_t phys, unsigned long nr_pages);
void *kho_restore_vmalloc(const struct kho_vmalloc *preservation);
@@ -80,6 +81,10 @@ static inline void *kho_alloc_preserve(size_t size)
static inline void kho_unpreserve_free(void *mem) { }
static inline void kho_restore_free(void *mem) { }
+static inline bool kho_is_restorable_phys(phys_addr_t phys)
+{
+ return false;
+}
static inline struct folio *kho_restore_folio(phys_addr_t phys)
{
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index cc68a3692905..215b27f5f85f 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -291,6 +291,19 @@ struct folio *kho_restore_folio(phys_addr_t phys)
}
EXPORT_SYMBOL_GPL(kho_restore_folio);
+bool kho_is_restorable_phys(phys_addr_t phys)
+{
+ struct page *page = pfn_to_online_page(PHYS_PFN(phys));
+ union kho_page_info info;
+
+ if (!page || !PAGE_ALIGNED(phys))
+ return false;
+
+ info.page_private = READ_ONCE(page->private);
+ return info.magic == KHO_PAGE_MAGIC && info.order <= MAX_PAGE_ORDER;
+}
+EXPORT_SYMBOL_GPL(kho_is_restorable_phys);
+
/**
* kho_restore_pages - restore list of contiguous order 0 pages.
* @phys: physical address of the first page.
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 939ef8d762ce..3eb9aeee6524 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -785,10 +785,17 @@ int luo_file_deserialize(struct luo_file_set *file_set,
u64 i;
int err;
- if (!file_set_ser->files) {
- WARN_ON(file_set_ser->count);
- return 0;
- }
+ if (!file_set_ser->count)
+ return file_set_ser->files ? -EINVAL : 0;
+
+ if (!file_set_ser->files)
+ return -EINVAL;
+
+ if (file_set_ser->count > LUO_FILE_MAX)
+ return -EINVAL;
+
+ if (!kho_is_restorable_phys(file_set_ser->files))
+ return -EINVAL;
file_set->count = file_set_ser->count;
file_set->files = phys_to_virt(file_set_ser->files);
@@ -799,6 +806,13 @@ int luo_file_deserialize(struct luo_file_set *file_set,
bool handler_found = false;
struct luo_file *luo_file;
+ if (strnlen(file_ser[i].compatible,
+ sizeof(file_ser[i].compatible)) ==
+ sizeof(file_ser[i].compatible)) {
+ err = -EINVAL;
+ goto err_discard;
+ }
+
list_private_for_each_entry(fh, &luo_file_handler_list, list) {
if (!strcmp(fh->compatible, file_ser[i].compatible)) {
handler_found = true;
diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c
index f52e8114837e..cdd293408138 100644
--- a/kernel/liveupdate/luo_flb.c
+++ b/kernel/liveupdate/luo_flb.c
@@ -165,7 +165,14 @@ static int luo_flb_retrieve_one(struct liveupdate_flb *flb)
return -ENODATA;
for (int i = 0; i < fh->header_ser->count; i++) {
+ if (strnlen(fh->ser[i].name, sizeof(fh->ser[i].name)) ==
+ sizeof(fh->ser[i].name))
+ return -EINVAL;
+
if (!strcmp(fh->ser[i].name, flb->compatible)) {
+ if (!fh->ser[i].count)
+ return -EINVAL;
+
private->incoming.data = fh->ser[i].data;
private->incoming.count = fh->ser[i].count;
found = true;
@@ -578,6 +585,7 @@ int __init luo_flb_setup_outgoing(void *fdt_out)
int __init luo_flb_setup_incoming(void *fdt_in)
{
+ struct luo_flb_header_ser *header_copy;
struct luo_flb_header_ser *header_ser;
int err, header_size, offset;
const void *ptr;
@@ -609,10 +617,31 @@ int __init luo_flb_setup_incoming(void *fdt_in)
}
header_ser_pa = get_unaligned((u64 *)ptr);
+ if (!header_ser_pa) {
+ pr_err("FLB header address is missing\n");
+ return -EINVAL;
+ }
+
+ if (!kho_is_restorable_phys(header_ser_pa))
+ return -EINVAL;
+
header_ser = phys_to_virt(header_ser_pa);
+ if (header_ser->pgcnt != LUO_FLB_PGCNT || header_ser->count > LUO_FLB_MAX) {
+ kho_restore_free(header_ser);
+ return -EINVAL;
+ }
+
+ header_copy = kmemdup(header_ser,
+ sizeof(*header_copy) +
+ header_ser->count *
+ sizeof(*luo_flb_global.incoming.ser),
+ GFP_KERNEL);
+ kho_restore_free(header_ser);
+ if (!header_copy)
+ return -ENOMEM;
- luo_flb_global.incoming.header_ser = header_ser;
- luo_flb_global.incoming.ser = (void *)(header_ser + 1);
+ luo_flb_global.incoming.header_ser = header_copy;
+ luo_flb_global.incoming.ser = (void *)(header_copy + 1);
luo_flb_global.incoming.active = true;
return 0;
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index e35e53efb355..0c9c82cd4ddc 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -688,6 +688,14 @@ int __init luo_session_setup_incoming(void *fdt_in)
}
header_ser_pa = get_unaligned((u64 *)ptr);
+ if (!header_ser_pa) {
+ pr_err("Session header address is missing\n");
+ return -EINVAL;
+ }
+
+ if (!kho_is_restorable_phys(header_ser_pa))
+ return -EINVAL;
+
header_ser = phys_to_virt(header_ser_pa);
luo_session_global.incoming.header_ser = header_ser;
@@ -712,9 +720,22 @@ int luo_session_deserialize(void)
if (!sh->active)
return err;
+ if (sh->header_ser->count > LUO_SESSION_MAX) {
+ pr_warn("Invalid session count %llu\n", sh->header_ser->count);
+ err = -EINVAL;
+ goto out_free_header;
+ }
+
for (int i = 0; i < sh->header_ser->count; i++) {
struct luo_session *session;
+ if (strnlen(sh->ser[i].name, sizeof(sh->ser[i].name)) ==
+ sizeof(sh->ser[i].name)) {
+ pr_warn("Session name is not NUL-terminated\n");
+ err = -EINVAL;
+ goto out_discard;
+ }
+
session = luo_session_alloc(sh->ser[i].name);
if (IS_ERR(session)) {
pr_warn("Failed to allocate session [%s] during deserialization %pe\n",
@@ -743,9 +764,11 @@ int luo_session_deserialize(void)
}
out_free_header:
- kho_restore_free(sh->header_ser);
- sh->header_ser = NULL;
- sh->ser = NULL;
+ if (sh->header_ser) {
+ kho_restore_free(sh->header_ser);
+ sh->header_ser = NULL;
+ sh->ser = NULL;
+ }
return err;
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 8a453c8bfdf5..b7f996176ad8 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -363,6 +363,10 @@ static void memfd_luo_abort(struct liveupdate_file_op_args *args)
struct memfd_luo_folio_ser *folios_ser;
struct memfd_luo_ser *ser;
+ if (!args->serialized_data ||
+ !kho_is_restorable_phys(args->serialized_data))
+ return;
+
ser = phys_to_virt(args->serialized_data);
if (!ser)
return;
@@ -399,10 +403,16 @@ static int memfd_luo_retrieve_folios(struct file *file,
{
struct inode *inode = file_inode(file);
struct address_space *mapping = inode->i_mapping;
+ u64 max_index = i_size_read(inode);
+ u64 prev_index = 0;
struct folio *folio;
+ long put_from = 0;
int err = -EIO;
long i;
+ if (max_index)
+ max_index = (max_index - 1) >> PAGE_SHIFT;
+
for (i = 0; i < nr_folios; i++) {
const struct memfd_luo_folio_ser *pfolio = &folios_ser[i];
phys_addr_t phys;
@@ -412,6 +422,19 @@ static int memfd_luo_retrieve_folios(struct file *file,
if (!pfolio->pfn)
continue;
+ put_from = i;
+ if (pfolio->flags &
+ ~(MEMFD_LUO_FOLIO_DIRTY | MEMFD_LUO_FOLIO_UPTODATE)) {
+ err = -EINVAL;
+ goto put_folios;
+ }
+
+ if (pfolio->index > max_index ||
+ (i && pfolio->index <= prev_index)) {
+ err = -EINVAL;
+ goto put_folios;
+ }
+
phys = PFN_PHYS(pfolio->pfn);
folio = kho_restore_folio(phys);
if (!folio) {
@@ -419,7 +442,9 @@ static int memfd_luo_retrieve_folios(struct file *file,
phys);
goto put_folios;
}
+ put_from = i + 1;
index = pfolio->index;
+ prev_index = index;
flags = pfolio->flags;
/* Set up the folio for insertion. */
@@ -469,10 +494,15 @@ static int memfd_luo_retrieve_folios(struct file *file,
* Note: don't free the folios already added to the file. They will be
* freed when the file is freed. Free the ones not added yet here.
*/
- for (long j = i + 1; j < nr_folios; j++) {
+ for (long j = put_from; j < nr_folios; j++) {
const struct memfd_luo_folio_ser *pfolio = &folios_ser[j];
+ phys_addr_t phys;
+
+ if (!pfolio->pfn)
+ continue;
- folio = kho_restore_folio(pfolio->pfn);
+ phys = PFN_PHYS(pfolio->pfn);
+ folio = kho_restore_folio(phys);
if (folio)
folio_put(folio);
}
@@ -487,10 +517,32 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
struct file *file;
int err;
+ if (!kho_is_restorable_phys(args->serialized_data))
+ return -EINVAL;
+
ser = phys_to_virt(args->serialized_data);
if (!ser)
return -EINVAL;
+ if (!!ser->nr_folios != !!ser->folios.first.phys) {
+ err = -EINVAL;
+ goto free_ser;
+ }
+
+ if (ser->nr_folios >
+ (((u64)ser->folios.total_pages << PAGE_SHIFT) /
+ sizeof(*folios_ser))) {
+ err = -EINVAL;
+ goto free_ser;
+ }
+
+ if (ser->nr_folios &&
+ (!ser->size ||
+ ser->nr_folios > ((ser->size - 1) >> PAGE_SHIFT) + 1)) {
+ err = -EINVAL;
+ goto free_ser;
+ }
+
file = memfd_alloc_file("", 0);
if (IS_ERR(file)) {
pr_err("failed to setup file: %pe\n", file);
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 5/5] liveupdate: harden FLB lifetime and remaining teardown paths
2026-03-24 21:39 ` [PATCH v4 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
@ 2026-03-24 21:39 ` Oskar Gerlicz Kowalczuk
0 siblings, 0 replies; 11+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-24 21:39 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
The earlier cleanup patches fix the primary deserialize rollback paths, but
some teardown and lifetime edges are still open.
Incoming FLB objects can still race with finish(), copied incoming FLB
metadata can be torn down while another thread scans it, and the remaining
raw serialized file cleanup still needs a bounded, handler-aware abort path.
The quiesce path also has to reject the /dev/liveupdate open window, not
just non-empty session lists.
Tighten the remaining teardown paths by:
- making incoming FLB access hold the incoming lock until the caller drops
it
- discarding copied incoming FLB metadata once the last incoming session is
gone or rollback aborts it
- adding bounded raw serialized file cleanup and a known-format memfd abort
helper
- teaching KHO about the size of a restorable block so cleanup and retrieve
paths can validate preserved extents before walking them
- rejecting handler registration changes while /dev/liveupdate is busy
This leaves the series on the simpler rebooting + mutex + refcount model,
while closing the remaining lifetime and cleanup holes around incoming
teardown.
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
include/linux/kexec_handover.h | 8 ++
include/linux/liveupdate.h | 9 +-
kernel/kexec_core.c | 2 +-
kernel/liveupdate/kexec_handover.c | 15 ++-
kernel/liveupdate/luo_core.c | 5 +
kernel/liveupdate/luo_file.c | 156 ++++++++++++++++++++++++++---
kernel/liveupdate/luo_flb.c | 87 +++++++++++-----
kernel/liveupdate/luo_internal.h | 3 +
kernel/liveupdate/luo_session.c | 127 ++++++++++++++++++-----
lib/tests/liveupdate.c | 2 +
mm/memfd_luo.c | 128 ++++++++++++++++-------
11 files changed, 437 insertions(+), 105 deletions(-)
diff --git a/include/linux/kexec_handover.h b/include/linux/kexec_handover.h
index af8284a440bf..1ceca20a4dc4 100644
--- a/include/linux/kexec_handover.h
+++ b/include/linux/kexec_handover.h
@@ -2,6 +2,7 @@
#ifndef LINUX_KEXEC_HANDOVER_H
#define LINUX_KEXEC_HANDOVER_H
+#include <linux/bug.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/types.h>
@@ -30,6 +31,7 @@ void *kho_alloc_preserve(size_t size);
void kho_unpreserve_free(void *mem);
void kho_restore_free(void *mem);
bool kho_is_restorable_phys(phys_addr_t phys);
+size_t kho_restorable_size(phys_addr_t phys);
struct folio *kho_restore_folio(phys_addr_t phys);
struct page *kho_restore_pages(phys_addr_t phys, unsigned long nr_pages);
void *kho_restore_vmalloc(const struct kho_vmalloc *preservation);
@@ -85,6 +87,12 @@ static inline bool kho_is_restorable_phys(phys_addr_t phys)
{
return false;
}
+static inline size_t kho_restorable_size(phys_addr_t phys)
+{
+ if (phys)
+ WARN_ON_ONCE(!IS_ENABLED(CONFIG_KEXEC_HANDOVER));
+ return 0;
+}
static inline struct folio *kho_restore_folio(phys_addr_t phys)
{
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 611907f57127..b278f7efc3c4 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -239,7 +239,10 @@ int liveupdate_register_flb(struct liveupdate_file_handler *fh,
int liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb);
-int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp);
+int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp)
+ __acquires(&ACCESS_PRIVATE(flb, private).incoming.lock);
+void liveupdate_flb_put_incoming(struct liveupdate_flb *flb)
+ __releases(&ACCESS_PRIVATE(flb, private).incoming.lock);
int liveupdate_flb_get_outgoing(struct liveupdate_flb *flb, void **objp);
#else /* CONFIG_LIVEUPDATE */
@@ -286,6 +289,10 @@ static inline int liveupdate_flb_get_incoming(struct liveupdate_flb *flb,
return -EOPNOTSUPP;
}
+static inline void liveupdate_flb_put_incoming(struct liveupdate_flb *flb)
+{
+}
+
static inline int liveupdate_flb_get_outgoing(struct liveupdate_flb *flb,
void **objp)
{
diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c
index 95710a7d5e56..2276ef1f8f7a 100644
--- a/kernel/kexec_core.c
+++ b/kernel/kexec_core.c
@@ -1232,7 +1232,7 @@ int kernel_kexec(void)
}
#endif
-Unlock:
+ Unlock:
if (liveupdate_prepared)
liveupdate_reboot_abort();
kexec_unlock();
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index 215b27f5f85f..105a4816320d 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -291,16 +291,25 @@ struct folio *kho_restore_folio(phys_addr_t phys)
}
EXPORT_SYMBOL_GPL(kho_restore_folio);
-bool kho_is_restorable_phys(phys_addr_t phys)
+size_t kho_restorable_size(phys_addr_t phys)
{
struct page *page = pfn_to_online_page(PHYS_PFN(phys));
union kho_page_info info;
if (!page || !PAGE_ALIGNED(phys))
- return false;
+ return 0;
info.page_private = READ_ONCE(page->private);
- return info.magic == KHO_PAGE_MAGIC && info.order <= MAX_PAGE_ORDER;
+ if (info.magic != KHO_PAGE_MAGIC || info.order > MAX_PAGE_ORDER)
+ return 0;
+
+ return PAGE_SIZE << info.order;
+}
+EXPORT_SYMBOL_GPL(kho_restorable_size);
+
+bool kho_is_restorable_phys(phys_addr_t phys)
+{
+ return kho_restorable_size(phys) != 0;
}
EXPORT_SYMBOL_GPL(kho_is_restorable_phys);
diff --git a/kernel/liveupdate/luo_core.c b/kernel/liveupdate/luo_core.c
index 95a0b81ce60d..f4a045687fe8 100644
--- a/kernel/liveupdate/luo_core.c
+++ b/kernel/liveupdate/luo_core.c
@@ -451,6 +451,11 @@ static struct luo_device_state luo_dev = {
.in_use = ATOMIC_INIT(0),
};
+bool luo_device_busy(void)
+{
+ return atomic_read(&luo_dev.in_use);
+}
+
static int __init liveupdate_ioctl_init(void)
{
if (!liveupdate_enabled())
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 3eb9aeee6524..83b263590541 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -112,6 +112,17 @@
#include <linux/string.h>
#include "luo_internal.h"
+#if IS_ENABLED(CONFIG_LIVEUPDATE_MEMFD)
+extern bool luo_file_abort_known_serialized(const char *compatible,
+ u64 serialized_data);
+#else
+static inline bool luo_file_abort_known_serialized(const char *compatible,
+ u64 serialized_data)
+{
+ return false;
+}
+#endif
+
static LIST_HEAD(luo_file_handler_list);
/* 2 4K pages, give space for 128 files per file_set */
@@ -170,6 +181,19 @@ struct luo_file {
u64 token;
};
+static struct liveupdate_file_handler *
+luo_file_find_handler(const char *compatible)
+{
+ struct liveupdate_file_handler *fh;
+
+ list_private_for_each_entry(fh, &luo_file_handler_list, list) {
+ if (!strcmp(fh->compatible, compatible))
+ return fh;
+ }
+
+ return NULL;
+}
+
static int luo_alloc_files_mem(struct luo_file_set *file_set)
{
size_t size;
@@ -660,6 +684,60 @@ static void luo_file_abort_one(struct luo_file *luo_file)
args.retrieve_status = luo_file->retrieve_status;
luo_file->fh->ops->abort(&args);
+ luo_flb_file_finish(luo_file->fh);
+}
+
+static void luo_file_abort_serialized_one(struct liveupdate_file_handler *fh,
+ const struct luo_file_ser *file_ser)
+{
+ struct liveupdate_file_op_args args = {0};
+
+ args.handler = fh;
+ args.serialized_data = file_ser->data;
+ fh->ops->abort(&args);
+ luo_flb_file_finish(fh);
+}
+
+static void luo_file_abort_unhandled_serialized(const struct luo_file_ser *file_ser)
+{
+ /*
+ * serialized_data is opaque outside the owning handler. Only use a
+ * handler-specific fallback when the serialized entry still carries a
+ * known type tag.
+ */
+ if (luo_file_abort_known_serialized(file_ser->compatible,
+ file_ser->data))
+ return;
+
+ pr_warn("Leaving serialized file payload for unhandled type '%s'\n",
+ file_ser->compatible);
+}
+
+static void luo_file_abort_serialized_range(struct luo_file_ser *file_ser,
+ u64 start, u64 count)
+{
+ u64 i;
+
+ for (i = start; i < count; i++) {
+ struct liveupdate_file_handler *fh;
+
+ if (strnlen(file_ser[i].compatible,
+ sizeof(file_ser[i].compatible)) ==
+ sizeof(file_ser[i].compatible)) {
+ pr_warn("Skipping unterminated serialized file handler name during abort\n");
+ continue;
+ }
+
+ fh = luo_file_find_handler(file_ser[i].compatible);
+ if (!fh) {
+ pr_warn("Skipping serialized file with missing handler '%s' during abort\n",
+ file_ser[i].compatible);
+ luo_file_abort_unhandled_serialized(&file_ser[i]);
+ continue;
+ }
+
+ luo_file_abort_serialized_one(fh, &file_ser[i]);
+ }
}
/**
@@ -753,6 +831,46 @@ void luo_file_abort_deserialized(struct luo_file_set *file_set)
file_set->files = NULL;
}
+void luo_file_abort_serialized(const struct luo_file_set_ser *file_set_ser)
+{
+ struct luo_file_ser *file_ser;
+ size_t bytes;
+ u64 count;
+
+ if (!file_set_ser->count) {
+ if (file_set_ser->files)
+ pr_warn("Ignoring serialized file pointer in empty file set\n");
+ /*
+ * The pointer is malformed relative to count, so ownership is
+ * unclear here. Leave it untouched rather than risk freeing a
+ * block which is still owned elsewhere.
+ */
+ return;
+ }
+
+ if (!file_set_ser->files || !kho_is_restorable_phys(file_set_ser->files)) {
+ pr_warn("Skipping invalid serialized file set pointer during abort\n");
+ return;
+ }
+
+ bytes = kho_restorable_size(file_set_ser->files);
+ file_ser = phys_to_virt(file_set_ser->files);
+ if (bytes < sizeof(*file_ser)) {
+ pr_warn("Serialized file set block is too small for cleanup\n");
+ kho_restore_free(file_ser);
+ return;
+ }
+
+ count = min_t(u64, file_set_ser->count, LUO_FILE_MAX);
+ count = min_t(u64, count, bytes / sizeof(*file_ser));
+ if (count != file_set_ser->count)
+ pr_warn("Clamping serialized file abort from %llu to %llu entries\n",
+ file_set_ser->count, count);
+
+ luo_file_abort_serialized_range(file_ser, 0, count);
+ kho_restore_free(file_ser);
+}
+
/**
* luo_file_deserialize - Reconstructs the list of preserved files in the new kernel.
* @file_set: The incoming file_set to fill with deserialized data.
@@ -782,6 +900,8 @@ int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser)
{
struct luo_file_ser *file_ser;
+ size_t bytes;
+ u64 count;
u64 i;
int err;
@@ -797,13 +917,22 @@ int luo_file_deserialize(struct luo_file_set *file_set,
if (!kho_is_restorable_phys(file_set_ser->files))
return -EINVAL;
- file_set->count = file_set_ser->count;
+ bytes = kho_restorable_size(file_set_ser->files);
+ if (file_set_ser->count > bytes / sizeof(*file_ser))
+ return -EINVAL;
+
+ count = file_set_ser->count;
+ file_set->count = 0;
+ /*
+ * file_set owns the top-level serialized array from this point on. The
+ * success path frees it via luo_file_finish(), and any deserialize
+ * failure frees it via luo_file_abort_deserialized().
+ */
file_set->files = phys_to_virt(file_set_ser->files);
file_ser = file_set->files;
- for (i = 0; i < file_set->count; i++) {
+ for (i = 0; i < count; i++) {
struct liveupdate_file_handler *fh;
- bool handler_found = false;
struct luo_file *luo_file;
if (strnlen(file_ser[i].compatible,
@@ -813,14 +942,8 @@ int luo_file_deserialize(struct luo_file_set *file_set,
goto err_discard;
}
- list_private_for_each_entry(fh, &luo_file_handler_list, list) {
- if (!strcmp(fh->compatible, file_ser[i].compatible)) {
- handler_found = true;
- break;
- }
- }
-
- if (!handler_found) {
+ fh = luo_file_find_handler(file_ser[i].compatible);
+ if (!fh) {
pr_warn("No registered handler for compatible '%s'\n",
file_ser[i].compatible);
err = -ENOENT;
@@ -839,11 +962,20 @@ int luo_file_deserialize(struct luo_file_set *file_set,
luo_file->token = file_ser[i].token;
mutex_init(&luo_file->mutex);
list_add_tail(&luo_file->list, &file_set->files_list);
+ file_set->count++;
}
return 0;
-
err_discard:
+ /*
+ * Entries already materialized into file_set are owned by the
+ * deserialized objects. Abort only the raw tail here and then let
+ * deserialized cleanup drop the constructed prefix and free the
+ * top-level serialized array owned by file_set.
+ */
+ luo_file_abort_serialized_range(file_ser, file_set->count, count);
+ file_set_ser->files = 0;
+ file_set_ser->count = 0;
luo_file_abort_deserialized(file_set);
return err;
}
diff --git a/kernel/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c
index cdd293408138..b3b6344b1680 100644
--- a/kernel/liveupdate/luo_flb.c
+++ b/kernel/liveupdate/luo_flb.c
@@ -128,6 +128,9 @@ static void luo_flb_file_unpreserve_one(struct liveupdate_flb *flb)
struct luo_flb_private *private = luo_flb_get_private(flb);
scoped_guard(mutex, &private->outgoing.lock) {
+ if (WARN_ON_ONCE(!private->outgoing.count))
+ return;
+
private->outgoing.count--;
if (!private->outgoing.count) {
struct liveupdate_flb_op_args args = {0};
@@ -191,7 +194,7 @@ static int luo_flb_retrieve_one(struct liveupdate_flb *flb)
return err;
private->incoming.obj = args.obj;
- private->incoming.retrieved = true;
+ WRITE_ONCE(private->incoming.retrieved, true);
return 0;
}
@@ -199,30 +202,38 @@ static int luo_flb_retrieve_one(struct liveupdate_flb *flb)
static void luo_flb_file_finish_one(struct liveupdate_flb *flb)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
- u64 count;
-
- scoped_guard(mutex, &private->incoming.lock)
- count = --private->incoming.count;
-
- if (!count) {
+ for (;;) {
struct liveupdate_flb_op_args args = {0};
+ bool need_retrieve = false;
+ u64 count;
- if (!private->incoming.retrieved) {
- int err = luo_flb_retrieve_one(flb);
-
- if (WARN_ON(err))
+ scoped_guard(mutex, &private->incoming.lock) {
+ if (READ_ONCE(private->incoming.finished))
return;
+
+ if (!private->incoming.count) {
+ need_retrieve = !READ_ONCE(private->incoming.retrieved);
+ count = 1;
+ } else {
+ count = --private->incoming.count;
+ if (!count) {
+ args.flb = flb;
+ args.obj = private->incoming.obj;
+ flb->ops->finish(&args);
+
+ private->incoming.data = 0;
+ private->incoming.obj = NULL;
+ WRITE_ONCE(private->incoming.retrieved, false);
+ WRITE_ONCE(private->incoming.finished, true);
+ }
+ }
}
- scoped_guard(mutex, &private->incoming.lock) {
- args.flb = flb;
- args.obj = private->incoming.obj;
- flb->ops->finish(&args);
+ if (!need_retrieve)
+ return;
- private->incoming.data = 0;
- private->incoming.obj = NULL;
- private->incoming.finished = true;
- }
+ if (WARN_ON(luo_flb_retrieve_one(flb)))
+ return;
}
}
@@ -303,6 +314,16 @@ void luo_flb_file_finish(struct liveupdate_file_handler *fh)
luo_flb_file_finish_one(iter->flb);
}
+void luo_flb_discard_incoming(void)
+{
+ struct luo_flb_header *fh = &luo_flb_global.incoming;
+
+ kfree(fh->header_ser);
+ fh->header_ser = NULL;
+ fh->ser = NULL;
+ fh->active = false;
+}
+
/**
* 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.
@@ -502,25 +523,38 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
* data, and -EOPNOTSUPP when live update is disabled or not configured.
*/
int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp)
+ __acquires(&ACCESS_PRIVATE(flb, private).incoming.lock)
{
struct luo_flb_private *private = luo_flb_get_private(flb);
if (!liveupdate_enabled())
return -EOPNOTSUPP;
- if (!private->incoming.obj) {
+ if (!READ_ONCE(private->incoming.obj)) {
int err = luo_flb_retrieve_one(flb);
if (err)
return err;
}
- guard(mutex)(&private->incoming.lock);
+ mutex_lock(&private->incoming.lock);
+ if (!private->incoming.obj) {
+ mutex_unlock(&private->incoming.lock);
+ return -ENODATA;
+ }
*objp = private->incoming.obj;
return 0;
}
+void liveupdate_flb_put_incoming(struct liveupdate_flb *flb)
+ __releases(&ACCESS_PRIVATE(flb, private).incoming.lock)
+{
+ struct luo_flb_private *private = luo_flb_get_private(flb);
+
+ mutex_unlock(&private->incoming.lock);
+}
+
/**
* liveupdate_flb_get_outgoing - Retrieve the outgoing FLB object.
* @flb: The FLB definition.
@@ -631,10 +665,17 @@ int __init luo_flb_setup_incoming(void *fdt_in)
return -EINVAL;
}
+ if (kho_restorable_size(header_ser_pa) < sizeof(*header_ser) ||
+ sizeof(*header_ser) +
+ header_ser->count * sizeof(*luo_flb_global.incoming.ser) >
+ kho_restorable_size(header_ser_pa)) {
+ kho_restore_free(header_ser);
+ return -EINVAL;
+ }
+
header_copy = kmemdup(header_ser,
sizeof(*header_copy) +
- header_ser->count *
- sizeof(*luo_flb_global.incoming.ser),
+ header_ser->count * sizeof(*luo_flb_global.incoming.ser),
GFP_KERNEL);
kho_restore_free(header_ser);
if (!header_copy)
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index 94ca236cde21..f95b23b5bfdd 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -91,6 +91,7 @@ void luo_session_abort_reboot(void);
int luo_session_deserialize(void);
bool luo_session_quiesce(void);
void luo_session_resume(void);
+bool luo_device_busy(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);
@@ -102,6 +103,7 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
struct file **filep);
int luo_file_finish(struct luo_file_set *file_set);
void luo_file_abort_deserialized(struct luo_file_set *file_set);
+void luo_file_abort_serialized(const struct luo_file_set_ser *file_set_ser);
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
void luo_file_set_init(struct luo_file_set *file_set);
@@ -112,6 +114,7 @@ void luo_flb_file_unpreserve(struct liveupdate_file_handler *fh);
void luo_flb_file_finish(struct liveupdate_file_handler *fh);
int __init luo_flb_setup_outgoing(void *fdt);
int __init luo_flb_setup_incoming(void *fdt);
+void luo_flb_discard_incoming(void);
void luo_flb_serialize(void);
#ifdef CONFIG_LIVEUPDATE_TEST
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 0c9c82cd4ddc..741427d5f9e5 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -51,6 +51,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/anon_inodes.h>
+#include <linux/atomic.h>
#include <linux/cleanup.h>
#include <linux/err.h>
#include <linux/errno.h>
@@ -144,6 +145,7 @@ static struct luo_session *luo_session_alloc(const char *name)
INIT_LIST_HEAD(&session->file_set.files_list);
luo_file_set_init(&session->file_set);
INIT_LIST_HEAD(&session->list);
+ /* The caller transfers this reference to list membership on insert. */
refcount_set(&session->refs, 1);
mutex_init(&session->mutex);
@@ -168,6 +170,15 @@ static void luo_session_put(struct luo_session *session)
luo_session_free(session);
}
+static int luo_session_finish_retrieved(struct luo_session *session)
+{
+ lockdep_assert_held(&session->mutex);
+ if (!session->retrieved)
+ return -EINVAL;
+
+ return luo_file_finish(&session->file_set);
+}
+
static int luo_session_insert(struct luo_session_header *sh,
struct luo_session *session)
{
@@ -285,25 +296,11 @@ static void luo_session_wait_reboot(struct luo_session_header *sh)
finish_wait(&sh->reboot_waitq, &wait);
}
-static int luo_session_finish_retrieved(struct luo_session *session)
-{
- lockdep_assert_held(&session->mutex);
- if (!session->retrieved)
- return -EINVAL;
-
- return luo_file_finish(&session->file_set);
-}
-
-static void luo_session_unfreeze_one(struct luo_session *session,
- struct luo_session_ser *ser)
-{
- guard(mutex)(&session->mutex);
- luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
-}
-
-static void luo_session_discard_deserialized(struct luo_session_header *sh)
+static void luo_session_discard_deserialized(struct luo_session_header *sh,
+ u64 abort_from)
{
struct luo_session *session;
+ u64 i;
down_write(&sh->rwsem);
while (!list_empty(&sh->list)) {
@@ -313,6 +310,18 @@ static void luo_session_discard_deserialized(struct luo_session_header *sh)
luo_session_put(session);
}
up_write(&sh->rwsem);
+
+ for (i = abort_from; i < sh->header_ser->count; i++)
+ luo_file_abort_serialized(&sh->ser[i].file_set_ser);
+
+ luo_flb_discard_incoming();
+}
+
+static void luo_session_unfreeze_one(struct luo_session *session,
+ struct luo_session_ser *ser)
+{
+ guard(mutex)(&session->mutex);
+ luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
}
static int luo_session_release(struct inode *inodep, struct file *filep)
@@ -320,6 +329,7 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
struct luo_session *session = filep->private_data;
struct luo_session_header *sh = &luo_session_global.incoming;
int ret = 0;
+ bool discard_flb = false;
bool removed = false;
if (session->incoming) {
@@ -327,13 +337,26 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
if (session->retrieved) {
ret = luo_session_finish_retrieved(session);
if (!ret) {
+ /*
+ * Make the session undiscoverable before
+ * publishing it as not retrieved again,
+ * otherwise luo_session_retrieve() can
+ * reopen it through the incoming list
+ * while close() is consuming it.
+ */
down_write(&sh->rwsem);
if (!list_empty(&session->list)) {
__luo_session_remove(sh, session);
+ discard_flb = !sh->count;
removed = true;
}
up_write(&sh->rwsem);
}
+ /*
+ * If close() is the implicit finish path, clear
+ * retrieved even on failure so the session can
+ * be retrieved again after this file is gone.
+ */
WRITE_ONCE(session->retrieved, false);
}
}
@@ -341,12 +364,15 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
if (ret) {
pr_warn("Unable to finish session [%s] on release\n",
session->name);
+ /* Drop the anon_inode_getfile() reference. */
luo_session_put(session);
return ret;
}
if (removed)
luo_session_put(session);
+ if (discard_flb)
+ luo_flb_discard_incoming();
luo_session_put(session);
return 0;
}
@@ -366,6 +392,11 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
break;
}
+ /*
+ * Once close() passes the rebooting gate it holds session->mutex, so
+ * serialization will either wait for teardown to finish or observe the
+ * session removed from the outgoing list.
+ */
luo_file_unpreserve_files(&session->file_set);
down_write(&sh->rwsem);
@@ -373,9 +404,11 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
up_write(&sh->rwsem);
mutex_unlock(&session->mutex);
+ /* Drop the list reference and the anon inode file reference. */
luo_session_put(session);
luo_session_put(session);
- return 0;
+
+ return ret;
}
static int luo_session_preserve_fd(struct luo_session *session,
@@ -444,15 +477,28 @@ static int luo_session_finish(struct luo_session *session,
{
struct liveupdate_session_finish *argp = ucmd->cmd;
struct luo_session_header *sh = &luo_session_global.incoming;
+ bool discard_flb = false;
bool removed = false;
int err;
+ /*
+ * FINISH consumes a retrieved incoming session. After a successful
+ * finish it is removed from the incoming list; release() then only
+ * drops the remaining file reference.
+ */
scoped_guard(mutex, &session->mutex) {
err = luo_session_finish_retrieved(session);
if (!err) {
+ /*
+ * Remove the session from the incoming list before it
+ * becomes observable as not retrieved. Otherwise a
+ * concurrent retrieve-by-name can take a new file
+ * reference to a session that FINISH is consuming.
+ */
down_write(&sh->rwsem);
if (!list_empty(&session->list)) {
__luo_session_remove(sh, session);
+ discard_flb = !sh->count;
removed = true;
}
up_write(&sh->rwsem);
@@ -464,6 +510,8 @@ static int luo_session_finish(struct luo_session *session,
if (removed)
luo_session_put(session);
+ if (discard_flb)
+ luo_flb_discard_incoming();
return luo_ucmd_respond(ucmd, sizeof(*argp));
}
@@ -550,6 +598,7 @@ static int luo_session_getfile(struct luo_session *session, struct file **filep)
lockdep_assert_held(&session->mutex);
snprintf(name_buf, sizeof(name_buf), "[luo_session] %s", session->name);
+ /* anon_inode_getfile() keeps the session alive until .release(). */
luo_session_get(session);
file = anon_inode_getfile(name_buf, &luo_session_fops, session, O_RDWR);
if (IS_ERR(file)) {
@@ -583,6 +632,11 @@ int luo_session_create(const char *name, struct file **filep)
return 0;
err_remove:
+ /*
+ * Serializer synchronizes outgoing session visibility with
+ * session->mutex. Keep the same lock while removing a create() failure
+ * so it cannot freeze a session whose file was never published.
+ */
scoped_guard(mutex, &session->mutex)
luo_session_remove(&luo_session_global.outgoing, session);
err_free:
@@ -617,9 +671,9 @@ int luo_session_retrieve(const char *name, struct file **filep)
err = -EINVAL;
else
err = luo_session_getfile(session, filep);
-
if (!err)
WRITE_ONCE(session->retrieved, true);
+
luo_session_put(session);
return err;
@@ -708,8 +762,10 @@ int __init luo_session_setup_incoming(void *fdt_in)
int luo_session_deserialize(void)
{
struct luo_session_header *sh = &luo_session_global.incoming;
+ size_t bytes;
static bool is_deserialized;
static int err;
+ u64 abort_from = 0;
/* If has been deserialized, always return the same error code */
if (is_deserialized)
@@ -726,9 +782,21 @@ int luo_session_deserialize(void)
goto out_free_header;
}
+ bytes = kho_restorable_size(virt_to_phys(sh->header_ser));
+ if (bytes < sizeof(*sh->header_ser) ||
+ sh->header_ser->count >
+ (bytes - sizeof(*sh->header_ser)) / sizeof(*sh->ser)) {
+ pr_warn("Serialized session block is too small for %llu sessions\n",
+ sh->header_ser->count);
+ err = -EINVAL;
+ goto out_free_header;
+ }
+
for (int i = 0; i < sh->header_ser->count; i++) {
struct luo_session *session;
+ abort_from = i;
+
if (strnlen(sh->ser[i].name, sizeof(sh->ser[i].name)) ==
sizeof(sh->ser[i].name)) {
pr_warn("Session name is not NUL-terminated\n");
@@ -743,15 +811,15 @@ int luo_session_deserialize(void)
err = PTR_ERR(session);
goto out_discard;
}
- session->incoming = true;
+ session->incoming = true;
err = luo_session_insert(sh, session);
if (err) {
- pr_warn("Failed to insert session [%s] %pe\n",
- session->name, ERR_PTR(err));
- luo_session_put(session);
- goto out_discard;
- }
+ pr_warn("Failed to insert session [%s] %pe\n",
+ session->name, ERR_PTR(err));
+ luo_session_put(session);
+ goto out_discard;
+ }
scoped_guard(mutex, &session->mutex)
err = luo_file_deserialize(&session->file_set,
@@ -759,6 +827,7 @@ int luo_session_deserialize(void)
if (err) {
pr_warn("Failed to deserialize session [%s] files %pe\n",
session->name, ERR_PTR(err));
+ abort_from = i;
goto out_discard;
}
}
@@ -773,7 +842,7 @@ int luo_session_deserialize(void)
return err;
out_discard:
- luo_session_discard_deserialized(sh);
+ luo_session_discard_deserialized(sh, abort_from);
goto out_free_header;
}
@@ -833,6 +902,7 @@ int luo_session_serialize(void)
}
}
sh->header_ser->count = 0;
+ /* Reset rebooting flag on serialization failure. */
luo_session_reboot_done(sh);
goto out_put_sessions;
}
@@ -879,10 +949,13 @@ void luo_session_abort_reboot(void)
*/
bool luo_session_quiesce(void)
{
+ if (luo_device_busy())
+ return false;
+
down_write(&luo_session_global.incoming.rwsem);
down_write(&luo_session_global.outgoing.rwsem);
- if (luo_session_global.incoming.count ||
+ if (luo_device_busy() || luo_session_global.incoming.count ||
luo_session_global.outgoing.count) {
up_write(&luo_session_global.outgoing.rwsem);
up_write(&luo_session_global.incoming.rwsem);
diff --git a/lib/tests/liveupdate.c b/lib/tests/liveupdate.c
index 496d6ef91a30..87d9302462d6 100644
--- a/lib/tests/liveupdate.c
+++ b/lib/tests/liveupdate.c
@@ -104,6 +104,8 @@ static void liveupdate_test_init(void)
if (err && err != -ENODATA && err != -ENOENT) {
pr_err("liveupdate_flb_get_incoming for %s failed: %pe\n",
flb->compatible, ERR_PTR(err));
+ } else if (!err) {
+ liveupdate_flb_put_incoming(flb);
}
}
initialized = true;
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index b7f996176ad8..ef69e4935ada 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -77,10 +77,14 @@
#include <linux/kho/abi/memfd.h>
#include <linux/liveupdate.h>
#include <linux/shmem_fs.h>
+#include <linux/string.h>
#include <linux/vmalloc.h>
#include <linux/memfd.h>
#include "internal.h"
+bool luo_file_abort_known_serialized(const char *compatible,
+ u64 serialized_data);
+
static int memfd_luo_preserve_folios(struct file *file,
struct kho_vmalloc *kho_vmalloc,
struct memfd_luo_folio_ser **out_folios_ser,
@@ -358,32 +362,90 @@ static void memfd_luo_discard_folios(const struct memfd_luo_folio_ser *folios_se
}
}
-static void memfd_luo_abort(struct liveupdate_file_op_args *args)
+static u64 memfd_luo_folios_capacity(const struct memfd_luo_ser *ser)
{
- struct memfd_luo_folio_ser *folios_ser;
- struct memfd_luo_ser *ser;
+ return ((u64)ser->folios.total_pages << PAGE_SHIFT) /
+ sizeof(struct memfd_luo_folio_ser);
+}
- if (!args->serialized_data ||
- !kho_is_restorable_phys(args->serialized_data))
- return;
+static bool memfd_luo_folio_desc_valid(const struct memfd_luo_ser *ser)
+{
+ if (!!ser->nr_folios != !!ser->folios.first.phys)
+ return false;
- ser = phys_to_virt(args->serialized_data);
- if (!ser)
- return;
+ if (!ser->nr_folios)
+ return true;
- if (ser->nr_folios) {
- folios_ser = kho_restore_vmalloc(&ser->folios);
- if (!folios_ser)
- goto out;
+ return ser->nr_folios <= memfd_luo_folios_capacity(ser);
+}
- memfd_luo_discard_folios(folios_ser, ser->nr_folios);
- vfree(folios_ser);
+static bool memfd_luo_serialized_valid(const struct memfd_luo_ser *ser)
+{
+ if (ser->size > MAX_LFS_FILESIZE)
+ return false;
+
+ if (!memfd_luo_folio_desc_valid(ser))
+ return false;
+
+ /*
+ * Invariant: memfd without serialized folios must have size == 0.
+ * Preserve serializes page-backed content for every page in a non-empty
+ * memfd, including holes.
+ */
+ if (!ser->nr_folios)
+ return !ser->size;
+
+ return ser->nr_folios <= ((ser->size - 1) >> PAGE_SHIFT) + 1;
+}
+
+static void memfd_luo_discard_serialized(struct memfd_luo_ser *ser)
+{
+ struct memfd_luo_folio_ser *folios_ser;
+
+ if (memfd_luo_folio_desc_valid(ser) && ser->folios.first.phys) {
+ folios_ser = kho_restore_vmalloc(&ser->folios);
+ if (folios_ser) {
+ memfd_luo_discard_folios(folios_ser, ser->nr_folios);
+ vfree(folios_ser);
+ }
}
-out:
kho_restore_free(ser);
}
+static bool memfd_luo_abort_serialized_data(u64 serialized_data)
+{
+ struct memfd_luo_ser *ser;
+ size_t bytes;
+
+ if (!serialized_data || !kho_is_restorable_phys(serialized_data))
+ return false;
+
+ bytes = kho_restorable_size(serialized_data);
+ ser = phys_to_virt(serialized_data);
+ if (bytes < sizeof(*ser)) {
+ kho_restore_free(ser);
+ return true;
+ }
+
+ memfd_luo_discard_serialized(ser);
+ return true;
+}
+
+bool luo_file_abort_known_serialized(const char *compatible, u64 serialized_data)
+{
+ if (strcmp(compatible, MEMFD_LUO_FH_COMPATIBLE))
+ return false;
+
+ return memfd_luo_abort_serialized_data(serialized_data);
+}
+
+static void memfd_luo_abort(struct liveupdate_file_op_args *args)
+{
+ if (!memfd_luo_abort_serialized_data(args->serialized_data))
+ return;
+}
+
static void memfd_luo_finish(struct liveupdate_file_op_args *args)
{
/*
@@ -521,33 +583,21 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
return -EINVAL;
ser = phys_to_virt(args->serialized_data);
- if (!ser)
+ if (kho_restorable_size(args->serialized_data) < sizeof(*ser)) {
+ kho_restore_free(ser);
return -EINVAL;
-
- if (!!ser->nr_folios != !!ser->folios.first.phys) {
- err = -EINVAL;
- goto free_ser;
- }
-
- if (ser->nr_folios >
- (((u64)ser->folios.total_pages << PAGE_SHIFT) /
- sizeof(*folios_ser))) {
- err = -EINVAL;
- goto free_ser;
}
- if (ser->nr_folios &&
- (!ser->size ||
- ser->nr_folios > ((ser->size - 1) >> PAGE_SHIFT) + 1)) {
+ if (!memfd_luo_serialized_valid(ser)) {
err = -EINVAL;
- goto free_ser;
+ goto discard_ser;
}
file = memfd_alloc_file("", 0);
if (IS_ERR(file)) {
pr_err("failed to setup file: %pe\n", file);
err = PTR_ERR(file);
- goto free_ser;
+ goto discard_ser;
}
vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
@@ -557,13 +607,13 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
folios_ser = kho_restore_vmalloc(&ser->folios);
if (!folios_ser) {
err = -EINVAL;
- goto put_file;
+ goto put_file_discard;
}
err = memfd_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
vfree(folios_ser);
if (err)
- goto put_file;
+ goto put_file_discard;
}
args->file = file;
@@ -571,10 +621,12 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
return 0;
-put_file:
+discard_ser:
+ memfd_luo_discard_serialized(ser);
+ return err;
+put_file_discard:
fput(file);
-free_ser:
- kho_restore_free(ser);
+ memfd_luo_discard_serialized(ser);
return err;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
2026-03-24 21:27 [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Oskar Gerlicz Kowalczuk
2026-03-24 21:27 ` [PATCH v4 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
@ 2026-03-25 13:38 ` Pasha Tatashin
2026-03-26 6:43 ` oskar
2026-03-26 6:47 ` oskar
1 sibling, 2 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-03-25 13:38 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Tue, Mar 24, 2026 at 5:29 PM Oskar Gerlicz Kowalczuk
<oskar@gerlicz.space> wrote:
>
> Hi Pasha,
>
> this v4 keeps the simpler direction from your mail: outgoing handover is
> still driven by a boolean rebooting gate, refcount pinning of outgoing
> sessions during serialization, and session->mutex as the serialization
> point for in-flight mutations. There is no return to the earlier closing
> counter or a larger session state machine.
>
> The main changes in this respin are:
>
> - reshape the series into five commits, each building and standing on its
> own
> - keep incoming session origin immutable and use retrieved only as the
> checked-out bit
> - make FINISH and implicit close consume incoming sessions without
> reopening races through retrieve-by-name
> - route deserialize failures through explicit rollback paths for
> sessions, files, and serialized memfd state
> - validate KHO-preserved extents before walking serialized metadata
> - harden incoming FLB lifetime and remaining teardown paths
>
> Patches 1-4 keep the core session, kexec, deserialize and validation work
> separate. Patch 5 carries the remaining FLB and teardown fixes needed to
> match the final tree.
>
> Oskar Gerlicz Kowalczuk (5):
> liveupdate: block outgoing session updates during reboot
> kexec: abort liveupdate handover on kernel_kexec() unwind
> liveupdate: fail session restore on file deserialization errors
> liveupdate: validate handover metadata before using it
> liveupdate: harden FLB lifetime and remaining teardown paths
>
> include/linux/kexec_handover.h | 13 +
> include/linux/liveupdate.h | 17 +-
> kernel/kexec_core.c | 4 +
> kernel/liveupdate/kexec_handover.c | 22 ++
> kernel/liveupdate/luo_core.c | 16 +-
> kernel/liveupdate/luo_file.c | 237 ++++++++++++--
> kernel/liveupdate/luo_flb.c | 116 +++++--
> kernel/liveupdate/luo_internal.h | 14 +-
> kernel/liveupdate/luo_session.c | 500 ++++++++++++++++++++++++-----
> lib/tests/liveupdate.c | 2 +
> mm/memfd_luo.c | 160 +++++++--
> 11 files changed, 934 insertions(+), 167 deletions(-)
Hi Oskar,
This is a NAK.
This patch series is still significantly over-engineering solutions to
straightforward problems, and the patches read as if they were
mechanically generated rather than thoughtfully designed. The sheer
volume of changes (nearly 1,000 lines) is unnecessary for what we are
trying to achieve.
As I pointed out previously, we need to keep this simple. For example,
the problem of preventing a return to userspace after a successful
liveupdate_reboot() does not require inventing a new
liveupdate_reboot_abort() function. It just requires placing the call
where it's the last function allowed to return, with a simple
exception for context-preserved kexec jumps. That is a trivial change:
- error = liveupdate_reboot();
- if (error)
- goto Unlock;
+ if (!kexec_image->preserve_context) {
+ error = liveupdate_reboot();
+ if (error)
+ goto Unlock;
+ }
The same principle applies to the other issues. You are doing rewrites
when targeted fixes should be applied:
- Sessions mutating after entering the reboot() syscall?: This is a
14-line fix [1].
- Destroyed serialization race during liveupdate_reboot()? This is a
70-line fix [2].
I was hoping that v4 (BTW, why are we at v4 in just a week?) would
include the patches I already provided, along with similarly scoped,
minimal fixes for the deserialization path if needed. Instead, I am
seeing another over-complicated rewrite.
Looking at deserialization, I am seeing, you are deleting a comment
that explicitly explains our error-handling design:
- /*
- * Note on error handling:
- *
- * If deserialization fails (e.g., allocation failure or corrupt data),
- * we intentionally skip cleanup of files that were already restored.
- *
- * A partial failure leaves the preserved state inconsistent.
- * Implementing a safe "undo" to unwind complex dependencies (sessions,
- * files, hardware state) is error-prone and provides little value, as
- * the system is effectively in a broken state.
- *
- * We treat these resources as leaked. The expected recovery path is for
- * userspace to detect the failure and trigger a reboot, which will
- * reliably reset devices and reclaim memory.
- */
You are adding a bunch of new functions to solve something we
intentionally chose not to solve. Attempting to cleanly unwind and
release incorrectly deserialized resources back to userspace is
dangerous. It risks security violations and, in the worst-case
scenario, customer data leaks. A failed deserialization means the
system is broken; we leak the resources (make them unavailable) and
rely on a reboot to reliably sanitize the state.
Please drop the over-engineered approaches. Use the patches provided,
keep the fixes minimal, and do not alter established security
boundaries like the deserialization error paths. Take your time to
manually self-review your work, do not rely on AI to do everything for
you.
[1] https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=d47b76707c4f352c3f70384d3d6a818e2875439a
[2] https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=f27a6a9364cd0a19067734eeb24ea4d290b72139
Pasha
>
> --
> 2.53.0
>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
2026-03-25 13:38 ` [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Pasha Tatashin
@ 2026-03-26 6:43 ` oskar
2026-03-26 6:47 ` oskar
1 sibling, 0 replies; 11+ messages in thread
From: oskar @ 2026-03-26 6:43 UTC (permalink / raw)
To: Pasha Tatashin
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On 2026-03-25 14:38, Pasha Tatashin wrote:
> On Tue, Mar 24, 2026 at 5:29 PM Oskar Gerlicz Kowalczuk
> <oskar@gerlicz.space> wrote:
>>
>> Hi Pasha,
>>
>> this v4 keeps the simpler direction from your mail: outgoing handover
>> is
>> still driven by a boolean rebooting gate, refcount pinning of outgoing
>> sessions during serialization, and session->mutex as the serialization
>> point for in-flight mutations. There is no return to the earlier
>> closing
>> counter or a larger session state machine.
>>
>> The main changes in this respin are:
>>
>> - reshape the series into five commits, each building and standing on
>> its
>> own
>> - keep incoming session origin immutable and use retrieved only as the
>> checked-out bit
>> - make FINISH and implicit close consume incoming sessions without
>> reopening races through retrieve-by-name
>> - route deserialize failures through explicit rollback paths for
>> sessions, files, and serialized memfd state
>> - validate KHO-preserved extents before walking serialized metadata
>> - harden incoming FLB lifetime and remaining teardown paths
>>
>> Patches 1-4 keep the core session, kexec, deserialize and validation
>> work
>> separate. Patch 5 carries the remaining FLB and teardown fixes needed
>> to
>> match the final tree.
>>
>> Oskar Gerlicz Kowalczuk (5):
>> liveupdate: block outgoing session updates during reboot
>> kexec: abort liveupdate handover on kernel_kexec() unwind
>> liveupdate: fail session restore on file deserialization errors
>> liveupdate: validate handover metadata before using it
>> liveupdate: harden FLB lifetime and remaining teardown paths
>>
>> include/linux/kexec_handover.h | 13 +
>> include/linux/liveupdate.h | 17 +-
>> kernel/kexec_core.c | 4 +
>> kernel/liveupdate/kexec_handover.c | 22 ++
>> kernel/liveupdate/luo_core.c | 16 +-
>> kernel/liveupdate/luo_file.c | 237 ++++++++++++--
>> kernel/liveupdate/luo_flb.c | 116 +++++--
>> kernel/liveupdate/luo_internal.h | 14 +-
>> kernel/liveupdate/luo_session.c | 500
>> ++++++++++++++++++++++++-----
>> lib/tests/liveupdate.c | 2 +
>> mm/memfd_luo.c | 160 +++++++--
>> 11 files changed, 934 insertions(+), 167 deletions(-)
>
> Hi Oskar,
>
> This is a NAK.
>
> This patch series is still significantly over-engineering solutions to
> straightforward problems, and the patches read as if they were
> mechanically generated rather than thoughtfully designed. The sheer
> volume of changes (nearly 1,000 lines) is unnecessary for what we are
> trying to achieve.
>
> As I pointed out previously, we need to keep this simple. For example,
> the problem of preventing a return to userspace after a successful
> liveupdate_reboot() does not require inventing a new
> liveupdate_reboot_abort() function. It just requires placing the call
> where it's the last function allowed to return, with a simple
> exception for context-preserved kexec jumps. That is a trivial change:
>
> - error = liveupdate_reboot();
> - if (error)
> - goto Unlock;
> + if (!kexec_image->preserve_context) {
> + error = liveupdate_reboot();
> + if (error)
> + goto Unlock;
> + }
>
> The same principle applies to the other issues. You are doing rewrites
> when targeted fixes should be applied:
> - Sessions mutating after entering the reboot() syscall?: This is a
> 14-line fix [1].
> - Destroyed serialization race during liveupdate_reboot()? This is a
> 70-line fix [2].
>
> I was hoping that v4 (BTW, why are we at v4 in just a week?) would
> include the patches I already provided, along with similarly scoped,
> minimal fixes for the deserialization path if needed. Instead, I am
> seeing another over-complicated rewrite.
>
> Looking at deserialization, I am seeing, you are deleting a comment
> that explicitly explains our error-handling design:
> - /*
> - * Note on error handling:
> - *
> - * If deserialization fails (e.g., allocation failure or
> corrupt data),
> - * we intentionally skip cleanup of files that were already
> restored.
> - *
> - * A partial failure leaves the preserved state inconsistent.
> - * Implementing a safe "undo" to unwind complex dependencies
> (sessions,
> - * files, hardware state) is error-prone and provides little
> value, as
> - * the system is effectively in a broken state.
> - *
> - * We treat these resources as leaked. The expected recovery
> path is for
> - * userspace to detect the failure and trigger a reboot, which
> will
> - * reliably reset devices and reclaim memory.
> - */
>
> You are adding a bunch of new functions to solve something we
> intentionally chose not to solve. Attempting to cleanly unwind and
> release incorrectly deserialized resources back to userspace is
> dangerous. It risks security violations and, in the worst-case
> scenario, customer data leaks. A failed deserialization means the
> system is broken; we leak the resources (make them unavailable) and
> rely on a reboot to reliably sanitize the state.
>
> Please drop the over-engineered approaches. Use the patches provided,
> keep the fixes minimal, and do not alter established security
> boundaries like the deserialization error paths. Take your time to
> manually self-review your work, do not rely on AI to do everything for
> you.
>
> [1]
> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=d47b76707c4f352c3f70384d3d6a818e2875439a
> [2]
> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=f27a6a9364cd0a19067734eeb24ea4d290b72139
>
> Pasha
>
>>
>> --
>> 2.53.0
>>
Hi Pasha, thank you for the detailed feedback and for sharing your
patches. I understand that my previous version was overcomplicated. I
will rework the series to follow your approach and keep the changes
minimal. I plan to send an updated version later today. Thanks, Oskar
Gerlicz-Kowalczuk
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
2026-03-25 13:38 ` [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Pasha Tatashin
2026-03-26 6:43 ` oskar
@ 2026-03-26 6:47 ` oskar
2026-03-26 9:18 ` Mike Rapoport
1 sibling, 1 reply; 11+ messages in thread
From: oskar @ 2026-03-26 6:47 UTC (permalink / raw)
To: Pasha Tatashin
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On 2026-03-25 14:38, Pasha Tatashin wrote:
> On Tue, Mar 24, 2026 at 5:29 PM Oskar Gerlicz Kowalczuk
> <oskar@gerlicz.space> wrote:
>>
>> Hi Pasha,
>>
>> this v4 keeps the simpler direction from your mail: outgoing handover
>> is
>> still driven by a boolean rebooting gate, refcount pinning of outgoing
>> sessions during serialization, and session->mutex as the serialization
>> point for in-flight mutations. There is no return to the earlier
>> closing
>> counter or a larger session state machine.
>>
>> The main changes in this respin are:
>>
>> - reshape the series into five commits, each building and standing on
>> its
>> own
>> - keep incoming session origin immutable and use retrieved only as the
>> checked-out bit
>> - make FINISH and implicit close consume incoming sessions without
>> reopening races through retrieve-by-name
>> - route deserialize failures through explicit rollback paths for
>> sessions, files, and serialized memfd state
>> - validate KHO-preserved extents before walking serialized metadata
>> - harden incoming FLB lifetime and remaining teardown paths
>>
>> Patches 1-4 keep the core session, kexec, deserialize and validation
>> work
>> separate. Patch 5 carries the remaining FLB and teardown fixes needed
>> to
>> match the final tree.
>>
>> Oskar Gerlicz Kowalczuk (5):
>> liveupdate: block outgoing session updates during reboot
>> kexec: abort liveupdate handover on kernel_kexec() unwind
>> liveupdate: fail session restore on file deserialization errors
>> liveupdate: validate handover metadata before using it
>> liveupdate: harden FLB lifetime and remaining teardown paths
>>
>> include/linux/kexec_handover.h | 13 +
>> include/linux/liveupdate.h | 17 +-
>> kernel/kexec_core.c | 4 +
>> kernel/liveupdate/kexec_handover.c | 22 ++
>> kernel/liveupdate/luo_core.c | 16 +-
>> kernel/liveupdate/luo_file.c | 237 ++++++++++++--
>> kernel/liveupdate/luo_flb.c | 116 +++++--
>> kernel/liveupdate/luo_internal.h | 14 +-
>> kernel/liveupdate/luo_session.c | 500
>> ++++++++++++++++++++++++-----
>> lib/tests/liveupdate.c | 2 +
>> mm/memfd_luo.c | 160 +++++++--
>> 11 files changed, 934 insertions(+), 167 deletions(-)
>
> Hi Oskar,
>
> This is a NAK.
>
> This patch series is still significantly over-engineering solutions to
> straightforward problems, and the patches read as if they were
> mechanically generated rather than thoughtfully designed. The sheer
> volume of changes (nearly 1,000 lines) is unnecessary for what we are
> trying to achieve.
>
> As I pointed out previously, we need to keep this simple. For example,
> the problem of preventing a return to userspace after a successful
> liveupdate_reboot() does not require inventing a new
> liveupdate_reboot_abort() function. It just requires placing the call
> where it's the last function allowed to return, with a simple
> exception for context-preserved kexec jumps. That is a trivial change:
>
> - error = liveupdate_reboot();
> - if (error)
> - goto Unlock;
> + if (!kexec_image->preserve_context) {
> + error = liveupdate_reboot();
> + if (error)
> + goto Unlock;
> + }
>
> The same principle applies to the other issues. You are doing rewrites
> when targeted fixes should be applied:
> - Sessions mutating after entering the reboot() syscall?: This is a
> 14-line fix [1].
> - Destroyed serialization race during liveupdate_reboot()? This is a
> 70-line fix [2].
>
> I was hoping that v4 (BTW, why are we at v4 in just a week?) would
> include the patches I already provided, along with similarly scoped,
> minimal fixes for the deserialization path if needed. Instead, I am
> seeing another over-complicated rewrite.
>
> Looking at deserialization, I am seeing, you are deleting a comment
> that explicitly explains our error-handling design:
> - /*
> - * Note on error handling:
> - *
> - * If deserialization fails (e.g., allocation failure or
> corrupt data),
> - * we intentionally skip cleanup of files that were already
> restored.
> - *
> - * A partial failure leaves the preserved state inconsistent.
> - * Implementing a safe "undo" to unwind complex dependencies
> (sessions,
> - * files, hardware state) is error-prone and provides little
> value, as
> - * the system is effectively in a broken state.
> - *
> - * We treat these resources as leaked. The expected recovery
> path is for
> - * userspace to detect the failure and trigger a reboot, which
> will
> - * reliably reset devices and reclaim memory.
> - */
>
> You are adding a bunch of new functions to solve something we
> intentionally chose not to solve. Attempting to cleanly unwind and
> release incorrectly deserialized resources back to userspace is
> dangerous. It risks security violations and, in the worst-case
> scenario, customer data leaks. A failed deserialization means the
> system is broken; we leak the resources (make them unavailable) and
> rely on a reboot to reliably sanitize the state.
>
> Please drop the over-engineered approaches. Use the patches provided,
> keep the fixes minimal, and do not alter established security
> boundaries like the deserialization error paths. Take your time to
> manually self-review your work, do not rely on AI to do everything for
> you.
>
> [1]
> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=d47b76707c4f352c3f70384d3d6a818e2875439a
> [2]
> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=f27a6a9364cd0a19067734eeb24ea4d290b72139
>
> Pasha
>
>>
>> --
>> 2.53.0
>>
Hi Pasha, thank you for the detailed feedback and for sharing your
patches. I understand that my previous version was overcomplicated. I
will rework the series to follow your approach and keep the changes
minimal. I plan to send an updated version later today.
Thanks,
Oskar Gerlicz-Kowalczuk
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
2026-03-26 6:47 ` oskar
@ 2026-03-26 9:18 ` Mike Rapoport
2026-03-26 14:49 ` Pasha Tatashin
0 siblings, 1 reply; 11+ messages in thread
From: Mike Rapoport @ 2026-03-26 9:18 UTC (permalink / raw)
To: oskar
Cc: Pasha Tatashin, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
Hi Oskar,
On Thu, Mar 26, 2026 at 07:47:03AM +0100, oskar@gerlicz.space wrote:
> On 2026-03-25 14:38, Pasha Tatashin wrote:
> > On Tue, Mar 24, 2026 at 5:29 PM Oskar Gerlicz Kowalczuk
>
> Hi Pasha, thank you for the detailed feedback and for sharing your patches.
> I understand that my previous version was overcomplicated. I will rework the
> series to follow your approach and keep the changes minimal. I plan to send
> an updated version later today.
When you are resending please make sure to avoid deep threading of the
replies so that all patch emails would be In-Reply-To to the cover letter.
> Thanks,
> Oskar Gerlicz-Kowalczuk
>
--
Sincerely yours,
Mike.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime
2026-03-26 9:18 ` Mike Rapoport
@ 2026-03-26 14:49 ` Pasha Tatashin
0 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-03-26 14:49 UTC (permalink / raw)
To: Mike Rapoport
Cc: oskar, Baoquan He, Pratyush Yadav, Andrew Morton, linux-kernel,
kexec, linux-mm
On Thu, Mar 26, 2026 at 5:18 AM Mike Rapoport <rppt@kernel.org> wrote:
>
> Hi Oskar,
>
> On Thu, Mar 26, 2026 at 07:47:03AM +0100, oskar@gerlicz.space wrote:
> > On 2026-03-25 14:38, Pasha Tatashin wrote:
> > > On Tue, Mar 24, 2026 at 5:29 PM Oskar Gerlicz Kowalczuk
> >
> > Hi Pasha, thank you for the detailed feedback and for sharing your patches.
> > I understand that my previous version was overcomplicated. I will rework the
> > series to follow your approach and keep the changes minimal. I plan to send
> > an updated version later today.
Please take a few days to manually review the patches before sending
them out; no need to rush them out today. As I mentioned earlier, it
is very unusual for a series to iterate to v4 within a week.
> When you are resending please make sure to avoid deep threading of the
> replies so that all patch emails would be In-Reply-To to the cover letter.
+1
>
> > Thanks,
> > Oskar Gerlicz-Kowalczuk
> >
>
> --
> Sincerely yours,
> Mike.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-03-26 14:50 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-24 21:27 [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Oskar Gerlicz Kowalczuk
2026-03-24 21:27 ` [PATCH v4 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
2026-03-24 21:27 ` [PATCH v4 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
2026-03-24 21:39 ` [PATCH v4 5/5] liveupdate: harden FLB lifetime and remaining teardown paths Oskar Gerlicz Kowalczuk
2026-03-25 13:38 ` [PATCH v4 0/5] liveupdate: tighten handover cleanup and session lifetime Pasha Tatashin
2026-03-26 6:43 ` oskar
2026-03-26 6:47 ` oskar
2026-03-26 9:18 ` Mike Rapoport
2026-03-26 14:49 ` Pasha Tatashin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox