* [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
` (7 more replies)
0 siblings, 8 replies; 20+ 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] 20+ 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
` (6 subsequent siblings)
7 siblings, 1 reply; 20+ 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] 20+ 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
` (5 subsequent siblings)
7 siblings, 1 reply; 20+ 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] 20+ 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
` (4 subsequent siblings)
7 siblings, 1 reply; 20+ 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] 20+ 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
` (3 subsequent siblings)
7 siblings, 1 reply; 20+ 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] 20+ 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
` (2 subsequent siblings)
7 siblings, 1 reply; 20+ 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] 20+ 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
2026-10-15 17:48 ` [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze David Matlack
7 siblings, 1 reply; 20+ 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] 20+ 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
2026-10-15 17:48 ` [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze David Matlack
7 siblings, 1 reply; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ 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; 20+ 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] 20+ messages in thread
* Re: [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
2026-10-15 9:38 [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze Pasha Tatashin
` (6 preceding siblings ...)
2026-10-15 9:38 ` [PATCH v1 7/7] selftests: liveupdate: Add test coverage for " Pasha Tatashin
@ 2026-10-15 17:48 ` David Matlack
2026-10-15 18:23 ` Pasha Tatashin
7 siblings, 1 reply; 20+ messages in thread
From: David Matlack @ 2026-10-15 17:48 UTC (permalink / raw)
To: Pasha Tatashin
Cc: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, kexec, pratyush, graf
On 2026-10-15 05:38 AM, Pasha Tatashin wrote:
> 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.
Thanks for putting this together. I'm on board with driving
retrieve-into through a per-handler argument instead of a LUO-level
flag. It's what Pratyush suggested on the RFC too (sorry Pratyush, I
never replied to that). I still need to review the actual patch but
I think the idea is fine.
However, nothing ties the arg struct to a handler, it's interpreted by
whichever handler owns the token. At LPC we discussed letting userspace
pick the handler in PRESERVE_FD/RETRIEVE_FD, so that it controls which
one is used when more than one can take a file. If we do that, the
handler name would also be the natural key for which arg struct is
expected.
> 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.
I've been working on a v1 of the tmpfs preservation since LPC. v1 fixes
a number of problems in the RFC that I think this series still has
(sorry for the wall of text, it is LLM-generated):
- Failures that userspace can fix lose the file. The folios are
restored, charged and added one at a time, so if the target's mount
is too small (ENOSPC/quota) or the caller hits its memcg limit
partway through, the target is truncated, the folios are freed, and
the failure is permanent. Only bad-argument errors are retryable.
v1 restores all the folios first, charges all the blocks and memcg
up front, and adds all the folios to the page cache or none of them.
It keeps the restored folios in private_data between attempts (LUO
needs to pass private_data to .retrieve() and keep it on failure),
so the only permanent failure left is losing the KHO state itself.
- memfd_pin_folios() doesn't require the inode lock, so iommufd can
pin folios of the target while they're being added or truncated
away. v1 starts with two patches that make iommufd take the lock
and make memfd_pin_folios() assert it.
- The target has to be opened for writing (FMODE_WRITE). Since LUO
holds a reference to it until finish, that means a restored binary
can't be executed until the session is finished (-ETXTBSY). v1
instead requires mnt_want_write_file() and file_permission(MAY_WRITE)
under the inode lock, like FS_IOC_ENABLE_VERITY. That also rejects
immutable files, which a fd opened for writing before chattr +i
doesn't.
- There's no file_modified() or fsnotify_modify(), so setuid/setgid
bits aren't cleared and timestamps aren't updated. The normal write
path does both.
- f_pos is set through fget() + vfs_setpos() without f_pos_lock, which
races with read()/lseek() on the same open file. v1 uses
fdget_pos(). (Sashiko called this an ABBA deadlock. I don't think it
is one, since vfs_setpos() doesn't take the lock, but the race is
real.)
- Early failures leak the preserved folios. The seals/flags check
frees ser and records a permanent error, so .finish() skips the file
and the folios are never freed. Sashiko flagged this as well. The
new MEMFD_LUO_ALL_FLAGS check adds another way to get there. In v1,
early failures don't consume anything and aren't permanent, so
.finish() cleans up.
- If a token was already retrieved, luo_retrieve_file() returns the
existing file and ignores arg. Userspace that asks to retrieve into
X gets success and an fd for a different file. I think LUO should
fail with -EBUSY if an arg is passed for a token that was already
retrieved.
- Sashiko's WARN_ON(liveupdate_claim_file()) report on 3/7 is valid:
xa_insert() can fail with -ENOMEM there, and userspace can trigger
that.
One place our series are not aligned is MEMFD_LUO_FLAG_TMPFS. My
thinking has been that RETRIEVE_FD should support retrieving into any
shmem file (including linked files, memfds, and O_TMPFILE). It is
userspace's choice at retrieve time. The only thing the kernel enforces
are the fundamental requirements such as exceeding the file's mount's
limit. When I think about it that way there is nothing special about
retrieving into a memfd and MEMFD_LUO_FLAG_TMPFS is just artificially
limiting how a shmem file can be retrieved. WDYT?
One last thing is my v1 also comes with a dedicated tmpfs selftest with
much broader positive and negative test coverage that I would like to
land (things link O_TMPFILE and read-only targets, executing restored
binaries, exact block accounting with statfs() and st_blocks, including
large folios that extend past EOF, etc.).
Maybe what we can do is keep your 1-5/7 patches and I'll rebase my v1 on
top and send that as a replacement for 6-7/7?
Thanks,
David
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
2026-10-15 17:48 ` [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze David Matlack
@ 2026-10-15 18:23 ` Pasha Tatashin
2026-10-15 18:54 ` David Matlack
0 siblings, 1 reply; 20+ messages in thread
From: Pasha Tatashin @ 2026-10-15 18:23 UTC (permalink / raw)
To: David Matlack
Cc: Pasha Tatashin, linux-kselftest, rppt, shuah, akpm, linux-mm,
vipinsh, linux-kernel, kexec, pratyush, graf
On 10-15 17:48, David Matlack wrote:
> On 2026-10-15 05:38 AM, Pasha Tatashin wrote:
> > 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.
>
> Thanks for putting this together. I'm on board with driving
> retrieve-into through a per-handler argument instead of a LUO-level
> flag. It's what Pratyush suggested on the RFC too (sorry Pratyush, I
> never replied to that). I still need to review the actual patch but
> I think the idea is fine.
Yeah, Pratyush and I discussed this at LPC. I've been using this extra
argument in my cpu_preserve series to launch jobs at a given entry
address on a preserved CPU, and figured it would be a useful, generic
interface for others as well. So I decided to send this series now to
reduce the extra infrastructure I carry in my cpu_preserve series, and
your tmpfs work was a good reason to land it now :-)
> However, nothing ties the arg struct to a handler, it's interpreted by
> whichever handler owns the token. At LPC we discussed letting userspace
> pick the handler in PRESERVE_FD/RETRIEVE_FD, so that it controls which
> one is used when more than one can take a file. If we do that, the
> handler name would also be the natural key for which arg struct is
> expected.
>
> > 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.
>
> I've been working on a v1 of the tmpfs preservation since LPC. v1 fixes
> a number of problems in the RFC that I think this series still has
> (sorry for the wall of text, it is LLM-generated):
>
> - Failures that userspace can fix lose the file. The folios are
> restored, charged and added one at a time, so if the target's mount
> is too small (ENOSPC/quota) or the caller hits its memcg limit
> partway through, the target is truncated, the folios are freed, and
> the failure is permanent. Only bad-argument errors are retryable.
> v1 restores all the folios first, charges all the blocks and memcg
> up front, and adds all the folios to the page cache or none of them.
> It keeps the restored folios in private_data between attempts (LUO
> needs to pass private_data to .retrieve() and keep it on failure),
> so the only permanent failure left is losing the KHO state itself.
I think the current behavior is fine: if the file doesn't fit when
restoring, I think it is okay to lose the file permanently. We shouldn't
keep preserved data around in private_data once we have started
retrieving folios.
> - memfd_pin_folios() doesn't require the inode lock, so iommufd can
> pin folios of the target while they're being added or truncated
> away. v1 starts with two patches that make iommufd take the lock
> and make memfd_pin_folios() assert it.
Hm, isn't this mostly orthogonal to the change we're making here?
>
> - The target has to be opened for writing (FMODE_WRITE). Since LUO
> holds a reference to it until finish, that means a restored binary
> can't be executed until the session is finished (-ETXTBSY). v1
> instead requires mnt_want_write_file() and file_permission(MAY_WRITE)
> under the inode lock, like FS_IOC_ENABLE_VERITY. That also rejects
> immutable files, which a fd opened for writing before chattr +i
> doesn't.
Ah, good point.
> - There's no file_modified() or fsnotify_modify(), so setuid/setgid
> bits aren't cleared and timestamps aren't updated. The normal write
> path does both.
That is true, though I'm not sure fsnotify_modify() is really needed
since the whole file is restored at once and I doubt anyone is watching
for modifications during restore (clearing setuid/setgid via
file_modified() makes sense, though).
> - f_pos is set through fget() + vfs_setpos() without f_pos_lock, which
> races with read()/lseek() on the same open file. v1 uses
> fdget_pos(). (Sashiko called this an ABBA deadlock. I don't think it
> is one, since vfs_setpos() doesn't take the lock, but the race is
> real.)
Right, this was reported by Sashiko; I haven't looked into it yet.
>
> - Early failures leak the preserved folios. The seals/flags check
> frees ser and records a permanent error, so .finish() skips the file
> and the folios are never freed. Sashiko flagged this as well. The
> new MEMFD_LUO_ALL_FLAGS check adds another way to get there. In v1,
> early failures don't consume anything and aren't permanent, so
> .finish() cleans up.
We originally agreed that if retrieve() fails, we fail permanently and
leak resources until reboot to avoid memory corruption from ongoing DMA
activity. However, now that we have the retryable option, we can start
relaxing what is considered a permanent failure vs. a recoverable/early
failure that .finish() can clean up.
> - If a token was already retrieved, luo_retrieve_file() returns the
> existing file and ignores arg. Userspace that asks to retrieve into
> X gets success and an fd for a different file. I think LUO should
> fail with -EBUSY if an arg is passed for a token that was already
> retrieved.
Agreed, if an arg is passed for a token that was already retrieved,
failing with -EBUSY (or -EINVAL if the target file doesn't match the
originally retrieved struct file) makes sense.
> - Sashiko's WARN_ON(liveupdate_claim_file()) report on 3/7 is valid:
> xa_insert() can fail with -ENOMEM there, and userspace can trigger
> that.
Yeah, we can address that.
> One place our series are not aligned is MEMFD_LUO_FLAG_TMPFS. My
> thinking has been that RETRIEVE_FD should support retrieving into any
> shmem file (including linked files, memfds, and O_TMPFILE). It is
> userspace's choice at retrieve time. The only thing the kernel enforces
> are the fundamental requirements such as exceeding the file's mount's
> limit. When I think about it that way there is nothing special about
> retrieving into a memfd and MEMFD_LUO_FLAG_TMPFS is just artificially
> limiting how a shmem file can be retrieved. WDYT?
I do not want LUO to change the type of a preserved file across live
update. Take seals, for example: a memfd can be sealed with the
guarantee that it will never be modified, whereas a regular tmpfs file
does not support sealing. Allowing a preserved memfd to be retrieved
into a tmpfs file would create a loophole to strip seals (or bypass
mount/superblock boundaries). I strongly prefer keeping the file type
strictly the same across preserve and retrieve.
> One last thing is my v1 also comes with a dedicated tmpfs selftest with
> much broader positive and negative test coverage that I would like to
> land (things link O_TMPFILE and read-only targets, executing restored
> binaries, exact block accounting with statfs() and st_blocks, including
> large folios that extend past EOF, etc.).
>
> Maybe what we can do is keep your 1-5/7 patches and I'll rebase my v1 on
> top and send that as a replacement for 6-7/7?
Sure, please go ahead and send the new version.
Pasha
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
2026-10-15 18:23 ` Pasha Tatashin
@ 2026-10-15 18:54 ` David Matlack
2026-10-15 20:38 ` Pasha Tatashin
0 siblings, 1 reply; 20+ messages in thread
From: David Matlack @ 2026-10-15 18:54 UTC (permalink / raw)
To: Pasha Tatashin
Cc: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, kexec, pratyush, graf
On 2026-10-15 02:23 PM, Pasha Tatashin wrote:
> On 10-15 17:48, David Matlack wrote:
> > On 2026-10-15 05:38 AM, Pasha Tatashin wrote:
> > > 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.
> >
> > Thanks for putting this together. I'm on board with driving
> > retrieve-into through a per-handler argument instead of a LUO-level
> > flag. It's what Pratyush suggested on the RFC too (sorry Pratyush, I
> > never replied to that). I still need to review the actual patch but
> > I think the idea is fine.
>
> Yeah, Pratyush and I discussed this at LPC. I've been using this extra
> argument in my cpu_preserve series to launch jobs at a given entry
> address on a preserved CPU, and figured it would be a useful, generic
> interface for others as well. So I decided to send this series now to
> reduce the extra infrastructure I carry in my cpu_preserve series, and
> your tmpfs work was a good reason to land it now :-)
>
> > However, nothing ties the arg struct to a handler, it's interpreted by
> > whichever handler owns the token. At LPC we discussed letting userspace
> > pick the handler in PRESERVE_FD/RETRIEVE_FD, so that it controls which
> > one is used when more than one can take a file. If we do that, the
> > handler name would also be the natural key for which arg struct is
> > expected.
> >
> > > 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.
> >
> > I've been working on a v1 of the tmpfs preservation since LPC. v1 fixes
> > a number of problems in the RFC that I think this series still has
> > (sorry for the wall of text, it is LLM-generated):
> >
> > - Failures that userspace can fix lose the file. The folios are
> > restored, charged and added one at a time, so if the target's mount
> > is too small (ENOSPC/quota) or the caller hits its memcg limit
> > partway through, the target is truncated, the folios are freed, and
> > the failure is permanent. Only bad-argument errors are retryable.
> > v1 restores all the folios first, charges all the blocks and memcg
> > up front, and adds all the folios to the page cache or none of them.
> > It keeps the restored folios in private_data between attempts (LUO
> > needs to pass private_data to .retrieve() and keep it on failure),
> > so the only permanent failure left is losing the KHO state itself.
>
> I think the current behavior is fine: if the file doesn't fit when
> restoring, I think it is okay to lose the file permanently. We shouldn't
> keep preserved data around in private_data once we have started
> retrieving folios.
Ok I think we can omit keeping restore folios in private_data across
retry attempts. It is a bit subtle.
> > - memfd_pin_folios() doesn't require the inode lock, so iommufd can
> > pin folios of the target while they're being added or truncated
> > away. v1 starts with two patches that make iommufd take the lock
> > and make memfd_pin_folios() assert it.
>
> Hm, isn't this mostly orthogonal to the change we're making here?
Kind of. Today .retrieve() fills a brand-new memfd that nobody else can
reach until the ioctl returns. Retrieve-into fills a file that other
processes can already open by path and pass to iommufd, so pinning can
now race with the folios being added, or truncated away on failure.
In the failure case a folio can stay pinned (and mapped for DMA) after
it's been removed from the file. Retrieve-into introduces the race, and
the fix is two small prep patches.
> > - The target has to be opened for writing (FMODE_WRITE). Since LUO
> > holds a reference to it until finish, that means a restored binary
> > can't be executed until the session is finished (-ETXTBSY). v1
> > instead requires mnt_want_write_file() and file_permission(MAY_WRITE)
> > under the inode lock, like FS_IOC_ENABLE_VERITY. That also rejects
> > immutable files, which a fd opened for writing before chattr +i
> > doesn't.
>
> Ah, good point.
>
> > - There's no file_modified() or fsnotify_modify(), so setuid/setgid
> > bits aren't cleared and timestamps aren't updated. The normal write
> > path does both.
>
> That is true, though I'm not sure fsnotify_modify() is really needed
> since the whole file is restored at once and I doubt anyone is watching
> for modifications during restore (clearing setuid/setgid via
> file_modified() makes sense, though).
>
> > - f_pos is set through fget() + vfs_setpos() without f_pos_lock, which
> > races with read()/lseek() on the same open file. v1 uses
> > fdget_pos(). (Sashiko called this an ABBA deadlock. I don't think it
> > is one, since vfs_setpos() doesn't take the lock, but the race is
> > real.)
>
> Right, this was reported by Sashiko; I haven't looked into it yet.
>
> >
> > - Early failures leak the preserved folios. The seals/flags check
> > frees ser and records a permanent error, so .finish() skips the file
> > and the folios are never freed. Sashiko flagged this as well. The
> > new MEMFD_LUO_ALL_FLAGS check adds another way to get there. In v1,
> > early failures don't consume anything and aren't permanent, so
> > .finish() cleans up.
>
> We originally agreed that if retrieve() fails, we fail permanently and
> leak resources until reboot to avoid memory corruption from ongoing DMA
> activity. However, now that we have the retryable option, we can start
> relaxing what is considered a permanent failure vs. a recoverable/early
> failure that .finish() can clean up.
Ok I think we are ligned that we need to keep the preserved folios
around until finish(). I think this point was saying there are bugs in
the existing memfd_luo code and in this series that leak the folios even
after finish(). Either way I'll fix them up.
>
> > - If a token was already retrieved, luo_retrieve_file() returns the
> > existing file and ignores arg. Userspace that asks to retrieve into
> > X gets success and an fd for a different file. I think LUO should
> > fail with -EBUSY if an arg is passed for a token that was already
> > retrieved.
>
> Agreed, if an arg is passed for a token that was already retrieved,
> failing with -EBUSY (or -EINVAL if the target file doesn't match the
> originally retrieved struct file) makes sense.
>
>
> > - Sashiko's WARN_ON(liveupdate_claim_file()) report on 3/7 is valid:
> > xa_insert() can fail with -ENOMEM there, and userspace can trigger
> > that.
>
> Yeah, we can address that.
>
> > One place our series are not aligned is MEMFD_LUO_FLAG_TMPFS. My
> > thinking has been that RETRIEVE_FD should support retrieving into any
> > shmem file (including linked files, memfds, and O_TMPFILE). It is
> > userspace's choice at retrieve time. The only thing the kernel enforces
> > are the fundamental requirements such as exceeding the file's mount's
> > limit. When I think about it that way there is nothing special about
> > retrieving into a memfd and MEMFD_LUO_FLAG_TMPFS is just artificially
> > limiting how a shmem file can be retrieved. WDYT?
>
> I do not want LUO to change the type of a preserved file across live
> update. Take seals, for example: a memfd can be sealed with the
> guarantee that it will never be modified, whereas a regular tmpfs file
> does not support sealing. Allowing a preserved memfd to be retrieved
> into a tmpfs file would create a loophole to strip seals (or bypass
> mount/superblock boundaries). I strongly prefer keeping the file type
> strictly the same across preserve and retrieve.
I agree that seals must not be strippable. I am not proposing to allow
that. Regular tmpfs files are created with F_SEAL_SEAL, and v1 refuses
to retrieve into a target that has F_SEAL_SEAL if the preserved file has
any seals the target doesn't have. So a sealed memfd can only be
retrieved into a memfd that allows sealing, and gets its seals back
there the same way F_ADD_SEALS would add them. Only unsealed memfds can
be retrieved into regular tmpfs files.
On mount boundaries, retrieving into a tmpfs file charges the blocks
to that mount, so its size limit and quota apply. That's stricter than
the unlimited internal shm mount that memfds live on, not looser.
This is what I mean about enforcing the fundamental requirements, rather
than enforcing something artifical like MEMFD_LUO_FLAG_TMPFS.
BTW, MEMFD_LUO_FLAG_TMPFS is set based on i_nlink, so it doesn't really
track the file type. An O_TMPFILE or unlinked tmpfs file gets preserved
without the flag and comes back as an anonymous memfd on the internal
shm mount. That changes its type and drops its mount's size limit and
quota, which is the boundary crossing you're worried about.
If you still want the type to stay the same, we can define it by
superblock instead of i_nlink:
- memfds (on the internal shm mount): plain retrieve only, as today.
- files on a tmpfs mount, linked or not: retrieve-into only, into any
empty tmpfs file, including O_TMPFILE files.
That keeps your guarantee, fixes the O_TMPFILE case above, and still
lets userspace restore into an O_TMPFILE file and linkat() it into
place once it's verified.
But my preference is still to enforce the fundamental requirements,
since we have to enforce them either way and MEMFD_LUO_FLAG_TMPFS adds
ABI.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
2026-10-15 18:54 ` David Matlack
@ 2026-10-15 20:38 ` Pasha Tatashin
2026-10-15 21:53 ` David Matlack
0 siblings, 1 reply; 20+ messages in thread
From: Pasha Tatashin @ 2026-10-15 20:38 UTC (permalink / raw)
To: David Matlack
Cc: Pasha Tatashin, linux-kselftest, rppt, shuah, akpm, linux-mm,
vipinsh, linux-kernel, kexec, pratyush, graf
> > > - memfd_pin_folios() doesn't require the inode lock, so iommufd can
> > > pin folios of the target while they're being added or truncated
> > > away. v1 starts with two patches that make iommufd take the lock
> > > and make memfd_pin_folios() assert it.
> >
> > Hm, isn't this mostly orthogonal to the change we're making here?
>
> Kind of. Today .retrieve() fills a brand-new memfd that nobody else can
> reach until the ioctl returns. Retrieve-into fills a file that other
> processes can already open by path and pass to iommufd, so pinning can
> now race with the folios being added, or truncated away on failure.
>
> In the failure case a folio can stay pinned (and mapped for DMA) after
> it's been removed from the file. Retrieve-into introduces the race, and
> the fix is two small prep patches.
> > We originally agreed that if retrieve() fails, we fail permanently and
> > leak resources until reboot to avoid memory corruption from ongoing DMA
> > activity. However, now that we have the retryable option, we can start
> > relaxing what is considered a permanent failure vs. a recoverable/early
> > failure that .finish() can clean up.
>
> Ok I think we are ligned that we need to keep the preserved folios
> around until finish(). I think this point was saying there are bugs in
> the existing memfd_luo code and in this series that leak the folios even
> after finish(). Either way I'll fix them up.
I think we are solving this problem incorrectly, LUO should still always
re-create files, even the tmpfs files.
What if instead of retrieve-into an existing file FD, userspace passes
a dir_fd on the target tmpfs mount where the file should be restored?
During preserve we can record the file's basename if the file was linked
(or mark that it was unlinked). On retrieve, creates a brand-new file in
dir_fd (e.g. starts as an unlinked O_TMPFILE on that tmpfs superblock),
populates all the folios and metadata while the file is still unreachable
by anyone else, links it into dir_fd under the preserved name at the end
(or leaves it unlinked if the original file was unlinked), and returns
the FD of the new file to userspace.
That way:
1. Just like memfd today, .retrieve() always populates a brand-new
file that no other process can open by path or pass to iommufd until
retrieve succeeds, so the memfd_pin_folios() / truncate race goes
away completely.
2. Userspace gets back a single new FD from RETRIEVE_FD instead of
ending up with two FDs (the pre-created one and the retrieved one)
referencing the same struct file.
3. Unlinked tmpfs files are also supported on the target tmpfs mount
(preserving mount options and size/inode accounting) instead of
silently falling back to the internal shm_mnt.
We can even postpone linking the file into dir_fd until finish(), so
that it only appears in the filesystem once it is fully populated.
Actually, I think, this is the right way to do tmpfs preservation,
instead of relying on user to provide the FD of an open file to us.
> > I do not want LUO to change the type of a preserved file across live
> > update. Take seals, for example: a memfd can be sealed with the
> > guarantee that it will never be modified, whereas a regular tmpfs file
> > does not support sealing. Allowing a preserved memfd to be retrieved
> > into a tmpfs file would create a loophole to strip seals (or bypass
> > mount/superblock boundaries). I strongly prefer keeping the file type
> > strictly the same across preserve and retrieve.
>
> I agree that seals must not be strippable. I am not proposing to allow
> that. Regular tmpfs files are created with F_SEAL_SEAL, and v1 refuses
> to retrieve into a target that has F_SEAL_SEAL if the preserved file has
> any seals the target doesn't have. So a sealed memfd can only be
> retrieved into a memfd that allows sealing, and gets its seals back
> there the same way F_ADD_SEALS would add them. Only unsealed memfds can
> be retrieved into regular tmpfs files.
>
> On mount boundaries, retrieving into a tmpfs file charges the blocks
> to that mount, so its size limit and quota apply. That's stricter than
> the unlimited internal shm mount that memfds live on, not looser.
>
> This is what I mean about enforcing the fundamental requirements, rather
> than enforcing something artifical like MEMFD_LUO_FLAG_TMPFS.
>
> BTW, MEMFD_LUO_FLAG_TMPFS is set based on i_nlink, so it doesn't really
> track the file type. An O_TMPFILE or unlinked tmpfs file gets preserved
> without the flag and comes back as an anonymous memfd on the internal
> shm mount. That changes its type and drops its mount's size limit and
> quota, which is the boundary crossing you're worried about.
>
> If you still want the type to stay the same, we can define it by
> superblock instead of i_nlink:
>
> - memfds (on the internal shm mount): plain retrieve only, as today.
> - files on a tmpfs mount, linked or not: retrieve-into only, into any
> empty tmpfs file, including O_TMPFILE files.
I would like file type to stay exactly the same, I really don't want to
play the game where LUO can magically change file from one type to
another even if from Linux internal implementation they share a lot of
code, they are still a different file types from the user perspective.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze
2026-10-15 20:38 ` Pasha Tatashin
@ 2026-10-15 21:53 ` David Matlack
0 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-10-15 21:53 UTC (permalink / raw)
To: Pasha Tatashin
Cc: linux-kselftest, rppt, shuah, akpm, linux-mm, vipinsh,
linux-kernel, kexec, pratyush, graf
On 2026-10-15 04:38 PM, Pasha Tatashin wrote:
> What if instead of retrieve-into an existing file FD, userspace passes
> a dir_fd on the target tmpfs mount where the file should be restored?
This is good. This eliminates the pinning race and also eliminates the
checks v1 had to do on the userspace-provided file.
> During preserve we can record the file's basename if the file was linked
> (or mark that it was unlinked). On retrieve, creates a brand-new file in
> dir_fd (e.g. starts as an unlinked O_TMPFILE on that tmpfs superblock),
> populates all the folios and metadata while the file is still unreachable
> by anyone else, links it into dir_fd under the preserved name at the end
> (or leaves it unlinked if the original file was unlinked), and returns
> the FD of the new file to userspace.
I don't think the name should be in the ABI though:
- Userspace already has to know what each token is, so it knows the
name. And a basename isn't enough anyway, since userspace still has
to supply the directory.
- "The name" isn't well defined. A file can have several hard links,
and it can be renamed or unlinked between preserve and freeze.
- Linking in the kernel needs a policy for collisions, and linking at
finish() can fail (or find the name taken) at a point where there's
no good way to report it.
- The name isn't the only thing needed to create the file properly.
There's also the mode, the owner (fsuid/fsgid, setgid directories,
idmapped mounts), default ACLs and LSM labels, the access mode of the
fd, and whether the file may be linked at all. If we record those in
the ABI it grows a lot.
What if we model the retrieve args on open() directly? RETRIEVE_FD
behaves like openat(dirfd, ".", O_TMPFILE | flags, mode), then fills the
file, then returns the fd:
struct liveupdate_memfd_retrieve_arg {
__u32 size;
__s32 dirfd;
__aligned_u64 flags; /* subset of O_*, ORed with O_TMPFILE */
__aligned_u64 mode;
};
In the kernel this is file_open_root(&dir_path, ".", O_TMPFILE | flags,
mode), which goes through the same path as open().
Userspace links the file into place with linkat() whenever it wants, the
usual O_TMPFILE way, or passes O_EXCL to make it unlinkable. Then
there's no name in the ABI or the uAPI, no linking in the kernel,
unlinked files are just files userspace never links, and "don't link it
until it's fully populated" comes for free.
One quirk: open() rejects O_TMPFILE without write access. With LUO
holding the file until finish, that brings back the -ETXTBSY problem for
restored binaries. vfs_tmpfile() itself doesn't need write access, and
neither does filling the file, so I think we can allow an O_RDONLY |
O_TMPFILE here.
> I would like file type to stay exactly the same, I really don't want to
> play the game where LUO can magically change file from one type to
> another even if from Linux internal implementation they share a lot of
> code, they are still a different file types from the user perspective.
Ok, I don't feel that strongly. We should just do it based on the
superblock rather than i_nlink, so that O_TMPFILE and unlinked tmpfs
files stay on a user mount.
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-10-15 21:53 UTC | newest]
Thread overview: 20+ 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
2026-10-15 17:48 ` [PATCH v1 0/7] liveupdate: tmpfs preservation and early freeze David Matlack
2026-10-15 18:23 ` Pasha Tatashin
2026-10-15 18:54 ` David Matlack
2026-10-15 20:38 ` Pasha Tatashin
2026-10-15 21:53 ` David Matlack
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox