* [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
@ 2026-03-21 14:36 Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-21 14:36 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
kernel_kexec() serializes outgoing sessions before the reboot path
freezes tasks, so close() and session ioctls can still mutate a
session while handover state is being prepared. The original v2 code
also let incoming lookups keep a bare session pointer after dropping
the list lock.
That leaves two correctness problems in the reboot path: outgoing state
can change after serialization starts, and incoming sessions can be
freed while another thread still holds a pointer to them.
Add refcounted session lifetime management, track in-flight outgoing
close() paths with an atomic closing counter, and make serialization
wait for closing to drain before setting rebooting. Reject phase-invalid
ioctls, keep incoming release on a common cleanup path, and make the
release wait freezable without spinning.
Fixes: fc5acd5c89fe ("liveupdate: block outgoing session updates during reboot")
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
kernel/liveupdate/luo_internal.h | 12 +-
kernel/liveupdate/luo_session.c | 236 +++++++++++++++++++++++++++----
2 files changed, 221 insertions(+), 27 deletions(-)
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index 8083d8739b09..0cfc0269d746 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,7 @@ 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.
+ * @state: Current lifecycle phase of the session.
* @file_set: A set of files that belong to this session.
* @mutex: protects fields in the luo_session.
*/
@@ -72,8 +72,14 @@ struct luo_session {
char name[LIVEUPDATE_SESSION_NAME_LENGTH];
struct luo_session_ser *ser;
struct list_head list;
- bool retrieved;
+ enum {
+ LUO_SESSION_OUTGOING,
+ LUO_SESSION_INCOMING,
+ LUO_SESSION_RETRIEVED,
+ LUO_SESSION_CLOSED,
+ } state;
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..d97ec91e1118 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>
@@ -66,6 +67,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"
@@ -89,10 +91,13 @@
*/
struct luo_session_header {
long count;
+ atomic_t closing;
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 +115,24 @@ 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 = {
+ .closing = ATOMIC_INIT(0),
.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,8 @@ 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);
+ session->state = LUO_SESSION_OUTGOING;
+ refcount_set(&session->refs, 1);
mutex_init(&session->mutex);
return session;
@@ -140,6 +158,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 +181,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,17 +204,98 @@ 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(&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_outgoing_begin(struct luo_session *session,
+ struct luo_session_header **shp)
+{
+ struct luo_session_header *sh;
+
+ if (READ_ONCE(session->state) != LUO_SESSION_OUTGOING)
+ 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_outgoing_end(struct luo_session_header *sh)
+{
+ if (sh)
+ up_read(&sh->rwsem);
+}
+
+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)
+{
+ int err;
+
+ guard(mutex)(&session->mutex);
+ if (session->state != LUO_SESSION_RETRIEVED)
+ return -EINVAL;
+
+ err = luo_file_finish(&session->file_set);
+ if (err)
+ session->state = LUO_SESSION_INCOMING;
+
+ return err;
+}
+
+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);
+ session->state = LUO_SESSION_CLOSED;
+ luo_file_abort_deserialized(&session->file_set);
+ luo_session_put(session);
+ }
+ up_write(&sh->rwsem);
+
+ luo_flb_discard_incoming();
}
static int luo_session_finish_one(struct luo_session *session)
{
guard(mutex)(&session->mutex);
+ if (session->state != LUO_SESSION_RETRIEVED)
+ return -EINVAL;
+
return luo_file_finish(&session->file_set);
}
@@ -204,26 +317,67 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
{
struct luo_session *session = filep->private_data;
struct luo_session_header *sh;
+ int state = READ_ONCE(session->state);
+ int ret = 0;
+ bool discard_flb = false;
- /* If retrieved is set, it means this session is from incoming list */
- if (session->retrieved) {
- int err = luo_session_finish_one(session);
+ if (state == LUO_SESSION_RETRIEVED) {
+ ret = luo_session_finish_retrieved(session);
- if (err) {
+ if (ret) {
pr_warn("Unable to finish session [%s] on release\n",
session->name);
- return err;
+ luo_session_put(session);
+ return ret;
}
- sh = &luo_session_global.incoming;
- } else {
scoped_guard(mutex, &session->mutex)
- luo_file_unpreserve_files(&session->file_set);
- sh = &luo_session_global.outgoing;
+ session->state = LUO_SESSION_CLOSED;
+ sh = &luo_session_global.incoming;
+ down_write(&sh->rwsem);
+ __luo_session_remove(sh, session);
+ discard_flb = !sh->count;
+ up_write(&sh->rwsem);
+ luo_session_put(session);
+ luo_session_put(session);
+ if (discard_flb)
+ luo_flb_discard_incoming();
+ return 0;
}
- luo_session_remove(sh, session);
- luo_session_free(session);
+ if (state != LUO_SESSION_OUTGOING) {
+ WARN_ON_ONCE(1);
+ luo_session_put(session);
+ return 0;
+ }
+ sh = &luo_session_global.outgoing;
+
+ for (;;) {
+ down_write(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting)) {
+ up_write(&sh->rwsem);
+ luo_session_wait_reboot(sh);
+ continue;
+ }
+
+ atomic_inc(&sh->closing);
+ __luo_session_remove(sh, session);
+ up_write(&sh->rwsem);
+ break;
+ }
+
+ scoped_guard(mutex, &session->mutex) {
+ session->state = LUO_SESSION_CLOSED;
+ luo_file_unpreserve_files(&session->file_set);
+ }
+
+ down_write(&sh->rwsem);
+ if (atomic_dec_and_test(&sh->closing))
+ wake_up_all(&sh->reboot_waitq);
+ up_write(&sh->rwsem);
+
+ luo_session_put(session);
+ luo_session_put(session);
return 0;
}
@@ -231,10 +385,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;
@@ -252,6 +412,11 @@ static int luo_session_retrieve_fd(struct luo_session *session,
struct file *file;
int err;
+ scoped_guard(mutex, &session->mutex) {
+ if (session->state != LUO_SESSION_RETRIEVED)
+ return -EINVAL;
+ }
+
argp->fd = get_unused_fd_flags(O_CLOEXEC);
if (argp->fd < 0)
return argp->fd;
@@ -281,8 +446,9 @@ 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);
+ int err;
+ err = luo_session_finish_one(session);
if (err)
return err;
@@ -371,9 +537,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;
@@ -403,7 +572,7 @@ int luo_session_create(const char *name, struct file **filep)
err_remove:
luo_session_remove(&luo_session_global.outgoing, session);
err_free:
- luo_session_free(session);
+ luo_session_put(session);
return err;
}
@@ -418,6 +587,7 @@ 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))) {
+ luo_session_get(it);
session = it;
break;
}
@@ -428,12 +598,14 @@ int luo_session_retrieve(const char *name, struct file **filep)
return -ENOENT;
guard(mutex)(&session->mutex);
- if (session->retrieved)
- return -EINVAL;
+ if (session->state != LUO_SESSION_INCOMING)
+ err = -EINVAL;
+ else
+ err = luo_session_getfile(session, filep);
- err = luo_session_getfile(session, filep);
if (!err)
- session->retrieved = true;
+ session->state = LUO_SESSION_RETRIEVED;
+ luo_session_put(session);
return err;
}
@@ -548,6 +720,7 @@ int luo_session_deserialize(void)
sh->ser[i].name, session);
return PTR_ERR(session);
}
+ session->state = LUO_SESSION_INCOMING;
err = luo_session_insert(sh, session);
if (err) {
@@ -578,6 +751,17 @@ int luo_session_serialize(void)
int err;
guard(rwsem_write)(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting))
+ return -EBUSY;
+
+ while (atomic_read(&sh->closing)) {
+ up_write(&sh->rwsem);
+ wait_event(sh->reboot_waitq, !atomic_read(&sh->closing));
+ down_write(&sh->rwsem);
+ if (READ_ONCE(sh->rebooting))
+ return -EBUSY;
+ }
+ WRITE_ONCE(sh->rebooting, true);
list_for_each_entry(session, &sh->list, list) {
err = luo_session_freeze_one(session, &sh->ser[i]);
if (err)
@@ -595,8 +779,11 @@ int luo_session_serialize(void)
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));
+ memset(&sh->ser[i], 0, sizeof(sh->ser[i]));
}
+ sh->header_ser->count = 0;
+ /* Reset rebooting flag on serialization failure. */
+ luo_session_reboot_done(sh);
return err;
}
@@ -624,7 +811,8 @@ bool luo_session_quiesce(void)
down_write(&luo_session_global.outgoing.rwsem);
if (luo_session_global.incoming.count ||
- luo_session_global.outgoing.count) {
+ luo_session_global.outgoing.count ||
+ atomic_read(&luo_session_global.outgoing.closing)) {
up_write(&luo_session_global.outgoing.rwsem);
up_write(&luo_session_global.incoming.rwsem);
return false;
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind
2026-03-21 14:36 [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
@ 2026-03-21 14:36 ` Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
2026-03-21 23:05 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Pasha Tatashin
2026-03-21 17:45 ` [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Andrew Morton
2026-03-21 22:28 ` Pasha Tatashin
2 siblings, 2 replies; 12+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-21 14:36 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 | 23 +++++++++++++++++++++++
5 files changed, 44 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 0cfc0269d746..8a6b1f6c9b4f 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -88,6 +88,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 d97ec91e1118..200dd3b8229c 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -788,6 +788,29 @@ int luo_session_serialize(void)
return err;
}
+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] 12+ messages in thread
* [PATCH v3 3/5] liveupdate: fail session restore on file deserialization errors
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
@ 2026-03-21 14:36 ` Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
2026-03-21 23:05 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Pasha Tatashin
1 sibling, 1 reply; 12+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-21 14:36 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 | 42 +++++++++-----------
mm/memfd_luo.c | 24 +++++++-----
5 files changed, 86 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 8a6b1f6c9b4f..4842c7dbeb63 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -102,6 +102,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 200dd3b8229c..602001327f58 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -693,24 +693,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;
@@ -718,7 +704,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->state = LUO_SESSION_INCOMING;
@@ -726,21 +713,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] 12+ messages in thread
* [PATCH v3 4/5] liveupdate: validate handover metadata before using it
2026-03-21 14:36 ` [PATCH v3 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
@ 2026-03-21 14:36 ` Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 5/5] liveupdate: harden FLB lifetime and teardown paths Oskar Gerlicz Kowalczuk
0 siblings, 1 reply; 12+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-21 14:36 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 602001327f58..f7f62f11d17c 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -673,6 +673,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;
@@ -697,9 +705,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",
@@ -728,9 +749,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] 12+ messages in thread
* [PATCH v3 5/5] liveupdate: harden FLB lifetime and teardown paths
2026-03-21 14:36 ` [PATCH v3 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
@ 2026-03-21 14:36 ` Oskar Gerlicz Kowalczuk
0 siblings, 0 replies; 12+ messages in thread
From: Oskar Gerlicz Kowalczuk @ 2026-03-21 14:36 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Baoquan He
Cc: Pratyush Yadav, Andrew Morton, linux-kernel, kexec, linux-mm,
Oskar Gerlicz Kowalczuk
The remaining FLB teardown paths still had a few lifetime holes. Incoming
counters could underflow, the returned incoming object was not pinned
against concurrent finish, and copied incoming FLB metadata could be
freed while another thread was still scanning it.
That makes incoming FLB teardown racy and can leave callers with stale
objects or freed metadata.
Guard the FLB counters, make liveupdate_flb_get_incoming() hold the
incoming FLB lock until the caller releases it, and serialize access to
the copied incoming FLB metadata while it is being retrieved or
discarded.
Fixes: 8c31961065ee ("liveupdate: harden FLB and incoming teardown paths")
Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
---
include/linux/liveupdate.h | 5 ++
kernel/liveupdate/luo_flb.c | 78 ++++++++++++++++++++++++--------
kernel/liveupdate/luo_internal.h | 1 +
lib/tests/liveupdate.c | 2 +
4 files changed, 66 insertions(+), 20 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 611907f57127..dd34587a9c4e 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -240,6 +240,7 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb);
int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp);
+void liveupdate_flb_put_incoming(struct liveupdate_flb *flb);
int liveupdate_flb_get_outgoing(struct liveupdate_flb *flb, void **objp);
#else /* CONFIG_LIVEUPDATE */
@@ -286,6 +287,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/liveupdate/luo_flb.c b/kernel/liveupdate/luo_flb.c
index cdd293408138..2cd5864c8b26 100644
--- a/kernel/liveupdate/luo_flb.c
+++ b/kernel/liveupdate/luo_flb.c
@@ -70,6 +70,8 @@ struct luo_flb_global {
long count;
};
+static DEFINE_MUTEX(luo_flb_incoming_lock);
+
static struct luo_flb_global luo_flb_global = {
.list = LIST_HEAD_INIT(luo_flb_global.list),
};
@@ -128,6 +130,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};
@@ -161,22 +166,24 @@ static int luo_flb_retrieve_one(struct liveupdate_flb *flb)
if (private->incoming.retrieved)
return 0;
- if (!fh->active)
- 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;
+ scoped_guard(mutex, &luo_flb_incoming_lock) {
+ if (!fh->active)
+ return -ENODATA;
- if (!strcmp(fh->ser[i].name, flb->compatible)) {
- if (!fh->ser[i].count)
+ 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;
- private->incoming.data = fh->ser[i].data;
- private->incoming.count = fh->ser[i].count;
- found = true;
- break;
+ 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;
+ break;
+ }
}
}
@@ -201,8 +208,12 @@ 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)
+ scoped_guard(mutex, &private->incoming.lock) {
+ if (WARN_ON_ONCE(!private->incoming.count))
+ return;
+
count = --private->incoming.count;
+ }
if (!count) {
struct liveupdate_flb_op_args args = {0};
@@ -303,6 +314,17 @@ 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;
+
+ guard(mutex)(&luo_flb_incoming_lock);
+ 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.
@@ -490,7 +512,8 @@ int liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
* @objp: Output parameter; will be populated with the live shared object.
*
* Returns a pointer to its shared live object for the incoming (post-reboot)
- * path.
+ * path. The returned pointer remains valid until
+ * liveupdate_flb_put_incoming() is called.
*
* If this is the first time the object is requested in the new kernel, this
* function will trigger the FLB's .retrieve() callback to reconstruct the
@@ -508,17 +531,30 @@ int liveupdate_flb_get_incoming(struct liveupdate_flb *flb, void **objp)
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)
+ goto err_unlock;
*objp = private->incoming.obj;
return 0;
+
+err_unlock:
+ mutex_unlock(&private->incoming.lock);
+ return -ENODATA;
+}
+
+void liveupdate_flb_put_incoming(struct liveupdate_flb *flb)
+{
+ struct luo_flb_private *private = luo_flb_get_private(flb);
+
+ mutex_unlock(&private->incoming.lock);
}
/**
@@ -640,9 +676,11 @@ int __init luo_flb_setup_incoming(void *fdt_in)
if (!header_copy)
return -ENOMEM;
- luo_flb_global.incoming.header_ser = header_copy;
- luo_flb_global.incoming.ser = (void *)(header_copy + 1);
- luo_flb_global.incoming.active = true;
+ scoped_guard(mutex, &luo_flb_incoming_lock) {
+ 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_internal.h b/kernel/liveupdate/luo_internal.h
index 4842c7dbeb63..e4d1919370f1 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -113,6 +113,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/lib/tests/liveupdate.c b/lib/tests/liveupdate.c
index 496d6ef91a30..620cefd52229 100644
--- a/lib/tests/liveupdate.c
+++ b/lib/tests/liveupdate.c
@@ -105,6 +105,8 @@ static void liveupdate_test_init(void)
pr_err("liveupdate_flb_get_incoming for %s failed: %pe\n",
flb->compatible, ERR_PTR(err));
}
+ if (!err)
+ liveupdate_flb_put_incoming(flb);
}
initialized = true;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
2026-03-21 14:36 [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
@ 2026-03-21 17:45 ` Andrew Morton
2026-03-21 22:28 ` Pasha Tatashin
2 siblings, 0 replies; 12+ messages in thread
From: Andrew Morton @ 2026-03-21 17:45 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Pasha Tatashin, Mike Rapoport, Baoquan He, Pratyush Yadav,
linux-kernel, kexec, linux-mm
On Sat, 21 Mar 2026 15:36:38 +0100 Oskar Gerlicz Kowalczuk <oskar@gerlicz.space> wrote:
> kernel_kexec() serializes outgoing sessions before the reboot path
> freezes tasks, so close() and session ioctls can still mutate a
> session while handover state is being prepared. The original v2 code
> also let incoming lookups keep a bare session pointer after dropping
> the list lock.
>
> That leaves two correctness problems in the reboot path: outgoing state
> can change after serialization starts, and incoming sessions can be
> freed while another thread still holds a pointer to them.
>
> Add refcounted session lifetime management, track in-flight outgoing
> close() paths with an atomic closing counter, and make serialization
> wait for closing to drain before setting rebooting. Reject phase-invalid
> ioctls, keep incoming release on a common cleanup path, and make the
> release wait freezable without spinning.
A [0/N] cover letter would be appropriate.
Please have a think about which patches should be backported into
-stable kernels. Possibly "none", given how new LUO is.
If there are any backportable patches, please ensure that they are at
the start-of-series, or separate them out into a separate series,
against current mainline.
AI review hasn't completed yet, but it has things to say:
https://sashiko.dev/#/patchset/20260321143642.166313-1-oskar@gerlicz.space
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
2026-03-21 14:36 [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
2026-03-21 17:45 ` [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Andrew Morton
@ 2026-03-21 22:28 ` Pasha Tatashin
2026-03-23 19:00 ` Pasha Tatashin
2 siblings, 1 reply; 12+ messages in thread
From: Pasha Tatashin @ 2026-03-21 22:28 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Sat, Mar 21, 2026 at 10:38 AM Oskar Gerlicz Kowalczuk
<oskar@gerlicz.space> wrote:
>
> kernel_kexec() serializes outgoing sessions before the reboot path
> freezes tasks, so close() and session ioctls can still mutate a
> session while handover state is being prepared. The original v2 code
> also let incoming lookups keep a bare session pointer after dropping
> the list lock.
>
> That leaves two correctness problems in the reboot path: outgoing state
> can change after serialization starts, and incoming sessions can be
> freed while another thread still holds a pointer to them.
>
> Add refcounted session lifetime management, track in-flight outgoing
> close() paths with an atomic closing counter, and make serialization
> wait for closing to drain before setting rebooting. Reject phase-invalid
> ioctls, keep incoming release on a common cleanup path, and make the
> release wait freezable without spinning.
>
> Fixes: fc5acd5c89fe ("liveupdate: block outgoing session updates during reboot")
> Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
> ---
> kernel/liveupdate/luo_internal.h | 12 +-
> kernel/liveupdate/luo_session.c | 236 +++++++++++++++++++++++++++----
> 2 files changed, 221 insertions(+), 27 deletions(-)
Hi Oskar,
Thank you for sending this series and finding these bugs in LUO. I
agree with Andrew that a cover letter would help to understand the
summary of the overall effort.
I have not reviewed the other patches yet, but for this patch, my
understanding is that it solves two specific races during reboot()
syscalls: session closure after serialization, and the addition of new
sessions or preserving new files after serialization.
Given that KHO is now stateless, and liveupdate_reboot() is
specifically placed at the last point where we can still return an
error to userspace, we should simply return an error if a userspace is
doing something unexpected.
Instead of creating a new state machine, let's just reuse the file
references and simply take them for each session at the beginning of
serialization. This ensures that no session closes will happen later.
For file preservation and session addition, we can block them by
simply adding a new boolean.
Please take a look at the two patches below and see if this approach
would work. It is a much smaller change compared to the proposed state
machine in this patch.
https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/log/?h=luo-reboot-sync/rfc/1
Thanks,
Pasha
>
> diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
> index 8083d8739b09..0cfc0269d746 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,7 @@ 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.
> + * @state: Current lifecycle phase of the session.
> * @file_set: A set of files that belong to this session.
> * @mutex: protects fields in the luo_session.
> */
> @@ -72,8 +72,14 @@ struct luo_session {
> char name[LIVEUPDATE_SESSION_NAME_LENGTH];
> struct luo_session_ser *ser;
> struct list_head list;
> - bool retrieved;
> + enum {
> + LUO_SESSION_OUTGOING,
> + LUO_SESSION_INCOMING,
> + LUO_SESSION_RETRIEVED,
> + LUO_SESSION_CLOSED,
> + } state;
> 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..d97ec91e1118 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>
> @@ -66,6 +67,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"
>
> @@ -89,10 +91,13 @@
> */
> struct luo_session_header {
> long count;
> + atomic_t closing;
> 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 +115,24 @@ 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 = {
> + .closing = ATOMIC_INIT(0),
> .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,8 @@ 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);
> + session->state = LUO_SESSION_OUTGOING;
> + refcount_set(&session->refs, 1);
> mutex_init(&session->mutex);
>
> return session;
> @@ -140,6 +158,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 +181,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,17 +204,98 @@ 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(&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_outgoing_begin(struct luo_session *session,
> + struct luo_session_header **shp)
> +{
> + struct luo_session_header *sh;
> +
> + if (READ_ONCE(session->state) != LUO_SESSION_OUTGOING)
> + 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_outgoing_end(struct luo_session_header *sh)
> +{
> + if (sh)
> + up_read(&sh->rwsem);
> +}
> +
> +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)
> +{
> + int err;
> +
> + guard(mutex)(&session->mutex);
> + if (session->state != LUO_SESSION_RETRIEVED)
> + return -EINVAL;
> +
> + err = luo_file_finish(&session->file_set);
> + if (err)
> + session->state = LUO_SESSION_INCOMING;
> +
> + return err;
> +}
> +
> +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);
> + session->state = LUO_SESSION_CLOSED;
> + luo_file_abort_deserialized(&session->file_set);
> + luo_session_put(session);
> + }
> + up_write(&sh->rwsem);
> +
> + luo_flb_discard_incoming();
> }
>
> static int luo_session_finish_one(struct luo_session *session)
> {
> guard(mutex)(&session->mutex);
> + if (session->state != LUO_SESSION_RETRIEVED)
> + return -EINVAL;
> +
> return luo_file_finish(&session->file_set);
> }
>
> @@ -204,26 +317,67 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
> {
> struct luo_session *session = filep->private_data;
> struct luo_session_header *sh;
> + int state = READ_ONCE(session->state);
> + int ret = 0;
> + bool discard_flb = false;
>
> - /* If retrieved is set, it means this session is from incoming list */
> - if (session->retrieved) {
> - int err = luo_session_finish_one(session);
> + if (state == LUO_SESSION_RETRIEVED) {
> + ret = luo_session_finish_retrieved(session);
>
> - if (err) {
> + if (ret) {
> pr_warn("Unable to finish session [%s] on release\n",
> session->name);
> - return err;
> + luo_session_put(session);
> + return ret;
> }
> - sh = &luo_session_global.incoming;
> - } else {
> scoped_guard(mutex, &session->mutex)
> - luo_file_unpreserve_files(&session->file_set);
> - sh = &luo_session_global.outgoing;
> + session->state = LUO_SESSION_CLOSED;
> + sh = &luo_session_global.incoming;
> + down_write(&sh->rwsem);
> + __luo_session_remove(sh, session);
> + discard_flb = !sh->count;
> + up_write(&sh->rwsem);
> + luo_session_put(session);
> + luo_session_put(session);
> + if (discard_flb)
> + luo_flb_discard_incoming();
> + return 0;
> }
>
> - luo_session_remove(sh, session);
> - luo_session_free(session);
> + if (state != LUO_SESSION_OUTGOING) {
> + WARN_ON_ONCE(1);
> + luo_session_put(session);
> + return 0;
> + }
>
> + sh = &luo_session_global.outgoing;
> +
> + for (;;) {
> + down_write(&sh->rwsem);
> + if (READ_ONCE(sh->rebooting)) {
> + up_write(&sh->rwsem);
> + luo_session_wait_reboot(sh);
> + continue;
> + }
> +
> + atomic_inc(&sh->closing);
> + __luo_session_remove(sh, session);
> + up_write(&sh->rwsem);
> + break;
> + }
> +
> + scoped_guard(mutex, &session->mutex) {
> + session->state = LUO_SESSION_CLOSED;
> + luo_file_unpreserve_files(&session->file_set);
> + }
> +
> + down_write(&sh->rwsem);
> + if (atomic_dec_and_test(&sh->closing))
> + wake_up_all(&sh->reboot_waitq);
> + up_write(&sh->rwsem);
> +
> + luo_session_put(session);
> + luo_session_put(session);
> return 0;
> }
>
> @@ -231,10 +385,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;
>
> @@ -252,6 +412,11 @@ static int luo_session_retrieve_fd(struct luo_session *session,
> struct file *file;
> int err;
>
> + scoped_guard(mutex, &session->mutex) {
> + if (session->state != LUO_SESSION_RETRIEVED)
> + return -EINVAL;
> + }
> +
> argp->fd = get_unused_fd_flags(O_CLOEXEC);
> if (argp->fd < 0)
> return argp->fd;
> @@ -281,8 +446,9 @@ 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);
> + int err;
>
> + err = luo_session_finish_one(session);
> if (err)
> return err;
>
> @@ -371,9 +537,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;
>
> @@ -403,7 +572,7 @@ int luo_session_create(const char *name, struct file **filep)
> err_remove:
> luo_session_remove(&luo_session_global.outgoing, session);
> err_free:
> - luo_session_free(session);
> + luo_session_put(session);
>
> return err;
> }
> @@ -418,6 +587,7 @@ 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))) {
> + luo_session_get(it);
> session = it;
> break;
> }
> @@ -428,12 +598,14 @@ int luo_session_retrieve(const char *name, struct file **filep)
> return -ENOENT;
>
> guard(mutex)(&session->mutex);
> - if (session->retrieved)
> - return -EINVAL;
> + if (session->state != LUO_SESSION_INCOMING)
> + err = -EINVAL;
> + else
> + err = luo_session_getfile(session, filep);
>
> - err = luo_session_getfile(session, filep);
> if (!err)
> - session->retrieved = true;
> + session->state = LUO_SESSION_RETRIEVED;
> + luo_session_put(session);
>
> return err;
> }
> @@ -548,6 +720,7 @@ int luo_session_deserialize(void)
> sh->ser[i].name, session);
> return PTR_ERR(session);
> }
> + session->state = LUO_SESSION_INCOMING;
>
> err = luo_session_insert(sh, session);
> if (err) {
> @@ -578,6 +751,17 @@ int luo_session_serialize(void)
> int err;
>
> guard(rwsem_write)(&sh->rwsem);
> + if (READ_ONCE(sh->rebooting))
> + return -EBUSY;
> +
> + while (atomic_read(&sh->closing)) {
> + up_write(&sh->rwsem);
> + wait_event(sh->reboot_waitq, !atomic_read(&sh->closing));
> + down_write(&sh->rwsem);
> + if (READ_ONCE(sh->rebooting))
> + return -EBUSY;
> + }
> + WRITE_ONCE(sh->rebooting, true);
> list_for_each_entry(session, &sh->list, list) {
> err = luo_session_freeze_one(session, &sh->ser[i]);
> if (err)
> @@ -595,8 +779,11 @@ int luo_session_serialize(void)
> 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));
> + memset(&sh->ser[i], 0, sizeof(sh->ser[i]));
> }
> + sh->header_ser->count = 0;
> + /* Reset rebooting flag on serialization failure. */
> + luo_session_reboot_done(sh);
>
> return err;
> }
> @@ -624,7 +811,8 @@ bool luo_session_quiesce(void)
> down_write(&luo_session_global.outgoing.rwsem);
>
> if (luo_session_global.incoming.count ||
> - luo_session_global.outgoing.count) {
> + luo_session_global.outgoing.count ||
> + atomic_read(&luo_session_global.outgoing.closing)) {
> up_write(&luo_session_global.outgoing.rwsem);
> up_write(&luo_session_global.incoming.rwsem);
> return false;
> --
> 2.53.0
>
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
@ 2026-03-21 23:05 ` Pasha Tatashin
2026-03-23 14:12 ` Pasha Tatashin
1 sibling, 1 reply; 12+ messages in thread
From: Pasha Tatashin @ 2026-03-21 23:05 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Sat, Mar 21, 2026 at 10:39 AM Oskar Gerlicz Kowalczuk
<oskar@gerlicz.space> wrote:
>
> 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>
AFAIK, preserve-context reboot is no longer used; in fact, it is
currently broken. I tried using it six years ago and hit an old
spinlock bug in the x86 code that prevented it, which means this path
touches dead code.
It was originally developed for machines lacking hardware support for
hibernation, and I can't imagine anyone using it today. (I know Amazon
is using it for a non intended reason and that is a very specific
scenario in a costumized kernel that internally fixes the upstream
bugs, and would not conlict with live update).
During live update, we cannot return to userspace after a successful
liveupdate_reboot(). We specifically place liveupdate_reboot() in the
kexec path as the last possible point of failure. Perhap, to guarantee
this behavior, we could add !KEXEC_JUMP as a dependency to KHO and
LUO or crash the kernel.
Pasha
> ---
> 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 | 23 +++++++++++++++++++++++
> 5 files changed, 44 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 0cfc0269d746..8a6b1f6c9b4f 100644
> --- a/kernel/liveupdate/luo_internal.h
> +++ b/kernel/liveupdate/luo_internal.h
> @@ -88,6 +88,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 d97ec91e1118..200dd3b8229c 100644
> --- a/kernel/liveupdate/luo_session.c
> +++ b/kernel/liveupdate/luo_session.c
> @@ -788,6 +788,29 @@ int luo_session_serialize(void)
> return err;
> }
>
> +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 [flat|nested] 12+ messages in thread
* Re: [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind
2026-03-21 23:05 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Pasha Tatashin
@ 2026-03-23 14:12 ` Pasha Tatashin
0 siblings, 0 replies; 12+ messages in thread
From: Pasha Tatashin @ 2026-03-23 14:12 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Sat, Mar 21, 2026 at 7:05 PM Pasha Tatashin
<pasha.tatashin@soleen.com> wrote:
>
> On Sat, Mar 21, 2026 at 10:39 AM Oskar Gerlicz Kowalczuk
> <oskar@gerlicz.space> wrote:
> >
> > 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>
>
> AFAIK, preserve-context reboot is no longer used; in fact, it is
> currently broken. I tried using it six years ago and hit an old
> spinlock bug in the x86 code that prevented it, which means this path
> touches dead code.
>
> It was originally developed for machines lacking hardware support for
> hibernation, and I can't imagine anyone using it today. (I know Amazon
> is using it for a non intended reason and that is a very specific
> scenario in a costumized kernel that internally fixes the upstream
> bugs, and would not conlict with live update).
>
> During live update, we cannot return to userspace after a successful
> liveupdate_reboot(). We specifically place liveupdate_reboot() in the
> kexec path as the last possible point of failure. Perhap, to guarantee
> this behavior, we could add !KEXEC_JUMP as a dependency to KHO and
> LUO or crash the kernel.
So, I think the right approach to address this issue is simply skip
liveupdate_reboot() call when we are doing preserve context kexec.
With preserve context we are going to get back to the current kernel,
there is absolutly no need to do serialization. So something like this
should work:
https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/commit/?h=luo-reboot-sync/rfc/1&id=b13cf19ebbf2f8483a3c5a61a7358611b391edf6
>
> Pasha
>
> > ---
> > 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 | 23 +++++++++++++++++++++++
> > 5 files changed, 44 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 0cfc0269d746..8a6b1f6c9b4f 100644
> > --- a/kernel/liveupdate/luo_internal.h
> > +++ b/kernel/liveupdate/luo_internal.h
> > @@ -88,6 +88,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 d97ec91e1118..200dd3b8229c 100644
> > --- a/kernel/liveupdate/luo_session.c
> > +++ b/kernel/liveupdate/luo_session.c
> > @@ -788,6 +788,29 @@ int luo_session_serialize(void)
> > return err;
> > }
> >
> > +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 [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
2026-03-21 22:28 ` Pasha Tatashin
@ 2026-03-23 19:00 ` Pasha Tatashin
2026-03-23 20:52 ` oskar
0 siblings, 1 reply; 12+ messages in thread
From: Pasha Tatashin @ 2026-03-23 19:00 UTC (permalink / raw)
To: Oskar Gerlicz Kowalczuk
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Sat, Mar 21, 2026 at 6:28 PM Pasha Tatashin
<pasha.tatashin@soleen.com> wrote:
>
> On Sat, Mar 21, 2026 at 10:38 AM Oskar Gerlicz Kowalczuk
> <oskar@gerlicz.space> wrote:
> >
> > kernel_kexec() serializes outgoing sessions before the reboot path
> > freezes tasks, so close() and session ioctls can still mutate a
> > session while handover state is being prepared. The original v2 code
> > also let incoming lookups keep a bare session pointer after dropping
> > the list lock.
> >
> > That leaves two correctness problems in the reboot path: outgoing state
> > can change after serialization starts, and incoming sessions can be
> > freed while another thread still holds a pointer to them.
> >
> > Add refcounted session lifetime management, track in-flight outgoing
> > close() paths with an atomic closing counter, and make serialization
> > wait for closing to drain before setting rebooting. Reject phase-invalid
> > ioctls, keep incoming release on a common cleanup path, and make the
> > release wait freezable without spinning.
> >
> > Fixes: fc5acd5c89fe ("liveupdate: block outgoing session updates during reboot")
> > Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
> > ---
> > kernel/liveupdate/luo_internal.h | 12 +-
> > kernel/liveupdate/luo_session.c | 236 +++++++++++++++++++++++++++----
> > 2 files changed, 221 insertions(+), 27 deletions(-)
>
> Hi Oskar,
>
> Thank you for sending this series and finding these bugs in LUO. I
> agree with Andrew that a cover letter would help to understand the
> summary of the overall effort.
>
> I have not reviewed the other patches yet, but for this patch, my
> understanding is that it solves two specific races during reboot()
> syscalls: session closure after serialization, and the addition of new
> sessions or preserving new files after serialization.
>
> Given that KHO is now stateless, and liveupdate_reboot() is
> specifically placed at the last point where we can still return an
> error to userspace, we should simply return an error if a userspace is
> doing something unexpected.
>
> Instead of creating a new state machine, let's just reuse the file
> references and simply take them for each session at the beginning of
> serialization. This ensures that no session closes will happen later.
> For file preservation and session addition, we can block them by
> simply adding a new boolean.
>
> Please take a look at the two patches below and see if this approach
> would work. It is a much smaller change compared to the proposed state
> machine in this patch.
>
> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/log/?h=luo-reboot-sync/rfc/1
Oskar, I made a few more changes to avoid returning an error if
get_file_active() fails. This prevents a race condition where the user
might call close(session_fd) right before calling reboot(). I
force-updated the above branch. Please let me know if you want to take
these changes and use them to in the next version.
Pasha
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
2026-03-23 19:00 ` Pasha Tatashin
@ 2026-03-23 20:52 ` oskar
2026-03-23 22:23 ` Pasha Tatashin
0 siblings, 1 reply; 12+ messages in thread
From: oskar @ 2026-03-23 20:52 UTC (permalink / raw)
To: Pasha Tatashin
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On 2026-03-23 20:00, Pasha Tatashin wrote:
> On Sat, Mar 21, 2026 at 6:28 PM Pasha Tatashin
> <pasha.tatashin@soleen.com> wrote:
>>
>> On Sat, Mar 21, 2026 at 10:38 AM Oskar Gerlicz Kowalczuk
>> <oskar@gerlicz.space> wrote:
>> >
>> > kernel_kexec() serializes outgoing sessions before the reboot path
>> > freezes tasks, so close() and session ioctls can still mutate a
>> > session while handover state is being prepared. The original v2 code
>> > also let incoming lookups keep a bare session pointer after dropping
>> > the list lock.
>> >
>> > That leaves two correctness problems in the reboot path: outgoing state
>> > can change after serialization starts, and incoming sessions can be
>> > freed while another thread still holds a pointer to them.
>> >
>> > Add refcounted session lifetime management, track in-flight outgoing
>> > close() paths with an atomic closing counter, and make serialization
>> > wait for closing to drain before setting rebooting. Reject phase-invalid
>> > ioctls, keep incoming release on a common cleanup path, and make the
>> > release wait freezable without spinning.
>> >
>> > Fixes: fc5acd5c89fe ("liveupdate: block outgoing session updates during reboot")
>> > Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
>> > ---
>> > kernel/liveupdate/luo_internal.h | 12 +-
>> > kernel/liveupdate/luo_session.c | 236 +++++++++++++++++++++++++++----
>> > 2 files changed, 221 insertions(+), 27 deletions(-)
>>
>> Hi Oskar,
>>
>> Thank you for sending this series and finding these bugs in LUO. I
>> agree with Andrew that a cover letter would help to understand the
>> summary of the overall effort.
>>
>> I have not reviewed the other patches yet, but for this patch, my
>> understanding is that it solves two specific races during reboot()
>> syscalls: session closure after serialization, and the addition of new
>> sessions or preserving new files after serialization.
>>
>> Given that KHO is now stateless, and liveupdate_reboot() is
>> specifically placed at the last point where we can still return an
>> error to userspace, we should simply return an error if a userspace is
>> doing something unexpected.
>>
>> Instead of creating a new state machine, let's just reuse the file
>> references and simply take them for each session at the beginning of
>> serialization. This ensures that no session closes will happen later.
>> For file preservation and session addition, we can block them by
>> simply adding a new boolean.
>>
>> Please take a look at the two patches below and see if this approach
>> would work. It is a much smaller change compared to the proposed state
>> machine in this patch.
>>
>> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/log/?h=luo-reboot-sync/rfc/1
>
> Oskar, I made a few more changes to avoid returning an error if
> get_file_active() fails. This prevents a race condition where the user
> might call close(session_fd) right before calling reboot(). I
> force-updated the above branch. Please let me know if you want to take
> these changes and use them to in the next version.
>
> Pasha
Hi Pasha,
thank you for taking the time to prototype this approach and for the
detailed explanation, I really appreciate it.
I agree that reusing file references and introducing a simple blocking
mechanism makes the solution much smaller and easier to reason about
compared to a dedicated state machine. Your patches definitely move
things in a nice direction in terms of simplicity.
While going through it, I was wondering if there might still be a couple
of corner cases worth discussing. In particular, do you think a boolean
gate is sufficient to cover in-flight operations that may have already
passed the check before serialization starts? It seems like those paths
could still potentially mutate session state during serialization.
I was also thinking about the lifetime of incoming sessions (especially
lookups holding pointers). Do you think file reference handling alone is
enough there, or would we still need some explicit lifetime protection?
I’m currently working on v4 and will take a closer look at your branch
to see if we can combine both approaches in a way that keeps the
solution simple while still covering these cases.
Thanks,
Oskar Gerlicz Kowalczuk
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot
2026-03-23 20:52 ` oskar
@ 2026-03-23 22:23 ` Pasha Tatashin
0 siblings, 0 replies; 12+ messages in thread
From: Pasha Tatashin @ 2026-03-23 22:23 UTC (permalink / raw)
To: oskar
Cc: Mike Rapoport, Baoquan He, Pratyush Yadav, Andrew Morton,
linux-kernel, kexec, linux-mm
On Mon, Mar 23, 2026 at 4:54 PM <oskar@gerlicz.space> wrote:
>
> On 2026-03-23 20:00, Pasha Tatashin wrote:
> > On Sat, Mar 21, 2026 at 6:28 PM Pasha Tatashin
> > <pasha.tatashin@soleen.com> wrote:
> >>
> >> On Sat, Mar 21, 2026 at 10:38 AM Oskar Gerlicz Kowalczuk
> >> <oskar@gerlicz.space> wrote:
> >> >
> >> > kernel_kexec() serializes outgoing sessions before the reboot path
> >> > freezes tasks, so close() and session ioctls can still mutate a
> >> > session while handover state is being prepared. The original v2 code
> >> > also let incoming lookups keep a bare session pointer after dropping
> >> > the list lock.
> >> >
> >> > That leaves two correctness problems in the reboot path: outgoing state
> >> > can change after serialization starts, and incoming sessions can be
> >> > freed while another thread still holds a pointer to them.
> >> >
> >> > Add refcounted session lifetime management, track in-flight outgoing
> >> > close() paths with an atomic closing counter, and make serialization
> >> > wait for closing to drain before setting rebooting. Reject phase-invalid
> >> > ioctls, keep incoming release on a common cleanup path, and make the
> >> > release wait freezable without spinning.
> >> >
> >> > Fixes: fc5acd5c89fe ("liveupdate: block outgoing session updates during reboot")
> >> > Signed-off-by: Oskar Gerlicz Kowalczuk <oskar@gerlicz.space>
> >> > ---
> >> > kernel/liveupdate/luo_internal.h | 12 +-
> >> > kernel/liveupdate/luo_session.c | 236 +++++++++++++++++++++++++++----
> >> > 2 files changed, 221 insertions(+), 27 deletions(-)
> >>
> >> Hi Oskar,
> >>
> >> Thank you for sending this series and finding these bugs in LUO. I
> >> agree with Andrew that a cover letter would help to understand the
> >> summary of the overall effort.
> >>
> >> I have not reviewed the other patches yet, but for this patch, my
> >> understanding is that it solves two specific races during reboot()
> >> syscalls: session closure after serialization, and the addition of new
> >> sessions or preserving new files after serialization.
> >>
> >> Given that KHO is now stateless, and liveupdate_reboot() is
> >> specifically placed at the last point where we can still return an
> >> error to userspace, we should simply return an error if a userspace is
> >> doing something unexpected.
> >>
> >> Instead of creating a new state machine, let's just reuse the file
> >> references and simply take them for each session at the beginning of
> >> serialization. This ensures that no session closes will happen later.
> >> For file preservation and session addition, we can block them by
> >> simply adding a new boolean.
> >>
> >> Please take a look at the two patches below and see if this approach
> >> would work. It is a much smaller change compared to the proposed state
> >> machine in this patch.
> >>
> >> https://git.kernel.org/pub/scm/linux/kernel/git/tatashin/linux.git/log/?h=luo-reboot-sync/rfc/1
> >
> > Oskar, I made a few more changes to avoid returning an error if
> > get_file_active() fails. This prevents a race condition where the user
> > might call close(session_fd) right before calling reboot(). I
> > force-updated the above branch. Please let me know if you want to take
> > these changes and use them to in the next version.
> >
> > Pasha
>
> Hi Pasha,
>
> thank you for taking the time to prototype this approach and for the
> detailed explanation, I really appreciate it.
>
> I agree that reusing file references and introducing a simple blocking
> mechanism makes the solution much smaller and easier to reason about
> compared to a dedicated state machine. Your patches definitely move
> things in a nice direction in terms of simplicity.
>
> While going through it, I was wondering if there might still be a couple
> of corner cases worth discussing. In particular, do you think a boolean
> gate is sufficient to cover in-flight operations that may have already
> passed the check before serialization starts? It seems like those paths
> could still potentially mutate session state during serialization.
I think it is robust, it works in conjucition with session mutex. If
an operation already passed the check, it already holds the session
mutex, and since serialization also takes this mutex, it will see
consistent data after pinning sessions via
luo_session_get_all_outgoing().
>
> I was also thinking about the lifetime of incoming sessions (especially
> lookups holding pointers). Do you think file reference handling alone is
> enough there, or would we still need some explicit lifetime protection?
I'm not sure about that; I have not looked into those patches in your
series yet.
>
> I’m currently working on v4 and will take a closer look at your branch
> to see if we can combine both approaches in a way that keeps the
> solution simple while still covering these cases.
Thanks!
Pasha
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-03-23 22:24 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-21 14:36 [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 3/5] liveupdate: fail session restore on file deserialization errors Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 4/5] liveupdate: validate handover metadata before using it Oskar Gerlicz Kowalczuk
2026-03-21 14:36 ` [PATCH v3 5/5] liveupdate: harden FLB lifetime and teardown paths Oskar Gerlicz Kowalczuk
2026-03-21 23:05 ` [PATCH v3 2/5] kexec: abort liveupdate handover on kernel_kexec() unwind Pasha Tatashin
2026-03-23 14:12 ` Pasha Tatashin
2026-03-21 17:45 ` [PATCH v3 1/5] liveupdate: block outgoing session updates during reboot Andrew Morton
2026-03-21 22:28 ` Pasha Tatashin
2026-03-23 19:00 ` Pasha Tatashin
2026-03-23 20:52 ` oskar
2026-03-23 22:23 ` Pasha Tatashin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox