* [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
@ 2026-10-15 9:38 Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early Pasha Tatashin
` (6 more replies)
0 siblings, 7 replies; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
This series allows preserving files in tmpfs via LUO to survive across a
live update, and allows userspace to decide when to freeze a session
instead of delaying until auto-freeze at kexec time. Once userspace is
done with a session, it can freeze the session immediately (and across
multiple sessions in parallel), reducing blackout time by not postponing
this operation to kexec shutdown where all sessions are frozen serially:
1. Early session freeze (LIVEUPDATE_SESSION_FREEZE):
Currently, LUO sessions are frozen automatically only inside the
reboot(LINUX_REBOOT_CMD_KEXEC) path, where luo_session_serialize()
iterates through all outgoing sessions under global write locks and
freezes them serially during shutdown.
Add the LIVEUPDATE_SESSION_FREEZE ioctl on session file descriptors so
userspace can freeze a session as soon as it is done with it (and freeze
multiple sessions concurrently, since luo_session_freeze() only acquires
the per-session mutex). During kexec, already-frozen sessions skip the
late auto-freeze step; if an early-frozen session is closed before
kexec, its files are rolled back via .unfreeze() before being
unpreserved.
2. Per-file user argument on preserve and retrieve:
File handlers may need handler-specific context when preserving or
retrieving a file descriptor without introducing custom session ioctls
for each file type. Whether the argument is required, optional, or
unused depends on the file handler.
Extend struct liveupdate_session_preserve_fd and
struct liveupdate_session_retrieve_fd with a 64-bit 'arg' field pointing
to a handler-defined argument structure that begins with a 32-bit 'size'
field. Because IOCTL_OP validates a minimum struct size up to 'token',
existing userspace callers passing the 16-byte structure remain
backward-compatible and have 'arg' zero-filled by
copy_struct_from_user().
Add parg and rarg (struct liveupdate_arg_spec) to
struct liveupdate_file_ops, initialized via LIVEUPDATE_ARG(_struct,
_min_last), which records the full struct size and minimum required
size while enforcing at compile time that the argument structure begins
with a 32-bit 'size' field. Core LUO validates the minimum size and
copies non-zero user arguments via copy_struct_from_user() up to the
handler's expected size (or rejects them with -EOPNOTSUPP when the
handler does not accept an argument) and passes the kernel copy to
.preserve() and .retrieve() in struct liveupdate_file_op_args. Also
allow .retrieve() callbacks to mark early argument/target validation
failures as retryable so userspace can retry
LIVEUPDATE_SESSION_RETRIEVE_FD without losing preserved KHO state.
3. Tmpfs file preservation and retrieval:
Using the file handler retrieve argument, extend mm/memfd_luo to allow
preserving generic shmem-backed tmpfs files (not just unlinked memfd
pseudofiles), recording MEMFD_LUO_FLAG_TMPFS in
struct memfd_luo_ser.flags so that preserved tmpfs files must be
retrieved as tmpfs files and preserved memfd files must be retrieved as
memfd files. Introduce struct liveupdate_memfd_retrieve_arg so
userspace can pass a writable, empty, linked tmpfs file descriptor via
the retrieve argument and have the kernel populate its address space
with the preserved folios.
David Matlack (2):
mm/memfd_luo: Support tmpfs preservation and retrieval
selftests: liveupdate: Add test coverage for tmpfs preservation and
retrieval
Pasha Tatashin (5):
liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions
early
selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE
liveupdate: Pass user argument to file preserve and retrieve
selftests: liveupdate: Add tests for preserve and retrieve user
argument
liveupdate: Allow file handlers to mark retrieve failures as retryable
include/linux/kho/abi/memfd.h | 13 +-
include/linux/liveupdate.h | 74 ++++++++-
include/uapi/linux/liveupdate.h | 59 ++++++-
kernel/liveupdate/luo_file.c | 152 ++++++++++++++----
kernel/liveupdate/luo_internal.h | 8 +-
kernel/liveupdate/luo_session.c | 58 ++++++-
mm/memfd_luo.c | 144 +++++++++++++----
.../liveupdate/lib/include/libliveupdate.h | 6 +
.../selftests/liveupdate/lib/lu_utils.c | 71 +++++++-
.../testing/selftests/liveupdate/liveupdate.c | 84 ++++++++++
.../selftests/liveupdate/luo_kexec_simple.c | 5 +
.../selftests/liveupdate/luo_multi_session.c | 110 +++++++++++--
12 files changed, 695 insertions(+), 89 deletions(-)
base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:47 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE Pasha Tatashin
` (5 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
Currently, LUO sessions are frozen automatically only inside the
reboot(LINUX_REBOOT_CMD_KEXEC) pathway, where luo_session_serialize()
iterates through all outgoing sessions under global write locks and
freezes them serially during shutdown. Allowing userspace to decide when
to freeze a session once it is done with it (and to freeze multiple
sessions in parallel, since luo_session_freeze() only acquires the
per-session mutex) avoids postponing this work to kexec shutdown and
reduces blackout time.
Introduce LIVEUPDATE_SESSION_FREEZE on session file descriptors to freeze
a session early. The ioctl runs .freeze() across all preserved files in the
session and serializes their metadata. Once frozen, no additional files can
be added to the session, and subsequent freeze calls return -EBUSY. Freeze
is only valid on outgoing sessions and returns -EINVAL on incoming ones.
During kexec, sessions that were already frozen early are skipped, while
sessions that did not invoke this ioctl auto-freeze as before. If an
early-frozen session is released before kexec, its files are rolled back
via .unfreeze() before being unpreserved.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
include/linux/liveupdate.h | 3 +-
include/uapi/linux/liveupdate.h | 25 +++++++++++++++
kernel/liveupdate/luo_file.c | 33 +++++++++++---------
kernel/liveupdate/luo_internal.h | 3 ++
kernel/liveupdate/luo_session.c | 52 +++++++++++++++++++++++++++++---
mm/memfd_luo.c | 5 +--
6 files changed, 99 insertions(+), 22 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 63ea5417de849..885ec0bbc4b8c 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -57,7 +57,8 @@ struct liveupdate_file_op_args {
* compatible with the given file.
* @preserve: Required. Performs state-saving for the file.
* @unpreserve: Required. Cleans up any resources allocated by @preserve.
- * @freeze: Optional. Final actions just before kernel transition.
+ * @freeze: Optional. Prepares final state before kexec (via
+ * LIVEUPDATE_SESSION_FREEZE or during reboot).
* @unfreeze: Optional. Undo freeze operations.
* @retrieve: Required. Restores the file in the new kernel.
* @can_finish: Optional. Check if this FD can finish, i.e. all restoration
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 4043d40387125..1644c502521e0 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -60,6 +60,7 @@ enum {
LIVEUPDATE_CMD_SESSION_RETRIEVE_FD = 0x41,
LIVEUPDATE_CMD_SESSION_FINISH = 0x42,
LIVEUPDATE_CMD_SESSION_GET_NAME = 0x43,
+ LIVEUPDATE_CMD_SESSION_FREEZE = 0x44,
};
/**
@@ -236,4 +237,28 @@ struct liveupdate_session_get_name {
#define LIVEUPDATE_SESSION_GET_NAME \
_IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_GET_NAME)
+/**
+ * struct liveupdate_session_freeze - ioctl(LIVEUPDATE_SESSION_FREEZE)
+ * @size: Input; sizeof(struct liveupdate_session_freeze)
+ * @reserved: Input; Must be zero. Reserved for future use.
+ *
+ * Freezes the session early from userspace before the reboot kexec phase.
+ *
+ * When this ioctl is called, the kernel executes the .freeze() callback on all
+ * preserved files in the session and serializes their metadata. Once frozen,
+ * no further files can be preserved in the session.
+ *
+ * During the subsequent reboot kexec, this session is recognized as already
+ * frozen and is skipped from the late auto freeze.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+struct liveupdate_session_freeze {
+ __u32 size;
+ __u32 reserved;
+};
+
+#define LIVEUPDATE_SESSION_FREEZE \
+ _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_FREEZE)
+
#endif /* _UAPI_LIVEUPDATE_H */
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index c39f96961a85b..3c1c86ff14d69 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -31,9 +31,9 @@
* - unpreserve(): Cleans up any resources allocated by .preserve(), called
* if the preservation process is aborted before the reboot (i.e. session is
* closed).
- * - freeze(): A final pre-reboot opportunity to prepare the state for kexec.
- * We are already in reboot syscall, and therefore userspace cannot mutate
- * the file anymore.
+ * - freeze(): Prepares the final state for kexec. Invoked either early via
+ * the LIVEUPDATE_SESSION_FREEZE ioctl once userspace is done modifying
+ * the session's files, or during the reboot() syscall.
* - unfreeze(): Undoes the actions of .freeze(), called if the live update
* is aborted after the freeze phase.
* - retrieve(): Reconstructs the file in the new kernel from the preserved
@@ -48,7 +48,8 @@
* handler, calls its .preserve() operation, and creates an internal &struct
* luo_file to track the live state.
*
- * 2. Freeze (Pre-Reboot): Just before the kexec, luo_file_freeze() is called.
+ * 2. Freeze (Pre-Reboot): Either explicitly via LIVEUPDATE_SESSION_FREEZE or
+ * just before the kexec during reboot(), luo_file_freeze() is called.
* It iterates through all preserved files, calls their respective .freeze()
* operation, and serializes their final metadata (compatible string, token,
* and data handle) into a contiguous memory block for KHO.
@@ -422,11 +423,11 @@ static void __luo_file_unfreeze(struct luo_file_set *file_set,
/**
* luo_file_freeze - Freezes all preserved files and serializes their metadata.
* @file_set: The file_set whose files are to be frozen.
- * @file_set_ser: Where to put the serialized file_set.
+ * @file_set_ser: Where to put the serialized file_set, or %NULL if freezing early.
*
- * This function is called from the reboot() syscall path, just before the
- * kernel transitions to the new image via kexec. Its purpose is to perform the
- * final preparation and serialization of all preserved files in the file_set.
+ * This function is called from the reboot() syscall path or from
+ * LIVEUPDATE_SESSION_FREEZE. Its purpose is to perform the preparation and
+ * serialization of all preserved files in the file_set.
*
* It iterates through each preserved file in FIFO order (the order of
* preservation) and performs two main actions:
@@ -449,7 +450,7 @@ static void __luo_file_unfreeze(struct luo_file_set *file_set,
* running state. The function then returns an error, causing the reboot()
* syscall to fail.
*
- * Context: Called only from the liveupdate_reboot() path.
+ * Context: Called from the liveupdate_reboot() path or LIVEUPDATE_SESSION_FREEZE.
* Return: 0 on success, or a negative errno on failure.
*/
int luo_file_freeze(struct luo_file_set *file_set,
@@ -487,8 +488,10 @@ int luo_file_freeze(struct luo_file_set *file_set,
file_ser->token = luo_file->token;
}
- file_set_ser->count = file_set->count;
- file_set_ser->files = kho_block_set_head_pa(&file_set->block_set);
+ if (file_set_ser) {
+ file_set_ser->count = file_set->count;
+ file_set_ser->files = kho_block_set_head_pa(&file_set->block_set);
+ }
return 0;
@@ -501,7 +504,7 @@ int luo_file_freeze(struct luo_file_set *file_set,
/**
* luo_file_unfreeze - Unfreezes all files in a file_set and clear serialization
* @file_set: The file_set whose files are to be unfrozen.
- * @file_set_ser: Serialized file_set.
+ * @file_set_ser: Serialized file_set, or %NULL if not serialized yet.
*
* This function rolls back the state of all files in a file_set after the
* freeze phase has begun but must be aborted. It is the counterpart to
@@ -512,7 +515,8 @@ int luo_file_freeze(struct luo_file_set *file_set,
* their respective .unfreeze() handler callbacks.
*
* Context: This is called when the live update is aborted during
- * the reboot() syscall, after luo_file_freeze() has been called.
+ * the reboot() syscall, after luo_file_freeze() has been called,
+ * or when an early-frozen session is released before kexec.
*/
void luo_file_unfreeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser)
@@ -521,7 +525,8 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
return;
__luo_file_unfreeze(file_set, NULL);
- memset(file_set_ser, 0, sizeof(*file_set_ser));
+ if (file_set_ser)
+ memset(file_set_ser, 0, sizeof(*file_set_ser));
}
/**
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index 64879ffe73781..b42369c3b1f30 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -64,6 +64,8 @@ struct luo_file_set {
* previous kernel) sessions.
* @retrieved: A boolean flag indicating whether this session has been
* retrieved by a consumer in the new kernel.
+ * @frozen: A boolean flag indicating whether this session has been
+ * frozen early from userspace.
* @file_set: A set of files that belong to this session.
* @mutex: protects fields in the luo_session.
*/
@@ -71,6 +73,7 @@ struct luo_session {
char name[LIVEUPDATE_SESSION_NAME_LENGTH];
struct list_head list;
bool retrieved;
+ bool frozen;
struct luo_file_set file_set;
struct mutex mutex;
};
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index f48e9a4185f97..403906937bc0f 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -34,10 +34,11 @@
* 1. Creation: A userspace agent calls `luo_session_create()` to create a
* new, empty session and receives a file descriptor for it.
*
- * 2. Serialization: When the `reboot(LINUX_REBOOT_CMD_KEXEC)` syscall is
- * made, `luo_session_serialize()` is called. It iterates through all
- * active sessions and writes their metadata into a memory area preserved
- * by KHO.
+ * 2. Freeze / Serialization: Sessions may be frozen early from userspace
+ * via `LIVEUPDATE_SESSION_FREEZE`. When the `reboot(LINUX_REBOOT_CMD_KEXEC)`
+ * syscall is made, `luo_session_serialize()` is called to freeze any
+ * remaining active sessions and write all session metadata into a memory
+ * area preserved by KHO.
*
* 3. Deserialization (in new kernel): After kexec, `luo_session_deserialize()`
* runs, reading the serialized data and creating a list of `struct
@@ -222,6 +223,10 @@ static void luo_session_unfreeze_one(struct luo_session *session,
struct luo_session_ser *ser)
{
guard(mutex)(&session->mutex);
+ if (session->frozen) {
+ memset(&ser->file_set_ser, 0, sizeof(ser->file_set_ser));
+ return;
+ }
luo_file_unfreeze(&session->file_set, &ser->file_set_ser);
}
@@ -229,6 +234,12 @@ static int luo_session_freeze_one(struct luo_session *session,
struct luo_session_ser *ser)
{
guard(mutex)(&session->mutex);
+ if (session->frozen) {
+ ser->file_set_ser.count = session->file_set.count;
+ ser->file_set_ser.files =
+ kho_block_set_head_pa(&session->file_set.block_set);
+ return 0;
+ }
return luo_file_freeze(&session->file_set, &ser->file_set_ser);
}
@@ -249,8 +260,11 @@ static int luo_session_release(struct inode *inodep, struct file *filep)
}
sh = &luo_session_global.incoming;
} else {
- scoped_guard(mutex, &session->mutex)
+ scoped_guard(mutex, &session->mutex) {
+ if (session->frozen)
+ luo_file_unfreeze(&session->file_set, NULL);
luo_file_unpreserve_files(&session->file_set);
+ }
sh = &luo_session_global.outgoing;
}
@@ -267,6 +281,9 @@ static int luo_session_preserve_fd(struct luo_session *session,
int err;
guard(mutex)(&session->mutex);
+ if (session->frozen)
+ return -EBUSY;
+
err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
if (err)
return err;
@@ -340,11 +357,34 @@ static int luo_session_get_name(struct luo_session *session,
return luo_ucmd_respond(ucmd, sizeof(*argp));
}
+static int luo_session_freeze(struct luo_session *session,
+ struct luo_ucmd *ucmd)
+{
+ struct liveupdate_session_freeze *argp = ucmd->cmd;
+ int err;
+
+ if (argp->reserved)
+ return -EINVAL;
+
+ guard(mutex)(&session->mutex);
+ if (session->frozen)
+ return -EBUSY;
+
+ err = luo_file_freeze(&session->file_set, NULL);
+ if (err)
+ return err;
+
+ session->frozen = true;
+
+ return luo_ucmd_respond(ucmd, sizeof(*argp));
+}
+
union ucmd_buffer {
struct liveupdate_session_finish finish;
struct liveupdate_session_preserve_fd preserve;
struct liveupdate_session_retrieve_fd retrieve;
struct liveupdate_session_get_name get_name;
+ struct liveupdate_session_freeze freeze;
};
/* Type of sessions the ioctl applies to. */
@@ -382,6 +422,8 @@ static const struct luo_ioctl_op luo_session_ioctl_ops[] = {
struct liveupdate_session_retrieve_fd, token, LUO_IOCTL_INCOMING),
IOCTL_OP(LIVEUPDATE_SESSION_GET_NAME, luo_session_get_name,
struct liveupdate_session_get_name, name, LUO_IOCTL_ALL),
+ IOCTL_OP(LIVEUPDATE_SESSION_FREEZE, luo_session_freeze,
+ struct liveupdate_session_freeze, reserved, LUO_IOCTL_OUTGOING),
};
static bool luo_ioctl_type_valid(struct luo_session *session,
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 59de210bee5f9..26929495786e0 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -43,8 +43,9 @@
* allocating pages for them during preservation.
*
* File Position
- * The current file position is preserved, allowing applications to continue
- * reading/writing from their last position.
+ * The file position at the time the session is frozen is preserved,
+ * allowing applications to continue reading/writing from their last
+ * position.
*
* File Status Flags
* memfds are always opened with ``O_RDWR`` and ``O_LARGEFILE``. This property
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:45 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve Pasha Tatashin
` (4 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
Provide luo_session_freeze() in the liveupdate test library and add unit
tests in liveupdate.c covering the LIVEUPDATE_SESSION_FREEZE ioctl. The
tests verify that an outgoing session with preserved files can be frozen
early, that freezing an already frozen session returns -EBUSY, and that
attempting to preserve an additional file descriptor into a frozen session
fails with -EBUSY.
Also freeze the test session early in luo_kexec_simple.c to exercise
LIVEUPDATE_SESSION_FREEZE across a kexec reboot.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
.../liveupdate/lib/include/libliveupdate.h | 1 +
.../selftests/liveupdate/lib/lu_utils.c | 10 +++++
.../testing/selftests/liveupdate/liveupdate.c | 40 +++++++++++++++++++
.../selftests/liveupdate/luo_kexec_simple.c | 5 +++
4 files changed, 56 insertions(+)
diff --git a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
index fa07fed083646..15a7db3a6b3e8 100644
--- a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
+++ b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
@@ -26,6 +26,7 @@ int luo_create_session(int luo_fd, const char *name);
int luo_retrieve_session(int luo_fd, const char *name);
int luo_session_finish(int session_fd);
int luo_get_session_name(int session_fd, char *name, size_t name_len);
+int luo_session_freeze(int session_fd);
int luo_ensure_nofile_limit(long min_limit);
int luo_session_preserve_fd(int session_fd, int fd, __u64 token);
diff --git a/tools/testing/selftests/liveupdate/lib/lu_utils.c b/tools/testing/selftests/liveupdate/lib/lu_utils.c
index 74d41115c2815..6315746ff08f8 100644
--- a/tools/testing/selftests/liveupdate/lib/lu_utils.c
+++ b/tools/testing/selftests/liveupdate/lib/lu_utils.c
@@ -198,6 +198,16 @@ int luo_session_finish(int session_fd)
return 0;
}
+int luo_session_freeze(int session_fd)
+{
+ struct liveupdate_session_freeze arg = { .size = sizeof(arg) };
+
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_FREEZE, &arg) < 0)
+ return -errno;
+
+ return 0;
+}
+
void create_state_file(int luo_fd, const char *session_name, int token,
int next_stage)
{
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 2dedd5fc2534f..9de068f31c3ec 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -530,4 +530,44 @@ TEST_F(liveupdate_device, preserve_many_files)
ASSERT_EQ(close(session_fd), 0);
}
+/*
+ * Test Case: Session Freeze
+ *
+ * Verifies that an outgoing session can be frozen early via
+ * LIVEUPDATE_SESSION_FREEZE, that subsequent freeze attempts return -EBUSY,
+ * and that attempting to preserve an additional file descriptor into a frozen
+ * session fails with -EBUSY.
+ */
+TEST_F(liveupdate_device, session_freeze)
+{
+ int session_fd, mem_fd1, mem_fd2;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = luo_create_session(self->fd1, "session-freeze-test");
+ ASSERT_GE(session_fd, 0);
+
+ mem_fd1 = memfd_create("freeze-memfd-1", 0);
+ ASSERT_GE(mem_fd1, 0);
+ ASSERT_EQ(luo_session_preserve_fd(session_fd, mem_fd1, 0x1111), 0);
+
+ /* Freeze the session early */
+ ASSERT_EQ(luo_session_freeze(session_fd), 0);
+
+ /* Subsequent freeze attempt on an already frozen session must fail with -EBUSY */
+ EXPECT_EQ(luo_session_freeze(session_fd), -EBUSY);
+
+ /* Preserving a file after freeze must fail with -EBUSY */
+ mem_fd2 = memfd_create("freeze-memfd-2", 0);
+ ASSERT_GE(mem_fd2, 0);
+ EXPECT_EQ(luo_session_preserve_fd(session_fd, mem_fd2, 0x2222), -EBUSY);
+
+ ASSERT_EQ(close(session_fd), 0);
+ ASSERT_EQ(close(mem_fd1), 0);
+ ASSERT_EQ(close(mem_fd2), 0);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/liveupdate/luo_kexec_simple.c b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
index 786ac93b9ae38..ab108c7f1d855 100644
--- a/tools/testing/selftests/liveupdate/luo_kexec_simple.c
+++ b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
@@ -40,6 +40,11 @@ static void run_stage_1(int luo_fd)
TEST_MEMFD_TOKEN);
}
+ ksft_print_msg("[STAGE 1] Freezing session '%s' early...\n",
+ TEST_SESSION_NAME);
+ if (luo_session_freeze(session_fd) < 0)
+ fail_exit("luo_session_freeze for '%s'", TEST_SESSION_NAME);
+
close(luo_fd);
daemonize_and_wait();
}
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:49 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument Pasha Tatashin
` (3 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
Allow userspace to pass a file-handler-specific argument structure when
preserving or retrieving an FD via LUO. File handlers may need
additional context during preservation or retrieval, such as a target
FD, device location, or handler-specific parameters, without introducing
separate ioctls for each file type. Whether the argument is required,
optional, or unused is defined by the file handler.
Extend struct liveupdate_session_preserve_fd and
struct liveupdate_session_retrieve_fd with a 64-bit arg field pointing
to a handler-defined argument structure that begins with a 32-bit size
field. Because IOCTL_OP validates a minimum size up to the token field,
existing callers passing the 16-byte struct remain compatible and have
arg zero-filled by copy_struct_from_user().
Add parg and rarg (struct liveupdate_arg_spec) to
struct liveupdate_file_ops, initialized via LIVEUPDATE_ARG(_struct,
_min_last), which records the full struct size and minimum required size
while enforcing at compile time that handler argument structures begin
with a 32-bit size field. When arg is non-zero, LUO validates the
minimum size and copies the user argument via copy_struct_from_user() up
to the handler's expected size and passes the kernel copy in
struct liveupdate_file_op_args, or returns -EOPNOTSUPP if the handler
does not accept an argument.
Also provide liveupdate_claim_file() and liveupdate_unclaim_file() so
handlers whose .retrieve() callback restores state into an existing
user-supplied file descriptor can claim it in luo_preserved_files before
modifying the file.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
include/linux/liveupdate.h | 60 +++++++++++++++++++
include/uapi/linux/liveupdate.h | 6 ++
kernel/liveupdate/luo_file.c | 99 +++++++++++++++++++++++++++++---
kernel/liveupdate/luo_internal.h | 5 +-
kernel/liveupdate/luo_session.c | 6 +-
5 files changed, 164 insertions(+), 12 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 885ec0bbc4b8c..52d3f453ae011 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -22,6 +22,36 @@ struct liveupdate_flb;
struct liveupdate_session;
struct file;
+/**
+ * struct liveupdate_arg_spec - Specification of a file handler user argument.
+ * @size: Full size of the argument structure in the current kernel.
+ * @min_size: Minimum valid user-supplied size (offsetofend of the last
+ * required field).
+ */
+struct liveupdate_arg_spec {
+ size_t size;
+ size_t min_size;
+};
+
+/**
+ * LIVEUPDATE_ARG - Validated specification of a file handler argument struct.
+ * @_struct: The argument structure type.
+ * @_min_last: The last required member in @_struct.
+ *
+ * All file handler argument structures passed via
+ * &liveupdate_session_preserve_fd.arg or &liveupdate_session_retrieve_fd.arg
+ * must begin with a 32-bit @size field so they can be extended compatibly.
+ * Use this macro to initialize &liveupdate_file_ops.parg and
+ * &liveupdate_file_ops.rarg.
+ */
+#define LIVEUPDATE_ARG(_struct, _min_last) \
+ { \
+ .size = sizeof(_struct) + \
+ BUILD_BUG_ON_ZERO(offsetof(_struct, size) != 0) + \
+ BUILD_BUG_ON_ZERO(sizeof_field(_struct, size) != sizeof(u32)), \
+ .min_size = offsetofend(_struct, _min_last), \
+ }
+
/**
* struct liveupdate_file_op_args - Arguments for file operation callbacks.
* @handler: The file handler being called.
@@ -39,6 +69,11 @@ struct file;
* is not preserved. Set by the handler's .preserve()
* callback, and must be freed in the handler's
* .unpreserve() callback.
+ * @arg: Kernel copy of the user-provided argument for .preserve()
+ * and .retrieve(), or %NULL if none was provided. Allocated
+ * to the handler's expected argument size with any trailing
+ * bytes beyond the user-supplied size zero-filled. Valid
+ * only for the duration of the callback.
*
* This structure bundles all parameters for the file operation callbacks.
* The 'data' and 'file' fields are used for both input and output.
@@ -49,6 +84,7 @@ struct liveupdate_file_op_args {
struct file *file;
u64 serialized_data;
void *private_data;
+ const void *arg;
};
/**
@@ -67,6 +103,12 @@ struct liveupdate_file_op_args {
* resources in the session.
* @finish: Required. Final cleanup in the new kernel.
* @get_id: Optional. Returns a unique identifier for the file.
+ * @parg: Optional. Specification of the user argument structure
+ * accepted by @preserve, initialized via LIVEUPDATE_ARG(). If
+ * unset, non-zero user arguments are rejected with -EOPNOTSUPP.
+ * @rarg: Optional. Specification of the user argument structure
+ * accepted by @retrieve, initialized via LIVEUPDATE_ARG(). If
+ * unset, non-zero user arguments are rejected with -EOPNOTSUPP.
* @owner: Module reference
*
* All operations (except can_preserve) receive a pointer to a
@@ -83,6 +125,8 @@ struct liveupdate_file_ops {
bool (*can_finish)(struct liveupdate_file_op_args *args);
void (*finish)(struct liveupdate_file_op_args *args);
unsigned long (*get_id)(struct file *file);
+ struct liveupdate_arg_spec parg;
+ struct liveupdate_arg_spec rarg;
struct module *owner;
};
@@ -237,6 +281,11 @@ int liveupdate_reboot(void);
int liveupdate_register_file_handler(struct liveupdate_file_handler *fh);
void liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh);
+int liveupdate_claim_file(struct liveupdate_file_handler *fh,
+ struct file *file);
+void liveupdate_unclaim_file(struct liveupdate_file_handler *fh,
+ struct file *file);
+
int liveupdate_register_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb);
void liveupdate_unregister_flb(struct liveupdate_file_handler *fh,
@@ -269,6 +318,17 @@ static inline void liveupdate_unregister_file_handler(struct liveupdate_file_han
{
}
+static inline int liveupdate_claim_file(struct liveupdate_file_handler *fh,
+ struct file *file)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline void liveupdate_unclaim_file(struct liveupdate_file_handler *fh,
+ struct file *file)
+{
+}
+
static inline int liveupdate_register_flb(struct liveupdate_file_handler *fh,
struct liveupdate_flb *flb)
{
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 1644c502521e0..57a3c129d5a9d 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -127,6 +127,8 @@ struct liveupdate_ioctl_retrieve_session {
* @size: Input; sizeof(struct liveupdate_session_preserve_fd)
* @fd: Input; The user-space file descriptor to be preserved.
* @token: Input; An opaque, unique token for preserved resource.
+ * @arg: Input; User-space pointer to a file-handler-specific argument
+ * structure starting with a 32-bit size field, or 0 if unused.
*
* Holds parameters for preserving a file descriptor.
*
@@ -150,6 +152,7 @@ struct liveupdate_session_preserve_fd {
__u32 size;
__s32 fd;
__aligned_u64 token;
+ __aligned_u64 arg;
};
#define LIVEUPDATE_SESSION_PRESERVE_FD \
@@ -161,6 +164,8 @@ struct liveupdate_session_preserve_fd {
* @fd: Output; The new file descriptor representing the fully restored
* kernel resource.
* @token: Input; An opaque, token that was used to preserve the resource.
+ * @arg: Input; User-space pointer to a file-handler-specific argument
+ * structure starting with a 32-bit size field, or 0 if unused.
*
* Retrieve a previously preserved file descriptor.
*
@@ -180,6 +185,7 @@ struct liveupdate_session_retrieve_fd {
__u32 size;
__s32 fd;
__aligned_u64 token;
+ __aligned_u64 arg;
};
#define LIVEUPDATE_SESSION_RETRIEVE_FD \
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 3c1c86ff14d69..e3bfdbc6baedc 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -112,6 +112,7 @@
#include <linux/xarray.h>
#include <linux/slab.h>
#include <linux/string.h>
+#include <linux/uaccess.h>
#include "luo_internal.h"
static LIST_HEAD(luo_file_handler_list);
@@ -176,6 +177,36 @@ static unsigned long luo_get_id(struct liveupdate_file_handler *fh,
return fh->ops->get_id ? fh->ops->get_id(file) : (unsigned long)file;
}
+/**
+ * liveupdate_claim_file - Claim a file in the LUO preserved files table.
+ * @fh: The file handler claiming @file.
+ * @file: The file to claim.
+ *
+ * File handlers whose .retrieve() callback restores state into an existing
+ * user-supplied &struct file (rather than allocating a new one) should call
+ * this before modifying @file to ensure @file is not already managed by LUO.
+ *
+ * Return: 0 on success, -EBUSY if @file is already managed by LUO, or a
+ * negative errno on allocation failure.
+ */
+int liveupdate_claim_file(struct liveupdate_file_handler *fh,
+ struct file *file)
+{
+ return xa_insert(&luo_preserved_files, luo_get_id(fh, file),
+ file, GFP_KERNEL);
+}
+
+/**
+ * liveupdate_unclaim_file - Release a claimed file from the LUO preserved files table.
+ * @fh: The file handler that claimed @file.
+ * @file: The file to release.
+ */
+void liveupdate_unclaim_file(struct liveupdate_file_handler *fh,
+ struct file *file)
+{
+ xa_erase(&luo_preserved_files, luo_get_id(fh, file));
+}
+
static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
{
struct luo_file *iter;
@@ -188,11 +219,45 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
return false;
}
+static int luo_copy_user_arg(u64 user_arg, const void **argp,
+ const struct liveupdate_arg_spec *spec)
+{
+ void *karg __free(kfree) = NULL;
+ void __user *uptr;
+ u32 user_size;
+ int err;
+
+ if (!user_arg)
+ return 0;
+ if (!spec->size)
+ return -EOPNOTSUPP;
+
+ uptr = u64_to_user_ptr(user_arg);
+ if (get_user(user_size, (u32 __user *)uptr))
+ return -EFAULT;
+ if (user_size < spec->min_size)
+ return -EINVAL;
+ if (user_size > PAGE_SIZE)
+ return -E2BIG;
+
+ karg = kmalloc(spec->size, GFP_KERNEL);
+ if (!karg)
+ return -ENOMEM;
+
+ err = copy_struct_from_user(karg, spec->size, uptr, user_size);
+ if (err)
+ return err;
+
+ *argp = no_free_ptr(karg);
+ return 0;
+}
+
/**
* luo_preserve_file - Initiate the preservation of a file descriptor.
* @file_set: The file_set to which the preserved file will be added.
* @token: A unique, user-provided identifier for the file.
* @fd: The file descriptor to be preserved.
+ * @arg: User-space pointer to the handler argument structure, or 0.
*
* This function orchestrates the first phase of preserving a file. Upon entry,
* it takes a reference to the 'struct file' via fget(), effectively making LUO
@@ -208,9 +273,11 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
* (allocates if needed).
* 3. Iterates through registered handlers, calling can_preserve() to find one
* compatible with the given @fd.
- * 4. Calls the handler's .preserve() operation, which saves the file's state
+ * 4. Copies the user argument structure (if @arg is non-zero) according to the
+ * handler's parg specification.
+ * 5. Calls the handler's .preserve() operation, which saves the file's state
* and returns an opaque private data handle.
- * 5. Adds the new instance to the file_set's internal list.
+ * 6. Adds the new instance to the file_set's internal list.
*
* On success, LUO takes a reference to the 'struct file' and considers it
* under its management until it is unpreserved or finished.
@@ -225,10 +292,12 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
* -EBADF if the file descriptor is invalid.
* -ENOSPC if the file_set is full.
* -ENOENT if no compatible handler is found.
+ * -EOPNOTSUPP if @arg is non-zero and the handler does not accept an arg.
* -ENOMEM on memory allocation failure.
* Other erros might be returned by .preserve().
*/
-int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd)
+int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd,
+ u64 arg)
{
struct liveupdate_file_op_args args = {0};
struct liveupdate_file_handler *fh;
@@ -264,10 +333,14 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd)
if (err)
goto err_fput;
+ err = luo_copy_user_arg(arg, &args.arg, &fh->ops->parg);
+ if (err)
+ goto err_module_put;
+
err = xa_insert(&luo_preserved_files, luo_get_id(fh, file),
file, GFP_KERNEL);
if (err)
- goto err_module_put;
+ goto err_free_arg;
err = luo_flb_file_preserve(fh);
if (err)
@@ -290,6 +363,7 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd)
if (err)
goto err_kfree;
+ kfree(args.arg);
luo_file->serialized_data = args.serialized_data;
luo_file->private_data = args.private_data;
list_add_tail(&luo_file->list, &file_set->files_list);
@@ -303,6 +377,8 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd)
luo_flb_file_unpreserve(fh);
err_erase_xa:
xa_erase(&luo_preserved_files, luo_get_id(fh, file));
+err_free_arg:
+ kfree(args.arg);
err_module_put:
module_put(fh->ops->owner);
err_fput:
@@ -533,6 +609,7 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
* luo_retrieve_file - Restores a preserved file from a file_set by its token.
* @file_set: The file_set from which to retrieve the file.
* @token: The unique token identifying the file to be restored.
+ * @arg: User-space pointer to the handler argument structure, or 0.
* @filep: Output parameter; on success, this is populated with a pointer
* to the newly retrieved 'struct file'.
*
@@ -552,9 +629,10 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
* kernel.
* Return: 0 on success. Returns a negative errno on failure:
* -ENOENT if no file with the matching token is found.
+ * -EOPNOTSUPP if @arg is non-zero and the handler does not accept an arg.
* Any error code returned by the handler's .retrieve() op.
*/
-int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
+int luo_retrieve_file(struct luo_file_set *file_set, u64 token, u64 arg,
struct file **filep)
{
struct liveupdate_file_op_args args = {0};
@@ -591,9 +669,14 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
return 0;
}
+ err = luo_copy_user_arg(arg, &args.arg, &luo_file->fh->ops->rarg);
+ if (err)
+ return err;
+
args.handler = luo_file->fh;
args.serialized_data = luo_file->serialized_data;
err = luo_file->fh->ops->retrieve(&args);
+ kfree(args.arg);
if (err) {
/* Keep the error code for later use. */
luo_file->retrieve_status = err;
@@ -604,9 +687,9 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
/* Get reference so we can keep this file in LUO until finish */
get_file(luo_file->file);
- WARN_ON(xa_insert(&luo_preserved_files,
- luo_get_id(luo_file->fh, luo_file->file),
- luo_file->file, GFP_KERNEL));
+ if (xa_load(&luo_preserved_files,
+ luo_get_id(luo_file->fh, luo_file->file)) != luo_file->file)
+ WARN_ON(liveupdate_claim_file(luo_file->fh, luo_file->file));
*filep = luo_file->file;
luo_file->retrieve_status = 1;
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index b42369c3b1f30..3bd9ef442934e 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -87,13 +87,14 @@ int __init luo_session_setup_incoming(u64 sessions_pa);
int luo_session_serialize(void);
int luo_session_deserialize(void);
-int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd);
+int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd,
+ u64 arg);
void luo_file_unpreserve_files(struct luo_file_set *file_set);
int luo_file_freeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
void luo_file_unfreeze(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
-int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
+int luo_retrieve_file(struct luo_file_set *file_set, u64 token, u64 arg,
struct file **filep);
int luo_file_finish(struct luo_file_set *file_set);
int luo_file_deserialize(struct luo_file_set *file_set,
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 403906937bc0f..74f8a1bf1da8c 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -284,7 +284,8 @@ static int luo_session_preserve_fd(struct luo_session *session,
if (session->frozen)
return -EBUSY;
- err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
+ err = luo_preserve_file(&session->file_set, argp->token, argp->fd,
+ argp->arg);
if (err)
return err;
@@ -307,7 +308,8 @@ static int luo_session_retrieve_fd(struct luo_session *session,
return argp->fd;
mutex_lock(&session->mutex);
- err = luo_retrieve_file(&session->file_set, argp->token, &file);
+ err = luo_retrieve_file(&session->file_set, argp->token, argp->arg,
+ &file);
mutex_unlock(&session->mutex);
if (err < 0)
goto err_put_fd;
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
` (2 preceding siblings ...)
2026-10-15 9:38 ` [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:45 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable Pasha Tatashin
` (2 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
Verify the user argument support in LIVEUPDATE_SESSION_PRESERVE_FD and
LIVEUPDATE_SESSION_RETRIEVE_FD. Add luo_session_preserve_fd_arg() and
luo_session_retrieve_fd_arg() helpers to libliveupdate so tests can pass
a handler-specific argument.
Add a preserve_memfd_with_arg test case to verify that memfd
preservation rejects a non-zero argument with -EOPNOTSUPP, accepts an
explicit zero argument, and remains compatible with callers passing the
legacy struct size up to the token field.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
.../liveupdate/lib/include/libliveupdate.h | 3 ++
.../selftests/liveupdate/lib/lu_utils.c | 17 ++++++-
.../testing/selftests/liveupdate/liveupdate.c | 44 +++++++++++++++++++
3 files changed, 62 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
index 15a7db3a6b3e8..8a38268e89dee 100644
--- a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
+++ b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
@@ -30,7 +30,10 @@ int luo_session_freeze(int session_fd);
int luo_ensure_nofile_limit(long min_limit);
int luo_session_preserve_fd(int session_fd, int fd, __u64 token);
+int luo_session_preserve_fd_arg(int session_fd, int fd, __u64 token,
+ __u64 user_arg);
int luo_session_retrieve_fd(int session_fd, __u64 token);
+int luo_session_retrieve_fd_arg(int session_fd, __u64 token, __u64 user_arg);
int create_and_preserve_memfd(int session_fd, int token, const char *data);
int restore_and_verify_memfd(int session_fd, int token, const char *expected_data);
diff --git a/tools/testing/selftests/liveupdate/lib/lu_utils.c b/tools/testing/selftests/liveupdate/lib/lu_utils.c
index 6315746ff08f8..5c6f8bf80b297 100644
--- a/tools/testing/selftests/liveupdate/lib/lu_utils.c
+++ b/tools/testing/selftests/liveupdate/lib/lu_utils.c
@@ -79,12 +79,14 @@ int luo_retrieve_session(int luo_fd, const char *name)
return arg.fd;
}
-int luo_session_preserve_fd(int session_fd, int fd, __u64 token)
+int luo_session_preserve_fd_arg(int session_fd, int fd, __u64 token,
+ __u64 user_arg)
{
struct liveupdate_session_preserve_fd arg = {
.size = sizeof(arg),
.fd = fd,
.token = token,
+ .arg = user_arg,
};
if (ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &arg))
@@ -93,11 +95,17 @@ int luo_session_preserve_fd(int session_fd, int fd, __u64 token)
return 0;
}
-int luo_session_retrieve_fd(int session_fd, __u64 token)
+int luo_session_preserve_fd(int session_fd, int fd, __u64 token)
+{
+ return luo_session_preserve_fd_arg(session_fd, fd, token, 0);
+}
+
+int luo_session_retrieve_fd_arg(int session_fd, __u64 token, __u64 user_arg)
{
struct liveupdate_session_retrieve_fd arg = {
.size = sizeof(arg),
.token = token,
+ .arg = user_arg,
};
if (ioctl(session_fd, LIVEUPDATE_SESSION_RETRIEVE_FD, &arg))
@@ -106,6 +114,11 @@ int luo_session_retrieve_fd(int session_fd, __u64 token)
return arg.fd;
}
+int luo_session_retrieve_fd(int session_fd, __u64 token)
+{
+ return luo_session_retrieve_fd_arg(session_fd, token, 0);
+}
+
/* Helper function to get a session name via ioctl. */
int luo_get_session_name(int session_fd, char *name, size_t name_len)
{
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 9de068f31c3ec..83e0978f3818d 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -20,6 +20,7 @@
#include <errno.h>
#include <fcntl.h>
+#include <stddef.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
@@ -570,4 +571,47 @@ TEST_F(liveupdate_device, session_freeze)
ASSERT_EQ(close(mem_fd2), 0);
}
+/*
+ * Test Case: Preserve MemFD with User Argument
+ *
+ * Verifies that passing a non-zero user argument to memfd preservation fails
+ * with -EOPNOTSUPP, while passing a zero argument or using the minimum struct
+ * size without the arg field succeeds.
+ */
+TEST_F(liveupdate_device, preserve_memfd_with_arg)
+{
+ struct liveupdate_session_preserve_fd min_arg = {};
+ int session_fd, mem_fd1, mem_fd2;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = luo_create_session(self->fd1, "preserve-arg-test");
+ ASSERT_GE(session_fd, 0);
+
+ mem_fd1 = memfd_create("arg-memfd-1", 0);
+ ASSERT_GE(mem_fd1, 0);
+
+ /* Non-zero arg is not supported by memfd handler */
+ EXPECT_EQ(luo_session_preserve_fd_arg(session_fd, mem_fd1, 0x1111, 1),
+ -EOPNOTSUPP);
+
+ /* Legacy struct size (up to token) should zero-fill arg and succeed */
+ min_arg.size = offsetof(struct liveupdate_session_preserve_fd, arg);
+ min_arg.fd = mem_fd1;
+ min_arg.token = 0x1111;
+ ASSERT_EQ(ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &min_arg), 0);
+
+ /* Explicit zero arg with full struct size should also succeed */
+ mem_fd2 = memfd_create("arg-memfd-2", 0);
+ ASSERT_GE(mem_fd2, 0);
+ ASSERT_EQ(luo_session_preserve_fd_arg(session_fd, mem_fd2, 0x2222, 0), 0);
+
+ ASSERT_EQ(close(session_fd), 0);
+ ASSERT_EQ(close(mem_fd1), 0);
+ ASSERT_EQ(close(mem_fd2), 0);
+}
+
TEST_HARNESS_MAIN
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
` (3 preceding siblings ...)
2026-10-15 9:38 ` [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:46 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 7/7] selftests: liveupdate: Add test coverage for " Pasha Tatashin
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
When luo_retrieve_file() invokes a handler's .retrieve() callback and it
returns an error, LUO currently records the error in
luo_file->retrieve_status and blocks any subsequent retrieve attempts.
This is necessary when the handler has already partially consumed or
freed the preserved KHO state.
However, with user arguments passed to .retrieve(), a handler may fail
early while validating caller-supplied parameters (such as a missing
argument, an invalid or read-only target file descriptor, or a non-empty
target file) before touching any preserved KHO state. In such cases,
userspace should be able to fix the argument and retry
LIVEUPDATE_SESSION_RETRIEVE_FD, and if the file is never retrieved,
.finish() must still run to release the preserved KHO state.
Add a retryable output flag to struct liveupdate_file_op_args. When a
handler sets args->retryable = true on error, luo_retrieve_file() leaves
luo_file->retrieve_status at 0 so the retrieve operation can be retried
and .finish() will still clean up the unconsumed preserved state if the
session is finished without retrieving the file.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
include/linux/liveupdate.h | 11 ++++++++---
include/uapi/linux/liveupdate.h | 7 ++++---
kernel/liveupdate/luo_file.c | 20 ++++++++++++++------
3 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 52d3f453ae011..4e1f0f4cbd6fb 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -57,9 +57,13 @@ struct liveupdate_arg_spec {
* @handler: The file handler being called.
* @retrieve_status: The retrieve status for the 'can_finish / finish'
* operation. A value of 0 means the retrieve has not been
- * attempted, a positive value means the retrieve was
- * successful, and a negative value means the retrieve failed,
- * and the value is the error code of the call.
+ * attempted (or only failed with a retryable error), a
+ * positive value means the retrieve was successful, and a
+ * negative value means the retrieve permanently failed, and
+ * the value is the error code of the call.
+ * @retryable: For .retrieve(): [OUT] Set to true by the callback on
+ * error when the preserved state has not been consumed and
+ * retrieval may be retried.
* @file: The file object. For retrieve: [OUT] The callback sets
* this to the new file. For other ops: [IN] The caller sets
* this to the file being operated on.
@@ -81,6 +85,7 @@ struct liveupdate_arg_spec {
struct liveupdate_file_op_args {
struct liveupdate_file_handler *handler;
int retrieve_status;
+ bool retryable;
struct file *file;
u64 serialized_data;
void *private_data;
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 57a3c129d5a9d..0ab28fd58fa5f 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -175,9 +175,10 @@ struct liveupdate_session_preserve_fd {
* associated with the token and populates the @fd field with a new file
* descriptor referencing the restored resource in the current (new) kernel.
* This operation must be performed *before* signaling completion via
- * %LIVEUPDATE_IOCTL_FINISH. If a retrieve of a token fails, subsequent
- * attempts to retrieve the token fail with the same error code. Failed
- * retrieves are not retried.
+ * %LIVEUPDATE_IOCTL_FINISH. If a retrieve of a token fails after consuming
+ * preserved state, subsequent attempts to retrieve the token fail with the
+ * same error code. Early argument or target validation errors before
+ * preserved state is consumed leave the token intact and may be retried.
*
* Return: 0 on success, negative error code on failure (e.g., invalid token).
*/
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index e3bfdbc6baedc..5c2ffec696885 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -138,9 +138,10 @@ static DEFINE_XARRAY(luo_preserved_files);
* @retrieve_status: Status code indicating whether a user/kernel in the new kernel has
* successfully called retrieve() on this file. This prevents
* multiple retrieval attempts. A value of 0 means a retrieve()
- * has not been attempted, a positive value means the retrieve()
- * was successful, and a negative value means the retrieve()
- * failed, and the value is the error code of the call.
+ * has not been attempted (or only failed with a retryable
+ * error), a positive value means the retrieve() was successful,
+ * and a negative value means the retrieve() permanently failed,
+ * and the value is the error code of the call.
* @mutex: A mutex that protects the fields of this specific instance
* (e.g., @retrieved, @file), ensuring that operations like
* retrieving or finishing a file are atomic.
@@ -620,7 +621,9 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
* The operation is idempotent: if a file has already been successfully
* retrieved, this function will simply return a pointer to the existing
* 'struct file' and report success without re-executing the retrieve
- * operation. This is handled by checking the 'retrieved' flag under a lock.
+ * operation. If a previous retrieve failed permanently, it returns that
+ * saved error code; if a previous retrieve failed with a retryable error
+ * before consuming preserved state, the retrieve operation is re-attempted.
*
* File retrieval can happen in any order; it is not bound by the order of
* preservation.
@@ -678,8 +681,13 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token, u64 arg,
err = luo_file->fh->ops->retrieve(&args);
kfree(args.arg);
if (err) {
- /* Keep the error code for later use. */
- luo_file->retrieve_status = err;
+ /*
+ * Keep the error code for later use unless the handler marked
+ * the failure as retryable (e.g. invalid user argument before
+ * preserved state was consumed).
+ */
+ if (!args.retryable)
+ luo_file->retrieve_status = err;
return err;
}
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
` (4 preceding siblings ...)
2026-10-15 9:38 ` [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:54 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 7/7] selftests: liveupdate: Add test coverage for " Pasha Tatashin
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
From: David Matlack <dmatlack@google.com>
Relax memfd_luo_can_preserve() to allow general shmem-backed files beyond
unlinked memfd pseudofiles, enabling userspace to preserve standard
tmpfs files across kexec.
Record MEMFD_LUO_FLAG_TMPFS in struct memfd_luo_ser.flags when a linked
tmpfs file is preserved so that tmpfs files must be retrieved as tmpfs
files and anonymous memfd files must be retrieved as memfd files.
To support restoring preserved tmpfs folios to a newly created tmpfs
file on the post-kexec filesystem, introduce struct
liveupdate_memfd_retrieve_arg in the UAPI and wire it up via .rarg on
memfd_luo_file_ops. When retrieving a preserved tmpfs file,
memfd_luo_retrieve() requires this argument, validates that the
specified file descriptor is a writable, empty, linked tmpfs file not
already claimed by LUO, locks its inode, and populates its address space
with the preserved folios. Validation failures on the user-provided
retrieve argument are marked retryable so userspace can retry
LIVEUPDATE_SESSION_RETRIEVE_FD without losing the preserved state.
Signed-off-by: David Matlack <dmatlack@google.com>
Co-developed-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
include/linux/kho/abi/memfd.h | 13 ++-
include/uapi/linux/liveupdate.h | 21 +++++
mm/memfd_luo.c | 139 +++++++++++++++++++++++++-------
3 files changed, 141 insertions(+), 32 deletions(-)
diff --git a/include/linux/kho/abi/memfd.h b/include/linux/kho/abi/memfd.h
index 08b10fea2afc8..47ff5ab967501 100644
--- a/include/linux/kho/abi/memfd.h
+++ b/include/linux/kho/abi/memfd.h
@@ -68,7 +68,18 @@ struct memfd_luo_folio_ser {
F_SEAL_EXEC)
/**
- * struct memfd_luo_ser - Main serialization structure for a memfd.
+ * MEMFD_LUO_FLAG_TMPFS - The preserved file is a tmpfs file.
+ *
+ * Indicates the file was preserved from a linked tmpfs file rather than an
+ * anonymous memfd, and must be retrieved to an empty tmpfs file.
+ */
+#define MEMFD_LUO_FLAG_TMPFS BIT(0)
+
+/* The set of flags this version supports preserving. */
+#define MEMFD_LUO_ALL_FLAGS MEMFD_LUO_FLAG_TMPFS
+
+/**
+ * struct memfd_luo_ser - Main serialization structure for a memfd or tmpfs file.
* @pos: The file's current position (f_pos).
* @size: The total size of the file in bytes (i_size).
* @seals: The seals present on the memfd. The seals are uABI so it is safe
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 0ab28fd58fa5f..24e938d91392c 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -268,4 +268,25 @@ struct liveupdate_session_freeze {
#define LIVEUPDATE_SESSION_FREEZE \
_IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_FREEZE)
+/**
+ * struct liveupdate_memfd_retrieve_arg - Argument for tmpfs file retrieval
+ * @size: Input; sizeof(struct liveupdate_memfd_retrieve_arg)
+ * @fd: Input; Writable, empty, linked tmpfs file descriptor to restore into.
+ *
+ * Passed via &struct liveupdate_session_retrieve_fd.arg on
+ * %LIVEUPDATE_SESSION_RETRIEVE_FD. This argument is required when retrieving
+ * a preserved linked tmpfs file and is rejected with -EINVAL when retrieving
+ * an anonymous memfd.
+ *
+ * @fd must refer to a writable, empty, linked tmpfs file that is not already
+ * claimed by LUO. On success, the file is populated with the preserved folios,
+ * its size and f_pos are restored, and the file descriptor returned in
+ * &struct liveupdate_session_retrieve_fd.fd references the same underlying open
+ * file description as @fd.
+ */
+struct liveupdate_memfd_retrieve_arg {
+ __u32 size;
+ __s32 fd;
+};
+
#endif /* _UAPI_LIVEUPDATE_H */
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 26929495786e0..9d18f01191c07 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -9,14 +9,14 @@
*/
/**
- * DOC: Memfd Preservation via LUO
+ * DOC: Memfd and Tmpfs Preservation via LUO
*
* Overview
* ========
*
- * Memory file descriptors (memfd) can be preserved over a kexec using the Live
- * Update Orchestrator (LUO) file preservation. This allows userspace to
- * transfer its memory contents to the next kernel after a kexec.
+ * Memory file descriptors and generic tmpfs files can be preserved over a
+ * kexec using the LUO file preservation. This allows userspace to transfer its
+ * memory contents to the next kernel after a kexec.
*
* The preservation is not intended to be transparent. Only select properties of
* the file are preserved. All others are reset to default. The preserved
@@ -24,16 +24,17 @@
*
* .. note::
* The LUO API is not stabilized yet, so the preserved properties of a memfd
- * are also not stable and are subject to backwards incompatible changes.
+ * or tmpfs file are also not stable and are subject to backwards
+ * incompatible changes.
*
* .. note::
- * Currently a memfd backed by Hugetlb is not supported. Memfds created
+ * Currently a file backed by Hugetlb is not supported. Memfds created
* with ``MFD_HUGETLB`` will be rejected.
*
* Preserved Properties
* ====================
*
- * The following properties of the memfd are preserved across kexec:
+ * The following properties of the memfd/tmpfs file are preserved across kexec:
*
* File Contents
* All data stored in the file is preserved.
@@ -300,6 +301,8 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
ser->size = inode_size;
ser->seals = seals;
+ if (inode->i_nlink)
+ ser->flags |= MEMFD_LUO_FLAG_TMPFS;
err = memfd_luo_preserve_folios(args->file, &ser->folios,
&folios_ser, &nr_folios);
@@ -516,10 +519,60 @@ static int memfd_luo_retrieve_folios(struct file *file,
return err;
}
+static struct file *memfd_luo_get_tmpfs_file(struct liveupdate_file_op_args *args)
+{
+ const struct liveupdate_memfd_retrieve_arg *rarg = args->arg;
+ struct inode *inode;
+ struct file *file;
+ int err;
+
+ if (!rarg)
+ return ERR_PTR(-EINVAL);
+
+ file = fget(rarg->fd);
+ if (!file)
+ return ERR_PTR(-EBADF);
+
+ if (!(file->f_mode & FMODE_WRITE)) {
+ err = -EACCES;
+ goto put_file;
+ }
+
+ if (!shmem_file(file)) {
+ err = -EINVAL;
+ goto put_file;
+ }
+
+ inode = file_inode(file);
+ inode_lock(inode);
+ if (!inode->i_nlink) {
+ err = -EINVAL;
+ goto unlock_inode;
+ }
+
+ if (i_size_read(inode) != 0 || !mapping_empty(inode->i_mapping)) {
+ err = -EBUSY;
+ goto unlock_inode;
+ }
+
+ err = liveupdate_claim_file(args->handler, file);
+ if (err)
+ goto unlock_inode;
+
+ return file;
+
+unlock_inode:
+ inode_unlock(inode);
+put_file:
+ fput(file);
+ return ERR_PTR(err);
+}
+
static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
{
struct memfd_luo_folio_ser *folios_ser;
struct memfd_luo_ser *ser;
+ struct inode *inode;
struct file *file;
int err;
@@ -527,50 +580,75 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
if (!ser)
return -EINVAL;
- /* Make sure the file only has seals supported by this version. */
- if (ser->seals & ~MEMFD_LUO_ALL_SEALS) {
+ /* Make sure the file only has seals and flags supported by this version. */
+ if ((ser->seals & ~MEMFD_LUO_ALL_SEALS) ||
+ (ser->flags & ~MEMFD_LUO_ALL_FLAGS)) {
err = -EOPNOTSUPP;
goto free_ser;
}
- /*
- * The seals are preserved. Allow sealing here so they can be added
- * later.
- */
- file = memfd_alloc_file("", MFD_ALLOW_SEALING);
- if (IS_ERR(file)) {
- pr_err("failed to setup file: %pe\n", file);
- err = PTR_ERR(file);
- goto free_ser;
- }
+ if (ser->flags & MEMFD_LUO_FLAG_TMPFS) {
+ file = memfd_luo_get_tmpfs_file(args);
+ if (IS_ERR(file)) {
+ args->retryable = true;
+ return PTR_ERR(file);
+ }
+ inode = file_inode(file);
+ } else {
+ if (args->arg) {
+ args->retryable = true;
+ return -EINVAL;
+ }
- err = memfd_add_seals(file, ser->seals);
- if (err) {
- pr_err("failed to add seals: %pe\n", ERR_PTR(err));
- goto put_file;
- }
+ /*
+ * The seals are preserved. Allow sealing here so they can be
+ * added later.
+ */
+ file = memfd_alloc_file("", MFD_ALLOW_SEALING);
+ if (IS_ERR(file)) {
+ pr_err("failed to setup file: %pe\n", file);
+ err = PTR_ERR(file);
+ goto free_ser;
+ }
- vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
- i_size_write(file_inode(file), ser->size);
+ err = memfd_add_seals(file, ser->seals);
+ if (err) {
+ pr_err("failed to add seals: %pe\n", ERR_PTR(err));
+ goto put_file;
+ }
+
+ inode = file_inode(file);
+ inode_lock(inode);
+ }
if (ser->nr_folios) {
folios_ser = kho_restore_vmalloc(&ser->folios);
if (!folios_ser) {
err = -EINVAL;
- goto put_file;
+ goto unclaim_file;
}
err = memfd_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
vfree(folios_ser);
if (err)
- goto put_file;
+ goto unclaim_file;
}
+ vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
+ i_size_write(inode, ser->size);
+ inode_unlock(inode);
+
args->file = file;
kho_restore_free(ser);
return 0;
+unclaim_file:
+ if (ser->flags & MEMFD_LUO_FLAG_TMPFS) {
+ shmem_truncate_range(inode, 0, (loff_t)-1);
+ liveupdate_unclaim_file(args->handler, file);
+ }
+ inode_unlock(inode);
put_file:
fput(file);
free_ser:
@@ -581,9 +659,7 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
struct file *file)
{
- struct inode *inode = file_inode(file);
-
- return shmem_file(file) && !inode->i_nlink;
+ return shmem_file(file);
}
static unsigned long memfd_luo_get_id(struct file *file)
@@ -599,6 +675,7 @@ static const struct liveupdate_file_ops memfd_luo_file_ops = {
.unpreserve = memfd_luo_unpreserve,
.can_preserve = memfd_luo_can_preserve,
.get_id = memfd_luo_get_id,
+ .rarg = LIVEUPDATE_ARG(struct liveupdate_memfd_retrieve_arg, fd),
.owner = THIS_MODULE,
};
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v1 7/7] selftests: liveupdate: Add test coverage for tmpfs preservation and retrieval
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
` (5 preceding siblings ...)
2026-10-15 9:38 ` [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval Pasha Tatashin
@ 2026-10-15 9:38 ` Pasha Tatashin
2026-10-15 9:47 ` sashiko-bot
6 siblings, 1 reply; 15+ messages in thread
From: Pasha Tatashin @ 2026-10-15 9:38 UTC (permalink / raw)
To: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, pasha.tatashin, dmatlack, kexec, pratyush, graf
From: David Matlack <dmatlack@google.com>
Extend the multi-session LUO test to verify preserving a path-backed
tmpfs file and retrieving its contents to a newly created empty tmpfs
file post-kexec using struct liveupdate_memfd_retrieve_arg with
LIVEUPDATE_SESSION_RETRIEVE_FD. Also verify that retrieve argument
validation errors (passing an argument when retrieving a memfd, omitting
the argument when retrieving a tmpfs file, or passing a read-only target
tmpfs FD) are retryable and allow a subsequent valid retrieve call to
succeed.
Signed-off-by: David Matlack <dmatlack@google.com>
Co-developed-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
.../liveupdate/lib/include/libliveupdate.h | 2 +
.../selftests/liveupdate/lib/lu_utils.c | 44 +++++++
.../selftests/liveupdate/luo_multi_session.c | 110 ++++++++++++++++--
3 files changed, 147 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
index 8a38268e89dee..dc5c01408462d 100644
--- a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
+++ b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
@@ -37,6 +37,8 @@ int luo_session_retrieve_fd_arg(int session_fd, __u64 token, __u64 user_arg);
int create_and_preserve_memfd(int session_fd, int token, const char *data);
int restore_and_verify_memfd(int session_fd, int token, const char *expected_data);
+int restore_and_verify_tmpfs(int session_fd, int token, int fd,
+ const char *expected_data);
void create_state_file(int luo_fd, const char *session_name, int token,
int next_stage);
diff --git a/tools/testing/selftests/liveupdate/lib/lu_utils.c b/tools/testing/selftests/liveupdate/lib/lu_utils.c
index 5c6f8bf80b297..1b1a556287d3a 100644
--- a/tools/testing/selftests/liveupdate/lib/lu_utils.c
+++ b/tools/testing/selftests/liveupdate/lib/lu_utils.c
@@ -347,3 +347,47 @@ int luo_test(int argc, char *argv[],
return 0;
}
+
+int restore_and_verify_tmpfs(int session_fd, int token, int fd,
+ const char *expected_data)
+{
+ struct liveupdate_memfd_retrieve_arg rarg = {
+ .size = sizeof(rarg),
+ .fd = fd,
+ };
+ long page_size = getpagesize();
+ void *map = MAP_FAILED;
+ struct stat st;
+ int ret_fd;
+
+ ret_fd = luo_session_retrieve_fd_arg(session_fd, token,
+ (unsigned long)&rarg);
+ if (ret_fd < 0) {
+ ksft_print_msg("luo_session_retrieve_fd_arg failed: %d\n",
+ ret_fd);
+ return ret_fd;
+ }
+ close(ret_fd);
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+ if (st.st_size != page_size) {
+ ksft_print_msg("Size mismatch! Expected %ld, Got %lld\n",
+ page_size, (long long)st.st_size);
+ return -EINVAL;
+ }
+
+ map = mmap(NULL, page_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED)
+ return -errno;
+
+ if (expected_data && strcmp(expected_data, map) != 0) {
+ ksft_print_msg("Data mismatch! Expected '%s', Got '%s'\n",
+ expected_data, (char *)map);
+ munmap(map, page_size);
+ return -EINVAL;
+ }
+
+ munmap(map, page_size);
+ return 0;
+}
diff --git a/tools/testing/selftests/liveupdate/luo_multi_session.c b/tools/testing/selftests/liveupdate/luo_multi_session.c
index aac24a5f5ce3c..46889676d9f52 100644
--- a/tools/testing/selftests/liveupdate/luo_multi_session.c
+++ b/tools/testing/selftests/liveupdate/luo_multi_session.c
@@ -9,7 +9,15 @@
* files.
*/
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
#include <libliveupdate.h>
+#include <linux/liveupdate.h>
#define SESSION_EMPTY_1 "multi-test-empty-1"
#define SESSION_EMPTY_2 "multi-test-empty-2"
@@ -23,6 +31,7 @@
#define MFD1_DATA "Data for session files 1"
#define MFD2_DATA "First file for session files 2"
#define MFD3_DATA "Second file for session files 2"
+#define MFD3_POS 42
#define STATE_SESSION_NAME "kexec_multi_state"
#define STATE_MEMFD_TOKEN 998
@@ -31,6 +40,8 @@
static void run_stage_1(int luo_fd)
{
int s_empty1_fd, s_empty2_fd, s_files1_fd, s_files2_fd;
+ int tmpfs_fd;
+ void *map;
ksft_print_msg("[STAGE 1] Starting pre-kexec setup for multi-session test...\n");
@@ -53,25 +64,50 @@ static void run_stage_1(int luo_fd)
s_files1_fd = luo_create_session(luo_fd, SESSION_FILES_1);
if (s_files1_fd < 0)
fail_exit("luo_create_session for '%s'", SESSION_FILES_1);
+ ksft_print_msg(" -> Preserving memfd (token %#x, content: \"%s\")\n",
+ MFD1_TOKEN, MFD1_DATA);
if (create_and_preserve_memfd(s_files1_fd, MFD1_TOKEN, MFD1_DATA) < 0) {
fail_exit("create_and_preserve_memfd for token %#x",
MFD1_TOKEN);
}
- ksft_print_msg("[STAGE 1] Creating session '%s' with two memfds...\n",
+ ksft_print_msg("[STAGE 1] Creating session '%s' with memfd and tmpfs file...\n",
SESSION_FILES_2);
s_files2_fd = luo_create_session(luo_fd, SESSION_FILES_2);
if (s_files2_fd < 0)
fail_exit("luo_create_session for '%s'", SESSION_FILES_2);
+ ksft_print_msg(" -> Preserving memfd (token %#x, content: \"%s\")\n",
+ MFD2_TOKEN, MFD2_DATA);
if (create_and_preserve_memfd(s_files2_fd, MFD2_TOKEN, MFD2_DATA) < 0) {
fail_exit("create_and_preserve_memfd for token %#x",
MFD2_TOKEN);
}
- if (create_and_preserve_memfd(s_files2_fd, MFD3_TOKEN, MFD3_DATA) < 0) {
- fail_exit("create_and_preserve_memfd for token %#x",
- MFD3_TOKEN);
- }
+
+ ksft_print_msg(" -> Populating tmpfs file at /mnt/mfd3...\n");
+ if (mkdir("/mnt", 0777) < 0 && errno != EEXIST)
+ fail_exit("mkdir /mnt");
+ if (mount("tmpfs", "/mnt", "tmpfs", 0, NULL) < 0)
+ fail_exit("mount tmpfs");
+ tmpfs_fd = open("/mnt/mfd3", O_CREAT | O_RDWR, 0666);
+ if (tmpfs_fd < 0)
+ fail_exit("failed to open tmpfs_fd");
+ ksft_print_msg(" -> Preserving generic tmpfs file (token %#x, content: \"%s\")\n",
+ MFD3_TOKEN, MFD3_DATA);
+ if (ftruncate(tmpfs_fd, getpagesize()) < 0)
+ fail_exit("ftruncate tmpfs_fd");
+ map = mmap(NULL, getpagesize(), PROT_WRITE, MAP_SHARED, tmpfs_fd, 0);
+ if (map == MAP_FAILED)
+ fail_exit("mmap tmpfs_fd");
+ strcpy(map, MFD3_DATA);
+ munmap(map, getpagesize());
+ if (lseek(tmpfs_fd, MFD3_POS, SEEK_SET) != MFD3_POS)
+ fail_exit("lseek tmpfs_fd");
+
+ if (luo_session_preserve_fd(s_files2_fd, tmpfs_fd, MFD3_TOKEN) < 0)
+ fail_exit("preserve tmpfs fd");
+
+ close(tmpfs_fd);
close(luo_fd);
daemonize_and_wait();
@@ -81,7 +117,10 @@ static void run_stage_1(int luo_fd)
static void run_stage_2(int luo_fd, int state_session_fd)
{
int s_empty1_fd, s_empty2_fd, s_files1_fd, s_files2_fd;
- int mfd1, mfd2, mfd3, stage;
+ struct liveupdate_memfd_retrieve_arg rarg = {
+ .size = sizeof(rarg),
+ };
+ int mfd1, mfd2, mfd3, ro_fd, ret, stage;
ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
@@ -110,6 +149,8 @@ static void run_stage_2(int luo_fd, int state_session_fd)
ksft_print_msg("[STAGE 2] Verifying contents of session '%s'...\n",
SESSION_FILES_1);
+ ksft_print_msg(" -> Retrieving memfd (token %#x) from session...\n",
+ MFD1_TOKEN);
mfd1 = restore_and_verify_memfd(s_files1_fd, MFD1_TOKEN, MFD1_DATA);
if (mfd1 < 0)
fail_exit("restore_and_verify_memfd for token %#x", MFD1_TOKEN);
@@ -118,14 +159,65 @@ static void run_stage_2(int luo_fd, int state_session_fd)
ksft_print_msg("[STAGE 2] Verifying contents of session '%s'...\n",
SESSION_FILES_2);
+ ksft_print_msg(" -> Creating empty tmpfs file at /mnt/mfd3...\n");
+ if (mkdir("/mnt", 0777) < 0 && errno != EEXIST)
+ fail_exit("mkdir /mnt");
+ if (mount("tmpfs", "/mnt", "tmpfs", 0, NULL) < 0)
+ fail_exit("mount tmpfs");
+ mfd3 = open("/mnt/mfd3", O_CREAT | O_RDWR, 0666);
+ if (mfd3 < 0)
+ fail_exit("open tmpfs file");
+ ro_fd = open("/mnt/mfd3", O_RDONLY);
+ if (ro_fd < 0)
+ fail_exit("open read-only tmpfs file");
+
+ ksft_print_msg(" -> Verifying retryable retrieve errors for memfd and tmpfs...\n");
+ rarg.fd = mfd3;
+ ret = luo_session_retrieve_fd_arg(s_files2_fd, MFD2_TOKEN,
+ (uintptr_t)&rarg);
+ if (ret != -EINVAL)
+ fail_exit("expected -EINVAL when passing arg to memfd retrieve, got %d",
+ ret);
+
+ ksft_print_msg(" -> Retrieving memfd (token %#x) from session...\n",
+ MFD2_TOKEN);
mfd2 = restore_and_verify_memfd(s_files2_fd, MFD2_TOKEN, MFD2_DATA);
if (mfd2 < 0)
fail_exit("restore_and_verify_memfd for token %#x", MFD2_TOKEN);
close(mfd2);
- mfd3 = restore_and_verify_memfd(s_files2_fd, MFD3_TOKEN, MFD3_DATA);
- if (mfd3 < 0)
- fail_exit("restore_and_verify_memfd for token %#x", MFD3_TOKEN);
+ ret = luo_session_retrieve_fd(s_files2_fd, MFD3_TOKEN);
+ if (ret != -EINVAL)
+ fail_exit("expected -EINVAL when retrieving tmpfs without arg, got %d",
+ ret);
+
+ rarg.fd = ro_fd;
+ ret = luo_session_retrieve_fd_arg(s_files2_fd, MFD3_TOKEN,
+ (uintptr_t)&rarg);
+ if (ret != -EACCES)
+ fail_exit("expected -EACCES when retrieving tmpfs into read-only fd, got %d",
+ ret);
+ close(ro_fd);
+
+ if (ftruncate(mfd3, 1) < 0)
+ fail_exit("ftruncate non-empty tmpfs file");
+ rarg.fd = mfd3;
+ ret = luo_session_retrieve_fd_arg(s_files2_fd, MFD3_TOKEN,
+ (uintptr_t)&rarg);
+ if (ret != -EBUSY)
+ fail_exit("expected -EBUSY when retrieving tmpfs into non-empty fd, got %d",
+ ret);
+ if (ftruncate(mfd3, 0) < 0)
+ fail_exit("ftruncate empty tmpfs file");
+
+ ksft_print_msg(" -> Retrieving tmpfs file (token %#x) from session...\n",
+ MFD3_TOKEN);
+ if (restore_and_verify_tmpfs(s_files2_fd, MFD3_TOKEN, mfd3,
+ MFD3_DATA) < 0)
+ fail_exit("restore_and_verify_tmpfs for token %#x",
+ MFD3_TOKEN);
+ if (lseek(mfd3, 0, SEEK_CUR) != MFD3_POS)
+ fail_exit("expected f_pos %d on restored tmpfs file", MFD3_POS);
close(mfd3);
ksft_print_msg("[STAGE 2] Test data verified successfully.\n");
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument
2026-10-15 9:38 ` [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument Pasha Tatashin
@ 2026-10-15 9:45 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:45 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: missing retrieve user arg test case
--
commit 420b1c3465899a231e547b85211758efc34277b8
Author: Pasha Tatashin <pasha.tatashin@soleen.com>
Date: Thu Oct 15 09:38:13 2026 +0000
selftests: liveupdate: Add tests for preserve and retrieve user argument
This commit adds luo_session_preserve_fd_arg() and
luo_session_retrieve_fd_arg() helpers to libliveupdate to pass handler-specific
arguments. It also introduces a preserve_memfd_with_arg test case to verify
memfd preservation behavior with different user arguments.
> diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
> index 9de068f31c3ec..83e0978f3818d 100644
> --- a/tools/testing/selftests/liveupdate/liveupdate.c
> +++ b/tools/testing/selftests/liveupdate/liveupdate.c
[ ... ]
> @@ -570,4 +571,47 @@ TEST_F(liveupdate_device, session_freeze)
> ASSERT_EQ(close(mem_fd2), 0);
> }
>
> +/*
> + * Test Case: Preserve MemFD with User Argument
> + *
> + * Verifies that passing a non-zero user argument to memfd preservation fails
> + * with -EOPNOTSUPP, while passing a zero argument or using the minimum struct
> + * size without the arg field succeeds.
> + */
> +TEST_F(liveupdate_device, preserve_memfd_with_arg)
> +{
> + struct liveupdate_session_preserve_fd min_arg = {};
> + int session_fd, mem_fd1, mem_fd2;
> +
> + self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
> + if (self->fd1 < 0 && errno == ENOENT)
> + SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
> + ASSERT_GE(self->fd1, 0);
> +
> + session_fd = luo_create_session(self->fd1, "preserve-arg-test");
> + ASSERT_GE(session_fd, 0);
> +
> + mem_fd1 = memfd_create("arg-memfd-1", 0);
> + ASSERT_GE(mem_fd1, 0);
> +
> + /* Non-zero arg is not supported by memfd handler */
> + EXPECT_EQ(luo_session_preserve_fd_arg(session_fd, mem_fd1, 0x1111, 1),
> + -EOPNOTSUPP);
> +
> + /* Legacy struct size (up to token) should zero-fill arg and succeed */
> + min_arg.size = offsetof(struct liveupdate_session_preserve_fd, arg);
> + min_arg.fd = mem_fd1;
> + min_arg.token = 0x1111;
> + ASSERT_EQ(ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &min_arg), 0);
> +
> + /* Explicit zero arg with full struct size should also succeed */
> + mem_fd2 = memfd_create("arg-memfd-2", 0);
> + ASSERT_GE(mem_fd2, 0);
> + ASSERT_EQ(luo_session_preserve_fd_arg(session_fd, mem_fd2, 0x2222, 0), 0);
> +
> + ASSERT_EQ(close(session_fd), 0);
> + ASSERT_EQ(close(mem_fd1), 0);
> + ASSERT_EQ(close(mem_fd2), 0);
> +}
[Severity: Medium]
The commit title and message explicitly claim to add tests for both preserve
and retrieve user argument, stating it verifies support in both
LIVEUPDATE_SESSION_PRESERVE_FD and LIVEUPDATE_SESSION_RETRIEVE_FD.
However, only the preserve_memfd_with_arg test case is implemented. No
corresponding test for the retrieve path is provided here.
Was the retrieve test case accidentally omitted from this patch?
> +
> TEST_HARNESS_MAIN
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=4
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE
2026-10-15 9:38 ` [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE Pasha Tatashin
@ 2026-10-15 9:45 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:45 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
> Provide luo_session_freeze() in the liveupdate test library and add unit
> tests in liveupdate.c covering the LIVEUPDATE_SESSION_FREEZE ioctl. The
> tests verify that an outgoing session with preserved files can be frozen
> early, that freezing an already frozen session returns -EBUSY, and that
> attempting to preserve an additional file descriptor into a frozen session
> fails with -EBUSY.
>
> Also freeze the test session early in luo_kexec_simple.c to exercise
> LIVEUPDATE_SESSION_FREEZE across a kexec reboot.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=2
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable
2026-10-15 9:38 ` [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable Pasha Tatashin
@ 2026-10-15 9:46 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:46 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
> When luo_retrieve_file() invokes a handler's .retrieve() callback and it
> returns an error, LUO currently records the error in
> luo_file->retrieve_status and blocks any subsequent retrieve attempts.
> This is necessary when the handler has already partially consumed or
> freed the preserved KHO state.
>
> However, with user arguments passed to .retrieve(), a handler may fail
> early while validating caller-supplied parameters (such as a missing
> argument, an invalid or read-only target file descriptor, or a non-empty
> target file) before touching any preserved KHO state. In such cases,
> userspace should be able to fix the argument and retry
> LIVEUPDATE_SESSION_RETRIEVE_FD, and if the file is never retrieved,
> .finish() must still run to release the preserved KHO state.
>
> Add a retryable output flag to struct liveupdate_file_op_args. When a
> handler sets args->retryable = true on error, luo_retrieve_file() leaves
> luo_file->retrieve_status at 0 so the retrieve operation can be retried
> and .finish() will still clean up the unconsumed preserved state if the
> session is finished without retrieving the file.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=5
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 7/7] selftests: liveupdate: Add test coverage for tmpfs preservation and retrieval
2026-10-15 9:38 ` [PATCH v1 7/7] selftests: liveupdate: Add test coverage for " Pasha Tatashin
@ 2026-10-15 9:47 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:47 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests: out-of-bounds read in ksft_print_msg format string
--
commit 6c97b421e43a12b89a960f6a03326dbad1153339
Author: David Matlack <dmatlack@google.com>
selftests: liveupdate: Add test coverage for tmpfs preservation and retrieval
This extends the multi-session live update test to verify preserving a
path-backed tmpfs file and retrieving its contents post-kexec. It also
verifies that retrieve argument validation errors are retryable and allow
a subsequent valid retrieve call to succeed.
> diff --git a/tools/testing/selftests/liveupdate/lib/lu_utils.c b/tools/testing/selftests/liveupdate/lib/lu_utils.c
> index 5c6f8bf80b297..1b1a556287d3a 100644
> --- a/tools/testing/selftests/liveupdate/lib/lu_utils.c
> +++ b/tools/testing/selftests/liveupdate/lib/lu_utils.c
[ ... ]
> +int restore_and_verify_tmpfs(int session_fd, int token, int fd,
> + const char *expected_data)
> +{
[ ... ]
> + map = mmap(NULL, page_size, PROT_READ, MAP_SHARED, fd, 0);
> + if (map == MAP_FAILED)
> + return -errno;
> +
> + if (expected_data && strcmp(expected_data, map) != 0) {
> + ksft_print_msg("Data mismatch! Expected '%s', Got '%s'\n",
> + expected_data, (char *)map);
[Severity: Low]
Does this risk an out-of-bounds read when reporting a data mismatch?
The memory mapped at map is strictly bounded by page_size and not guaranteed
to be null-terminated. If the restored file contents lack a null terminator
due to corruption or a failed restore, the %s format specifier could read
past the page boundary and cause a crash.
Could this be printed with a length limit to ensure it safely prints only
what is mapped?
> + munmap(map, page_size);
> + return -EINVAL;
> + }
> +
> + munmap(map, page_size);
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=7
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early
2026-10-15 9:38 ` [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early Pasha Tatashin
@ 2026-10-15 9:47 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:47 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
> Currently, LUO sessions are frozen automatically only inside the
> reboot(LINUX_REBOOT_CMD_KEXEC) pathway, where luo_session_serialize()
> iterates through all outgoing sessions under global write locks and
> freezes them serially during shutdown. Allowing userspace to decide when
> to freeze a session once it is done with it (and to freeze multiple
> sessions in parallel, since luo_session_freeze() only acquires the
> per-session mutex) avoids postponing this work to kexec shutdown and
> reduces blackout time.
>
> Introduce LIVEUPDATE_SESSION_FREEZE on session file descriptors to freeze
> a session early. The ioctl runs .freeze() across all preserved files in the
> session and serializes their metadata. Once frozen, no additional files can
> be added to the session, and subsequent freeze calls return -EBUSY. Freeze
> is only valid on outgoing sessions and returns -EINVAL on incoming ones.
>
> During kexec, sessions that were already frozen early are skipped, while
> sessions that did not invoke this ioctl auto-freeze as before. If an
> early-frozen session is released before kexec, its files are rolled back
> via .unfreeze() before being unpreserved.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=1
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve
2026-10-15 9:38 ` [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve Pasha Tatashin
@ 2026-10-15 9:49 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:49 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] core: user-triggerable WARN_ON() crash due to xa_insert allocation failure
--
commit 9f9574a189615ae795011fae912d53ecfd69c013
Author: Pasha Tatashin <pasha.tatashin@soleen.com>
liveupdate: Pass user argument to file preserve and retrieve
This commit allows userspace to pass a file-handler-specific argument structure
when preserving or retrieving an FD via LUO. It extends the preserve and
retrieve structures with a 64-bit arg field, while maintaining compatibility
with older callers. Handlers can now claim files in the preserved files table
before modifying them.
> diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
> index 3c1c86ff14d69..e3bfdbc6baedc 100644
> --- a/kernel/liveupdate/luo_file.c
> +++ b/kernel/liveupdate/luo_file.c
> @@ -176,6 +177,36 @@ static unsigned long luo_get_id(struct liveupdate_file_handler *fh,
> return fh->ops->get_id ? fh->ops->get_id(file) : (unsigned long)file;
> }
>
> +/**
> + * liveupdate_claim_file - Claim a file in the LUO preserved files table.
> + * @fh: The file handler claiming @file.
> + * @file: The file to claim.
> + *
> + * File handlers whose .retrieve() callback restores state into an existing
> + * user-supplied &struct file (rather than allocating a new one) should call
> + * this before modifying @file to ensure @file is not already managed by LUO.
> + *
> + * Return: 0 on success, -EBUSY if @file is already managed by LUO, or a
> + * negative errno on allocation failure.
> + */
> +int liveupdate_claim_file(struct liveupdate_file_handler *fh,
> + struct file *file)
> +{
> + return xa_insert(&luo_preserved_files, luo_get_id(fh, file),
> + file, GFP_KERNEL);
> +}
[ ... ]
> @@ -604,9 +687,9 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
> /* Get reference so we can keep this file in LUO until finish */
> get_file(luo_file->file);
>
> - WARN_ON(xa_insert(&luo_preserved_files,
> - luo_get_id(luo_file->fh, luo_file->file),
> - luo_file->file, GFP_KERNEL));
> + if (xa_load(&luo_preserved_files,
> + luo_get_id(luo_file->fh, luo_file->file)) != luo_file->file)
> + WARN_ON(liveupdate_claim_file(luo_file->fh, luo_file->file));
[Severity: Medium]
Is it structurally safe to wrap liveupdate_claim_file() in a WARN_ON() macro?
Because liveupdate_claim_file() uses xa_insert() with GFP_KERNEL, it can
legitimately return -ENOMEM under memory pressure. The WARN_ON() macro will
evaluate this non-zero error code as true and trigger a kernel warning.
Since luo_retrieve_file() is reachable via the LIVEUPDATE_SESSION_RETRIEVE_FD
ioctl, an unprivileged user can trigger this allocation under memory pressure.
On systems with panic_on_warn enabled, this could lead to a user-triggerable
system crash.
>
> *filep = luo_file->file;
> luo_file->retrieve_status = 1;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=3
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval
2026-10-15 9:38 ` [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval Pasha Tatashin
@ 2026-10-15 9:54 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-10-15 9:54 UTC (permalink / raw)
To: Pasha Tatashin, rppt, shuah, akpm, vipinsh, dmatlack, kexec,
pratyush, graf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] mm: memfd_luo_retrieve drops file seals on tmpfs retrieval causing security bypass
- [High] mm: memfd_luo_retrieve ABBA deadlock between inode lock and f_pos_lock
- [High] mm: permanent leak of KHO folios on early memfd_luo_retrieve errors
--
commit 54190d07831630c2e724ceea33310f62bf42a6dd
Author: David Matlack <dmatlack@google.com>
mm/memfd_luo: Support tmpfs preservation and retrieval
This commit relaxes memfd_luo_can_preserve() to support general shmem-backed
files, allowing userspace to preserve standard tmpfs files across kexec. It
introduces a new MEMFD_LUO_FLAG_TMPFS flag and a new UAPI struct
liveupdate_memfd_retrieve_arg to support restoring preserved tmpfs folios
to a newly created tmpfs file.
> diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
> --- a/mm/memfd_luo.c
> +++ b/mm/memfd_luo.c
> @@ -516,10 +519,60 @@ static int memfd_luo_retrieve_folios(struct file *file,
> return err;
> }
>
> +static struct file *memfd_luo_get_tmpfs_file(struct liveupdate_file_op_args *args)
> +{
> + const struct liveupdate_memfd_retrieve_arg *rarg = args->arg;
[ ... ]
> + inode = file_inode(file);
> + inode_lock(inode);
> + if (!inode->i_nlink) {
> + err = -EINVAL;
> + goto unlock_inode;
> + }
[ ... ]
> + return file;
> +
> +unlock_inode:
> + inode_unlock(inode);
> +put_file:
> + fput(file);
> + return ERR_PTR(err);
> +}
> +
> static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
> {
> struct memfd_luo_folio_ser *folios_ser;
> struct memfd_luo_ser *ser;
> + struct inode *inode;
> struct file *file;
> int err;
>
> @@ -527,50 +580,75 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
> if (!ser)
> return -EINVAL;
>
> - /* Make sure the file only has seals supported by this version. */
> - if (ser->seals & ~MEMFD_LUO_ALL_SEALS) {
> + /* Make sure the file only has seals and flags supported by this version. */
> + if ((ser->seals & ~MEMFD_LUO_ALL_SEALS) ||
> + (ser->flags & ~MEMFD_LUO_ALL_FLAGS)) {
> err = -EOPNOTSUPP;
> goto free_ser;
> }
[Severity: High]
Does this error path permanently leak the KHO memory?
If memfd_luo_retrieve() encounters a permanent error here (like unsupported
seals), it jumps to free_ser and returns an error without setting
args->retryable = true. The LUO core then records this negative error code
in luo_file->retrieve_status.
When memfd_luo_finish() is later called:
if (args->retrieve_status)
return;
It returns early because args->retrieve_status != 0, never attempting to
discard the ser->folios vmalloc array or the underlying physical data
folios. Those folios would remain permanently reserved in memblock.
> + if (ser->flags & MEMFD_LUO_FLAG_TMPFS) {
> + file = memfd_luo_get_tmpfs_file(args);
> + if (IS_ERR(file)) {
> + args->retryable = true;
> + return PTR_ERR(file);
> + }
> + inode = file_inode(file);
> + } else {
> + if (args->arg) {
> + args->retryable = true;
> + return -EINVAL;
> + }
>
> - /*
> - * The seals are preserved. Allow sealing here so they can be added
> - * later.
> - */
> - file = memfd_alloc_file("", MFD_ALLOW_SEALING);
> - if (IS_ERR(file)) {
> - pr_err("failed to setup file: %pe\n", file);
> - err = PTR_ERR(file);
> - goto free_ser;
> - }
> + /*
> + * The seals are preserved. Allow sealing here so they can be
> + * added later.
> + */
> + file = memfd_alloc_file("", MFD_ALLOW_SEALING);
> + if (IS_ERR(file)) {
> + pr_err("failed to setup file: %pe\n", file);
> + err = PTR_ERR(file);
> + goto free_ser;
> + }
>
> - err = memfd_add_seals(file, ser->seals);
> - if (err) {
> - pr_err("failed to add seals: %pe\n", ERR_PTR(err));
> - goto put_file;
> - }
> + err = memfd_add_seals(file, ser->seals);
> + if (err) {
> + pr_err("failed to add seals: %pe\n", ERR_PTR(err));
> + goto put_file;
> + }
> +
> + inode = file_inode(file);
> + inode_lock(inode);
> + }
[Severity: High]
Does this bypass security seals on retrieved tmpfs files?
memfd_luo_preserve() records seals in ser->seals for all files. However,
the tmpfs branch above entirely skips calling memfd_add_seals(). If an
immutable or write-sealed memfd was linked to the filesystem prior to kexec,
it appears it will be restored into the user's empty tmpfs file without any
seals applied, resulting in a fully writable and modifiable file.
> if (ser->nr_folios) {
> folios_ser = kho_restore_vmalloc(&ser->folios);
> if (!folios_ser) {
> err = -EINVAL;
> - goto put_file;
> + goto unclaim_file;
> }
>
> err = memfd_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
> vfree(folios_ser);
> if (err)
> - goto put_file;
> + goto unclaim_file;
> }
>
> + vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
[Severity: High]
Can this create an ABBA deadlock between inode_lock and f_pos_lock?
For tmpfs files, memfd_luo_get_tmpfs_file() acquires and returns with
inode_lock(inode) held. Later, vfs_setpos() is called here, which acquires
mutex_lock(&file->f_pos_lock) while inode_lock is still held.
Since the file is a normal file descriptor passed by userspace, the standard
VFS write path acquires file->f_pos_lock first via fdget_pos(), and then
acquires inode_lock(inode) inside the filesystem's write iterator. A
concurrent write() on the provided file descriptor could trigger a deadlock.
> + i_size_write(inode, ser->size);
> + inode_unlock(inode);
> +
> args->file = file;
> kho_restore_free(ser);
>
> return 0;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015093816.2438942-1-pasha.tatashin@soleen.com?part=6
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-10-15 9:54 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
2026-10-15 9:38 ` [PATCH v1 1/7] liveupdate: Add LIVEUPDATE_SESSION_FREEZE ioctl to freeze sessions early Pasha Tatashin
2026-10-15 9:47 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 2/7] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE Pasha Tatashin
2026-10-15 9:45 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 3/7] liveupdate: Pass user argument to file preserve and retrieve Pasha Tatashin
2026-10-15 9:49 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 4/7] selftests: liveupdate: Add tests for preserve and retrieve user argument Pasha Tatashin
2026-10-15 9:45 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 5/7] liveupdate: Allow file handlers to mark retrieve failures as retryable Pasha Tatashin
2026-10-15 9:46 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 6/7] mm/memfd_luo: Support tmpfs preservation and retrieval Pasha Tatashin
2026-10-15 9:54 ` sashiko-bot
2026-10-15 9:38 ` [PATCH v1 7/7] selftests: liveupdate: Add test coverage for " Pasha Tatashin
2026-10-15 9:47 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.