* [PATCH v4 01/19] liveupdate: Remove the list of steps from luo_preserve_file()'s comment
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 02/19] liveupdate: Pass user argument to file preserve and retrieve David Matlack
` (17 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Remove the numbered list of steps from the kernel-doc comment of
luo_preserve_file(), along with the sentence that introduces it, which
repeats the first sentence of the previous paragraph.
The list only restates what the code does, and goes stale whenever the
code changes.
Suggested-by: Pratyush Yadav (Google) <pratyush@kernel.org>
Signed-off-by: David Matlack <dmatlack@google.com>
---
kernel/liveupdate/luo_file.c | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index a39992da504f..a42b60356d52 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -214,18 +214,6 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
* unpreserved or successfully finished in the next kernel, preventing the file
* from being prematurely destroyed.
*
- * This function orchestrates the first phase of preserving a file. It performs
- * the following steps:
- *
- * 1. Validates that the @token is not already in use within the file_set.
- * 2. Ensures the file_set's memory for files serialization is allocated
- * (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
- * and returns an opaque private data handle.
- * 5. 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.
*
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 02/19] liveupdate: Pass user argument to file preserve and retrieve
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
2026-11-11 0:23 ` [PATCH v4 01/19] liveupdate: Remove the list of steps from luo_preserve_file()'s comment David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 03/19] selftests/liveupdate: Add tests for preserve and retrieve user argument David Matlack
` (16 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
From: Pasha Tatashin <pasha.tatashin@soleen.com>
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 the file
to restore into on retrieve, or other 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.
The argument only applies to the retrieve that restores the file.
Retrieving a token that was already retrieved returns the existing file,
so fail with -EBUSY if a non-zero argument is passed then, instead of
silently ignoring it.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Signed-off-by: David Matlack <dmatlack@google.com>
---
include/linux/liveupdate.h | 44 +++++++++++++++++++++
include/uapi/linux/liveupdate.h | 10 +++++
kernel/liveupdate/luo_file.c | 65 ++++++++++++++++++++++++++++++--
kernel/liveupdate/luo_internal.h | 5 ++-
kernel/liveupdate/luo_session.c | 6 ++-
5 files changed, 122 insertions(+), 8 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 6051abc0612c..7543f5d648d3 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.
@@ -40,6 +70,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.
@@ -51,6 +86,7 @@ struct liveupdate_file_op_args {
struct file *file;
u64 serialized_data;
void *private_data;
+ const void *arg;
};
/**
@@ -68,6 +104,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
@@ -84,6 +126,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;
};
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 4043d4038712..87e77c3bfd82 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -126,6 +126,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.
*
@@ -149,6 +151,7 @@ struct liveupdate_session_preserve_fd {
__u32 size;
__s32 fd;
__aligned_u64 token;
+ __aligned_u64 arg;
};
#define LIVEUPDATE_SESSION_PRESERVE_FD \
@@ -160,6 +163,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.
*
@@ -173,12 +178,17 @@ struct liveupdate_session_preserve_fd {
* attempts to retrieve the token fail with the same error code. Failed
* retrieves are not retried.
*
+ * Retrieving a token that was already retrieved successfully returns a new file
+ * descriptor for the same file. Passing a non-zero @arg in that case fails
+ * with -EBUSY, since @arg only applies to the retrieve that restores the file.
+ *
* Return: 0 on success, negative error code on failure (e.g., invalid token).
*/
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 a42b60356d52..1437630d435e 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -126,6 +126,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);
@@ -202,11 +203,43 @@ static bool luo_token_is_used(struct luo_file_set *file_set, u64 token)
return false;
}
+static int luo_copy_user_arg(const void __user *uptr, const void **argp,
+ const struct liveupdate_arg_spec *spec)
+{
+ void *karg __free(kfree) = NULL;
+ u32 user_size;
+ int err;
+
+ if (!uptr)
+ return 0;
+ if (!spec->size)
+ return -EOPNOTSUPP;
+
+ if (get_user(user_size, (const 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 NULL.
*
* 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
@@ -227,10 +260,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-NULL and the handler takes no 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,
+ const void __user *arg)
{
struct liveupdate_file_op_args args = {0};
struct liveupdate_file_handler *fh;
@@ -266,10 +301,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)
@@ -293,6 +332,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);
@@ -306,6 +346,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:
@@ -535,6 +577,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 NULL.
* @filep: Output parameter; on success, this is populated with a pointer
* to the newly retrieved 'struct file'.
*
@@ -554,10 +597,12 @@ 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-NULL and the handler takes no arg.
+ * -EBUSY if @arg is non-NULL and the file was already retrieved.
* Any error code returned by the handler's .retrieve() op.
*/
int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
- struct file **filep)
+ const void __user *arg, struct file **filep)
{
struct liveupdate_file_op_args args = {0};
struct luo_file *luo_file;
@@ -584,6 +629,13 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
}
if (luo_file->retrieve_status > 0) {
+ /*
+ * The argument only applies to the retrieve that restores the
+ * file. Don't silently ignore it.
+ */
+ if (arg)
+ return -EBUSY;
+
/*
* Someone is asking for this file again, so get a reference
* for them.
@@ -593,10 +645,15 @@ 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.session = luo_session_from_file_set(file_set);
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;
@@ -995,6 +1052,6 @@ int liveupdate_get_file_incoming(struct liveupdate_session *s, u64 token,
struct file **filep)
{
return luo_retrieve_file(luo_file_set_from_session_locked(s),
- token, filep);
+ token, 0, filep);
}
EXPORT_SYMBOL_GPL(liveupdate_get_file_incoming);
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index dac6644bfb18..e0be84acb33b 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -101,14 +101,15 @@ 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,
+ const void __user *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,
- struct file **filep);
+ const void __user *arg, struct file **filep);
int luo_file_finish(struct luo_file_set *file_set);
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 74bc66929f4b..0d7533f0bde6 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -268,7 +268,8 @@ static int luo_session_preserve_fd(struct luo_session *session,
int err;
guard(mutex)(&session->mutex);
- err = luo_preserve_file(&session->file_set, argp->token, argp->fd);
+ err = luo_preserve_file(&session->file_set, argp->token, argp->fd,
+ u64_to_user_ptr(argp->arg));
if (err)
return err;
@@ -291,7 +292,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,
+ u64_to_user_ptr(argp->arg), &file);
mutex_unlock(&session->mutex);
if (err < 0)
goto err_put_fd;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 03/19] selftests/liveupdate: Add tests for preserve and retrieve user argument
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
2026-11-11 0:23 ` [PATCH v4 01/19] liveupdate: Remove the list of steps from luo_preserve_file()'s comment David Matlack
2026-11-11 0:23 ` [PATCH v4 02/19] liveupdate: Pass user argument to file preserve and retrieve David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 04/19] liveupdate: Allow file handlers to mark retrieve failures as retryable David Matlack
` (15 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
From: Pasha Tatashin <pasha.tatashin@soleen.com>
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>
Signed-off-by: David Matlack <dmatlack@google.com>
Reviewed-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
.../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 fa07fed08364..cd1d843a0d3d 100644
--- a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
+++ b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
@@ -29,7 +29,10 @@ int luo_get_session_name(int session_fd, char *name, size_t name_len);
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 74d41115c281..db7a90b002cb 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 2dedd5fc2534..4c4272167635 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>
@@ -530,4 +531,47 @@ TEST_F(liveupdate_device, preserve_many_files)
ASSERT_EQ(close(session_fd), 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.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 04/19] liveupdate: Allow file handlers to mark retrieve failures as retryable
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (2 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 03/19] selftests/liveupdate: Add tests for preserve and retrieve user argument David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 05/19] mm: Rename memfd_luo to shmem_luo David Matlack
` (14 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
From: Pasha Tatashin <pasha.tatashin@soleen.com>
Add a retryable output flag to struct liveupdate_file_op_args, which a
handler's .retrieve() can set on error to tell LUO that it hasn't
consumed the preserved state. LUO then lets userspace retry the
retrieve, and .finish() still releases the preserved state if the file
is never retrieved.
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 or
invalid argument, before touching any preserved KHO state. In such
cases, userspace should be able to fix the argument and retry
LIVEUPDATE_SESSION_RETRIEVE_FD.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Signed-off-by: David Matlack <dmatlack@google.com>
---
include/linux/liveupdate.h | 11 ++++++++---
include/uapi/linux/liveupdate.h | 8 +++++---
kernel/liveupdate/luo_file.c | 20 ++++++++++++++------
3 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 7543f5d648d3..e4ad0bf78a5c 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -58,9 +58,13 @@ struct liveupdate_arg_spec {
* @session: The session this file belongs to.
* @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.
@@ -83,6 +87,7 @@ struct liveupdate_file_op_args {
struct liveupdate_file_handler *handler;
struct liveupdate_session *session;
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 87e77c3bfd82..7420e2c5434b 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -174,9 +174,11 @@ 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. A file handler may instead leave the token intact on errors
+ * that occur before it consumes the preserved state, e.g. an invalid @arg, so
+ * that the retrieve can be retried. See the documentation of the file handler.
*
* Retrieving a token that was already retrieved successfully returns a new file
* descriptor for the same file. Passing a non-zero @arg in that case fails
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 1437630d435e..0c9a3b6921d1 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -152,9 +152,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.
@@ -588,7 +589,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.
@@ -655,8 +658,13 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
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.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 05/19] mm: Rename memfd_luo to shmem_luo
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (3 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 04/19] liveupdate: Allow file handlers to mark retrieve failures as retryable David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 06/19] liveupdate: Don't WARN if tracking a retrieved file fails David Matlack
` (13 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Rename the memfd Live Update file handler from memfd_luo to shmem_luo,
including the source file, its KHO ABI header, its documentation and its
Kconfig option (CONFIG_LIVEUPDATE_MEMFD to CONFIG_LIVEUPDATE_SHMEM).
The handler preserves shmem files. Today those are memfds, but following
commits extend it to other files on tmpfs mounts, for which the memfd
name is confusing. Memfds created with MFD_HUGETLB are hugetlbfs files
and would need a separate handler, so name this one after its backing
filesystem instead.
Don't change the compatible string of the handler, "memfd-v2". It is
matched against the preserved state from the previous kernel, so
changing it would break live updates from kernels that use the old
name. The serialized format doesn't change either.
Also clarify in the documentation that memfd means a memfd created
without MFD_HUGETLB, and name the ABI documentation after the handler.
No functional change intended.
Signed-off-by: David Matlack <dmatlack@google.com>
Acked-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Acked-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
Documentation/core-api/liveupdate.rst | 2 +-
Documentation/mm/index.rst | 2 +-
...reservation.rst => shmem_preservation.rst} | 8 +-
MAINTAINERS | 4 +-
include/linux/kho/abi/{memfd.h => shmem.h} | 41 ++++---
kernel/liveupdate/Kconfig | 9 +-
mm/Makefile | 2 +-
mm/{memfd_luo.c => shmem_luo.c} | 115 +++++++++---------
8 files changed, 93 insertions(+), 90 deletions(-)
rename Documentation/mm/{memfd_preservation.rst => shmem_preservation.rst} (66%)
rename include/linux/kho/abi/{memfd.h => shmem.h} (65%)
rename mm/{memfd_luo.c => shmem_luo.c} (81%)
diff --git a/Documentation/core-api/liveupdate.rst b/Documentation/core-api/liveupdate.rst
index 2bce2644eba2..669722002440 100644
--- a/Documentation/core-api/liveupdate.rst
+++ b/Documentation/core-api/liveupdate.rst
@@ -37,7 +37,7 @@ The following types of file descriptors can be preserved
.. toctree::
:maxdepth: 1
- ../mm/memfd_preservation
+ ../mm/shmem_preservation
Public API
==========
diff --git a/Documentation/mm/index.rst b/Documentation/mm/index.rst
index e9ae5cd82163..ce167d41c5cc 100644
--- a/Documentation/mm/index.rst
+++ b/Documentation/mm/index.rst
@@ -62,7 +62,7 @@ documentation, or deleted if it has served its purpose.
hugetlbfs_reserv
ksm
memory-model
- memfd_preservation
+ shmem_preservation
mmu_notifier
multigen_lru
numa
diff --git a/Documentation/mm/memfd_preservation.rst b/Documentation/mm/shmem_preservation.rst
similarity index 66%
rename from Documentation/mm/memfd_preservation.rst
rename to Documentation/mm/shmem_preservation.rst
index c908a12dffa7..e79bf8afaa9e 100644
--- a/Documentation/mm/memfd_preservation.rst
+++ b/Documentation/mm/shmem_preservation.rst
@@ -4,16 +4,16 @@
Memfd Preservation via LUO
==========================
-.. kernel-doc:: mm/memfd_luo.c
+.. kernel-doc:: mm/shmem_luo.c
:doc: Memfd Preservation via LUO
Memfd Preservation ABI
======================
-.. kernel-doc:: include/linux/kho/abi/memfd.h
- :doc: memfd Live Update ABI
+.. kernel-doc:: include/linux/kho/abi/shmem.h
+ :doc: shmem Live Update ABI
-.. kernel-doc:: include/linux/kho/abi/memfd.h
+.. kernel-doc:: include/linux/kho/abi/shmem.h
:internal:
See Also
diff --git a/MAINTAINERS b/MAINTAINERS
index 1572f6c905aa..4c97aee1f17a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15524,14 +15524,14 @@ L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/liveupdate/linux.git
F: Documentation/core-api/liveupdate.rst
-F: Documentation/mm/memfd_preservation.rst
+F: Documentation/mm/shmem_preservation.rst
F: Documentation/userspace-api/liveupdate.rst
F: include/linux/kho/abi/
F: include/linux/liveupdate.h
F: include/uapi/linux/liveupdate.h
F: kernel/liveupdate/
F: lib/tests/liveupdate.c
-F: mm/memfd_luo.c
+F: mm/shmem_luo.c
F: tools/testing/selftests/liveupdate/
LLC (802.2)
diff --git a/include/linux/kho/abi/memfd.h b/include/linux/kho/abi/shmem.h
similarity index 65%
rename from include/linux/kho/abi/memfd.h
rename to include/linux/kho/abi/shmem.h
index 08b10fea2afc..b61687e358a2 100644
--- a/include/linux/kho/abi/memfd.h
+++ b/include/linux/kho/abi/shmem.h
@@ -8,49 +8,50 @@
* Pratyush Yadav <ptyadav@amazon.de>
*/
-#ifndef _LINUX_KHO_ABI_MEMFD_H
-#define _LINUX_KHO_ABI_MEMFD_H
+#ifndef _LINUX_KHO_ABI_SHMEM_H
+#define _LINUX_KHO_ABI_SHMEM_H
#include <linux/types.h>
#include <linux/kho/abi/kexec_handover.h>
/**
- * DOC: memfd Live Update ABI
+ * DOC: shmem Live Update ABI
*
- * memfd uses the ABI defined below for preserving its state across a kexec
- * reboot using the LUO.
+ * The shmem file handler, which preserves memfds created without
+ * ``MFD_HUGETLB``, uses the ABI defined below for preserving their state across
+ * a kexec reboot using the LUO.
*
- * The state is serialized into a packed structure `struct memfd_luo_ser`
+ * The state is serialized into a packed structure `struct shmem_luo_ser`
* which is handed over to the next kernel via the KHO mechanism.
*
* This interface is a contract. Any modification to the structure layout
* constitutes a breaking change. Such changes require incrementing the
- * version number in the MEMFD_LUO_FH_COMPATIBLE string.
+ * version number in the SHMEM_LUO_FH_COMPATIBLE string.
*/
/**
- * MEMFD_LUO_FOLIO_DIRTY - The folio is dirty.
+ * SHMEM_LUO_FOLIO_DIRTY - The folio is dirty.
*
* This flag indicates the folio contains data from user. A non-dirty folio is
* one that was allocated (say using fallocate(2)) but not written to.
*/
-#define MEMFD_LUO_FOLIO_DIRTY BIT(0)
+#define SHMEM_LUO_FOLIO_DIRTY BIT(0)
/**
- * MEMFD_LUO_FOLIO_UPTODATE - The folio is up-to-date.
+ * SHMEM_LUO_FOLIO_UPTODATE - The folio is up-to-date.
*
* An up-to-date folio has been zeroed out. shmem zeroes out folios on first
* use. This flag tracks which folios need zeroing.
*/
-#define MEMFD_LUO_FOLIO_UPTODATE BIT(1)
+#define SHMEM_LUO_FOLIO_UPTODATE BIT(1)
/**
- * struct memfd_luo_folio_ser - Serialized state of a single folio.
+ * struct shmem_luo_folio_ser - Serialized state of a single folio.
* @pfn: The page frame number of the folio.
* @flags: Flags to describe the state of the folio.
* @index: The page offset (pgoff_t) of the folio within the original file.
*/
-struct memfd_luo_folio_ser {
+struct shmem_luo_folio_ser {
u64 pfn:52;
u64 flags:12;
u64 index;
@@ -60,7 +61,7 @@ struct memfd_luo_folio_ser {
* The set of seals this version supports preserving. If support for any new
* seals is needed, add it here and bump version.
*/
-#define MEMFD_LUO_ALL_SEALS (F_SEAL_SEAL | \
+#define SHMEM_LUO_ALL_SEALS (F_SEAL_SEAL | \
F_SEAL_SHRINK | \
F_SEAL_GROW | \
F_SEAL_WRITE | \
@@ -68,7 +69,7 @@ struct memfd_luo_folio_ser {
F_SEAL_EXEC)
/**
- * struct memfd_luo_ser - Main serialization structure for a memfd.
+ * struct shmem_luo_ser - Main serialization structure for a memfd.
* @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
@@ -76,9 +77,9 @@ struct memfd_luo_folio_ser {
* @flags: Flags for the file. Unused flag bits must be set to 0.
* @nr_folios: Number of folios in the folios array.
* @folios: KHO vmalloc descriptor pointing to the array of
- * struct memfd_luo_folio_ser.
+ * struct shmem_luo_folio_ser.
*/
-struct memfd_luo_ser {
+struct shmem_luo_ser {
u64 pos;
u64 size;
u32 seals;
@@ -87,7 +88,7 @@ struct memfd_luo_ser {
struct kho_vmalloc folios;
} __packed;
-/* The compatibility string for memfd file handler */
-#define MEMFD_LUO_FH_COMPATIBLE "memfd-v2"
+/* The compatibility string for the shmem file handler */
+#define SHMEM_LUO_FH_COMPATIBLE "memfd-v2"
-#endif /* _LINUX_KHO_ABI_MEMFD_H */
+#endif /* _LINUX_KHO_ABI_SHMEM_H */
diff --git a/kernel/liveupdate/Kconfig b/kernel/liveupdate/Kconfig
index f50daee8eff0..79fbf4aa4842 100644
--- a/kernel/liveupdate/Kconfig
+++ b/kernel/liveupdate/Kconfig
@@ -69,15 +69,16 @@ config LIVEUPDATE
If unsure, say N.
-config LIVEUPDATE_MEMFD
- bool "Live update support for memfd"
+config LIVEUPDATE_SHMEM
+ bool "Live update support for shmem files"
depends on LIVEUPDATE
depends on MEMFD_CREATE
depends on SHMEM
default LIVEUPDATE
help
- Enable live update support for memfd regions. This allows preserving
- memfd-backed memory across kernel live updates.
+ Enable live update support for shmem files, i.e. memfds created
+ without MFD_HUGETLB. This allows preserving memfd-backed memory
+ across kernel live updates.
This can be used to back VM memory with memfds, allowing the guest
memory to persist, or for other user workloads needing to preserve
diff --git a/mm/Makefile b/mm/Makefile
index 2a3ec53d62ee..1b1aed389af7 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -101,7 +101,7 @@ obj-$(CONFIG_NUMA) += memory-tiers.o
obj-$(CONFIG_DEVICE_MIGRATION) += migrate_device.o
obj-$(CONFIG_TRANSPARENT_HUGEPAGE) += huge_memory.o khugepaged.o
obj-$(CONFIG_PAGE_COUNTER) += page_counter.o
-obj-$(CONFIG_LIVEUPDATE_MEMFD) += memfd_luo.o
+obj-$(CONFIG_LIVEUPDATE_SHMEM) += shmem_luo.o
obj-$(CONFIG_MEMCG_V1) += memcontrol-v1.o
obj-$(CONFIG_MEMCG) += memcontrol.o vmpressure.o
ifdef CONFIG_BPF_SYSCALL
diff --git a/mm/memfd_luo.c b/mm/shmem_luo.c
similarity index 81%
rename from mm/memfd_luo.c
rename to mm/shmem_luo.c
index 6b68220275eb..e747a0442295 100644
--- a/mm/memfd_luo.c
+++ b/mm/shmem_luo.c
@@ -18,6 +18,11 @@
* Update Orchestrator (LUO) file preservation. This allows userspace to
* transfer its memory contents to the next kernel after a kexec.
*
+ * In this document, "memfd" means a memfd created without ``MFD_HUGETLB``, i.e.
+ * a shmem file on the kernel's internal shmem mount. Memfds created with
+ * ``MFD_HUGETLB`` are hugetlbfs files, which this file handler doesn't
+ * preserve. Preserving them fails with ``-ENOENT``.
+ *
* The preservation is not intended to be transparent. Only select properties of
* the file are preserved. All others are reset to default. The preserved
* properties are described below.
@@ -26,10 +31,6 @@
* 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.
*
- * .. note::
- * Currently a memfd backed by Hugetlb is not supported. Memfds created
- * with ``MFD_HUGETLB`` will be rejected.
- *
* Preserved Properties
* ====================
*
@@ -52,7 +53,7 @@
*
* Seals
* File seals set on the memfd are preserved and re-applied on restore.
- * Only seals known to this LUO version (see ``MEMFD_LUO_ALL_SEALS``) may
+ * Only seals known to this LUO version (see ``SHMEM_LUO_ALL_SEALS``) may
* be present; preservation fails with ``-EOPNOTSUPP`` otherwise.
*
* Non-Preserved Properties
@@ -75,7 +76,7 @@
#include <linux/file.h>
#include <linux/io.h>
#include <linux/kexec_handover.h>
-#include <linux/kho/abi/memfd.h>
+#include <linux/kho/abi/shmem.h>
#include <linux/liveupdate.h>
#include <linux/shmem_fs.h>
#include <linux/vmalloc.h>
@@ -84,13 +85,13 @@
#include "internal.h"
-static int memfd_luo_preserve_folios(struct file *file,
+static int shmem_luo_preserve_folios(struct file *file,
struct kho_vmalloc *kho_vmalloc,
- struct memfd_luo_folio_ser **out_folios_ser,
+ struct shmem_luo_folio_ser **out_folios_ser,
u64 *nr_foliosp)
{
struct inode *inode = file_inode(file);
- struct memfd_luo_folio_ser *folios_ser;
+ struct shmem_luo_folio_ser *folios_ser;
unsigned int max_folios;
long i, size, nr_pinned;
struct folio **folios;
@@ -145,7 +146,7 @@ static int memfd_luo_preserve_folios(struct file *file,
}
for (i = 0; i < nr_folios; i++) {
- struct memfd_luo_folio_ser *pfolio = &folios_ser[i];
+ struct shmem_luo_folio_ser *pfolio = &folios_ser[i];
struct folio *folio = folios[i];
err = kho_preserve_folio(folio);
@@ -196,7 +197,7 @@ static int memfd_luo_preserve_folios(struct file *file,
folio_unlock(folio);
pfolio->pfn = folio_pfn(folio);
- pfolio->flags = MEMFD_LUO_FOLIO_DIRTY | MEMFD_LUO_FOLIO_UPTODATE;
+ pfolio->flags = SHMEM_LUO_FOLIO_DIRTY | SHMEM_LUO_FOLIO_UPTODATE;
pfolio->index = folio->index;
}
@@ -227,8 +228,8 @@ static int memfd_luo_preserve_folios(struct file *file,
return err;
}
-static void memfd_luo_unpreserve_folios(struct kho_vmalloc *kho_vmalloc,
- struct memfd_luo_folio_ser *folios_ser,
+static void shmem_luo_unpreserve_folios(struct kho_vmalloc *kho_vmalloc,
+ struct shmem_luo_folio_ser *folios_ser,
u64 nr_folios)
{
long i;
@@ -239,7 +240,7 @@ static void memfd_luo_unpreserve_folios(struct kho_vmalloc *kho_vmalloc,
kho_unpreserve_vmalloc(kho_vmalloc);
for (i = 0; i < nr_folios; i++) {
- const struct memfd_luo_folio_ser *pfolio = &folios_ser[i];
+ const struct shmem_luo_folio_ser *pfolio = &folios_ser[i];
struct folio *folio;
if (!pfolio->pfn)
@@ -254,11 +255,11 @@ static void memfd_luo_unpreserve_folios(struct kho_vmalloc *kho_vmalloc,
vfree(folios_ser);
}
-static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
+static int shmem_luo_preserve(struct liveupdate_file_op_args *args)
{
struct inode *inode = file_inode(args->file);
- struct memfd_luo_folio_ser *folios_ser;
- struct memfd_luo_ser *ser;
+ struct shmem_luo_folio_ser *folios_ser;
+ struct shmem_luo_ser *ser;
u64 nr_folios, inode_size;
int err = 0, seals;
@@ -279,7 +280,7 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
}
/* Make sure the file only has the seals supported by this version. */
- if (seals & ~MEMFD_LUO_ALL_SEALS) {
+ if (seals & ~SHMEM_LUO_ALL_SEALS) {
err = -EOPNOTSUPP;
goto err_free_ser;
}
@@ -299,7 +300,7 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
ser->size = inode_size;
ser->seals = seals;
- err = memfd_luo_preserve_folios(args->file, &ser->folios,
+ err = shmem_luo_preserve_folios(args->file, &ser->folios,
&folios_ser, &nr_folios);
if (err)
goto err_free_ser;
@@ -320,9 +321,9 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
return err;
}
-static int memfd_luo_freeze(struct liveupdate_file_op_args *args)
+static int shmem_luo_freeze(struct liveupdate_file_op_args *args)
{
- struct memfd_luo_ser *ser;
+ struct shmem_luo_ser *ser;
if (WARN_ON_ONCE(!args->serialized_data))
return -EINVAL;
@@ -338,10 +339,10 @@ static int memfd_luo_freeze(struct liveupdate_file_op_args *args)
return 0;
}
-static void memfd_luo_unpreserve(struct liveupdate_file_op_args *args)
+static void shmem_luo_unpreserve(struct liveupdate_file_op_args *args)
{
struct inode *inode = file_inode(args->file);
- struct memfd_luo_ser *ser;
+ struct shmem_luo_ser *ser;
if (WARN_ON_ONCE(!args->serialized_data))
return;
@@ -351,20 +352,20 @@ static void memfd_luo_unpreserve(struct liveupdate_file_op_args *args)
ser = phys_to_virt(args->serialized_data);
- memfd_luo_unpreserve_folios(&ser->folios, args->private_data,
+ shmem_luo_unpreserve_folios(&ser->folios, args->private_data,
ser->nr_folios);
kho_unpreserve_free(ser);
inode_unlock(inode);
}
-static void memfd_luo_discard_folios(const struct memfd_luo_folio_ser *folios_ser,
+static void shmem_luo_discard_folios(const struct shmem_luo_folio_ser *folios_ser,
u64 nr_folios)
{
u64 i;
for (i = 0; i < nr_folios; i++) {
- const struct memfd_luo_folio_ser *pfolio = &folios_ser[i];
+ const struct shmem_luo_folio_ser *pfolio = &folios_ser[i];
struct folio *folio;
phys_addr_t phys;
@@ -383,10 +384,10 @@ static void memfd_luo_discard_folios(const struct memfd_luo_folio_ser *folios_se
}
}
-static void memfd_luo_finish(struct liveupdate_file_op_args *args)
+static void shmem_luo_finish(struct liveupdate_file_op_args *args)
{
- struct memfd_luo_folio_ser *folios_ser;
- struct memfd_luo_ser *ser;
+ struct shmem_luo_folio_ser *folios_ser;
+ struct shmem_luo_ser *ser;
/*
* If retrieve was successful, nothing to do. If it failed, retrieve()
@@ -405,7 +406,7 @@ static void memfd_luo_finish(struct liveupdate_file_op_args *args)
if (!folios_ser)
goto out;
- memfd_luo_discard_folios(folios_ser, ser->nr_folios);
+ shmem_luo_discard_folios(folios_ser, ser->nr_folios);
vfree(folios_ser);
}
@@ -413,8 +414,8 @@ static void memfd_luo_finish(struct liveupdate_file_op_args *args)
kho_restore_free(ser);
}
-static int memfd_luo_retrieve_folios(struct file *file,
- struct memfd_luo_folio_ser *folios_ser,
+static int shmem_luo_retrieve_folios(struct file *file,
+ struct shmem_luo_folio_ser *folios_ser,
u64 nr_folios)
{
struct inode *inode = file_inode(file);
@@ -425,7 +426,7 @@ static int memfd_luo_retrieve_folios(struct file *file,
long i;
for (i = 0; i < nr_folios; i++) {
- const struct memfd_luo_folio_ser *pfolio = &folios_ser[i];
+ const struct shmem_luo_folio_ser *pfolio = &folios_ser[i];
phys_addr_t phys;
u64 index;
int flags;
@@ -463,9 +464,9 @@ static int memfd_luo_retrieve_folios(struct file *file,
goto unlock_folio;
}
- if (flags & MEMFD_LUO_FOLIO_UPTODATE)
+ if (flags & SHMEM_LUO_FOLIO_UPTODATE)
folio_mark_uptodate(folio);
- if (flags & MEMFD_LUO_FOLIO_DIRTY)
+ if (flags & SHMEM_LUO_FOLIO_DIRTY)
folio_mark_dirty(folio);
npages = folio_nr_pages(folio);
@@ -497,7 +498,7 @@ static int memfd_luo_retrieve_folios(struct file *file,
* freed when the file is freed. Free the ones not added yet here.
*/
for (long j = i + 1; j < nr_folios; j++) {
- const struct memfd_luo_folio_ser *pfolio = &folios_ser[j];
+ const struct shmem_luo_folio_ser *pfolio = &folios_ser[j];
phys_addr_t phys;
if (!pfolio->pfn)
@@ -514,10 +515,10 @@ static int memfd_luo_retrieve_folios(struct file *file,
return err;
}
-static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
+static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
{
- struct memfd_luo_folio_ser *folios_ser;
- struct memfd_luo_ser *ser;
+ struct shmem_luo_folio_ser *folios_ser;
+ struct shmem_luo_ser *ser;
struct file *file;
int err;
@@ -526,7 +527,7 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
return -EINVAL;
/* Make sure the file only has seals supported by this version. */
- if (ser->seals & ~MEMFD_LUO_ALL_SEALS) {
+ if (ser->seals & ~SHMEM_LUO_ALL_SEALS) {
err = -EOPNOTSUPP;
goto free_ser;
}
@@ -558,7 +559,7 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
goto put_file;
}
- err = memfd_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
+ err = shmem_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
vfree(folios_ser);
if (err)
goto put_file;
@@ -576,7 +577,7 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
return err;
}
-static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
+static bool shmem_luo_can_preserve(struct liveupdate_file_handler *handler,
struct file *file)
{
struct inode *inode = file_inode(file);
@@ -584,30 +585,30 @@ static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
return shmem_file(file) && !inode->i_nlink;
}
-static unsigned long memfd_luo_get_id(struct file *file)
+static unsigned long shmem_luo_get_id(struct file *file)
{
return (unsigned long)file_inode(file);
}
-static const struct liveupdate_file_ops memfd_luo_file_ops = {
- .freeze = memfd_luo_freeze,
- .finish = memfd_luo_finish,
- .retrieve = memfd_luo_retrieve,
- .preserve = memfd_luo_preserve,
- .unpreserve = memfd_luo_unpreserve,
- .can_preserve = memfd_luo_can_preserve,
- .get_id = memfd_luo_get_id,
+static const struct liveupdate_file_ops shmem_luo_file_ops = {
+ .freeze = shmem_luo_freeze,
+ .finish = shmem_luo_finish,
+ .retrieve = shmem_luo_retrieve,
+ .preserve = shmem_luo_preserve,
+ .unpreserve = shmem_luo_unpreserve,
+ .can_preserve = shmem_luo_can_preserve,
+ .get_id = shmem_luo_get_id,
.owner = THIS_MODULE,
};
-static struct liveupdate_file_handler memfd_luo_handler = {
- .ops = &memfd_luo_file_ops,
- .compatible = MEMFD_LUO_FH_COMPATIBLE,
+static struct liveupdate_file_handler shmem_luo_handler = {
+ .ops = &shmem_luo_file_ops,
+ .compatible = SHMEM_LUO_FH_COMPATIBLE,
};
-static int __init memfd_luo_init(void)
+static int __init shmem_luo_init(void)
{
- int err = liveupdate_register_file_handler(&memfd_luo_handler);
+ int err = liveupdate_register_file_handler(&shmem_luo_handler);
if (err && err != -EOPNOTSUPP) {
pr_err("Could not register luo filesystem handler: %pe\n",
@@ -618,4 +619,4 @@ static int __init memfd_luo_init(void)
return 0;
}
-late_initcall(memfd_luo_init);
+late_initcall(shmem_luo_init);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 06/19] liveupdate: Don't WARN if tracking a retrieved file fails
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (4 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 05/19] mm: Rename memfd_luo to shmem_luo David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 07/19] liveupdate: Add helpers to track the files that LUO manages David Matlack
` (12 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Handle a failure to add a retrieved file to luo_preserved_files instead
of triggering a WARN_ON(). Make the failure permanent, and keep the file
until the file set is finished, like any other retrieved file. Only warn
if the file is already tracked (-EBUSY), which would be a kernel bug,
since a retrieved file is always a new file.
xa_insert() can fail with -ENOMEM, and userspace can trigger that with
LIVEUPDATE_SESSION_RETRIEVE_FD, so warning about it lets userspace crash
systems with panic_on_warn.
The handler has already consumed the preserved state when this happens,
so the retrieve can't be retried. Keeping the file ensures that its
resources are not released before the file set is finished. Finishing
the file set then erases the file from luo_preserved_files, which does
nothing for a file that was never added.
Fixes: 00d0b372374f ("liveupdate: prevent double management of files")
Signed-off-by: David Matlack <dmatlack@google.com>
Acked-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/luo_file.c | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 0c9a3b6921d1..e5d333649f76 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -668,14 +668,28 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
return err;
}
+ err = xa_insert(&luo_preserved_files,
+ luo_get_id(luo_file->fh, args.file), args.file,
+ GFP_KERNEL);
+ if (err) {
+ /* A retrieved file is new, so it can't be tracked already. */
+ WARN_ON_ONCE(err == -EBUSY);
+
+ /*
+ * The handler has consumed the preserved state, so the retrieve
+ * can't be retried. Keep the file until the file set is
+ * finished, so that its resources are released at the same
+ * time as those of other retrieved files.
+ */
+ luo_file->file = args.file;
+ luo_file->retrieve_status = err;
+ return err;
+ }
+
luo_file->file = args.file;
/* Get reference so we can keep this file in LUO until finish */
get_file(luo_file->file);
- WARN_ON(xa_insert(&luo_preserved_files,
- luo_get_id(luo_file->fh, luo_file->file),
- luo_file->file, GFP_KERNEL));
-
*filep = luo_file->file;
luo_file->retrieve_status = 1;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 07/19] liveupdate: Add helpers to track the files that LUO manages
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (5 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 06/19] liveupdate: Don't WARN if tracking a retrieved file fails David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 08/19] liveupdate: Let file handlers retrieve into an existing file David Matlack
` (11 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add luo_track_file() and luo_untrack_file(), which add a file to and
remove it from luo_preserved_files, and use them instead of open-coding
xa_insert() and xa_erase() with luo_get_id() for preserved and retrieved
files.
The next commit lets file handlers make LUO track the file they retrieve
into, which adds more users. The helpers keep the key and the -EBUSY
behavior in one place.
No functional change intended.
Signed-off-by: David Matlack <dmatlack@google.com>
---
kernel/liveupdate/luo_file.c | 32 ++++++++++++++++++++------------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index e5d333649f76..91a2b74f71cb 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -192,6 +192,19 @@ static unsigned long luo_get_id(struct liveupdate_file_handler *fh,
return fh->ops->get_id ? fh->ops->get_id(file) : (unsigned long)file;
}
+/* Make LUO manage @file, or fail with -EBUSY if it already does. */
+static int luo_track_file(struct liveupdate_file_handler *fh, struct file *file)
+{
+ return xa_insert(&luo_preserved_files, luo_get_id(fh, file), file,
+ GFP_KERNEL);
+}
+
+static void luo_untrack_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;
@@ -306,14 +319,13 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd,
if (err)
goto err_module_put;
- err = xa_insert(&luo_preserved_files, luo_get_id(fh, file),
- file, GFP_KERNEL);
+ err = luo_track_file(fh, file);
if (err)
goto err_free_arg;
err = luo_flb_file_preserve(fh);
if (err)
- goto err_erase_xa;
+ goto err_untrack;
luo_file = kzalloc_obj(*luo_file);
if (!luo_file) {
@@ -345,8 +357,8 @@ int luo_preserve_file(struct luo_file_set *file_set, u64 token, int fd,
kfree(luo_file);
err_flb_unpreserve:
luo_flb_file_unpreserve(fh);
-err_erase_xa:
- xa_erase(&luo_preserved_files, luo_get_id(fh, file));
+err_untrack:
+ luo_untrack_file(fh, file);
err_free_arg:
kfree(args.arg);
err_module_put:
@@ -395,8 +407,7 @@ void luo_file_unpreserve_files(struct luo_file_set *file_set)
luo_file->fh->ops->unpreserve(&args);
luo_flb_file_unpreserve(luo_file->fh);
- xa_erase(&luo_preserved_files,
- luo_get_id(luo_file->fh, luo_file->file));
+ luo_untrack_file(luo_file->fh, luo_file->file);
module_put(luo_file->fh->ops->owner);
list_del(&luo_file->list);
@@ -668,9 +679,7 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
return err;
}
- err = xa_insert(&luo_preserved_files,
- luo_get_id(luo_file->fh, args.file), args.file,
- GFP_KERNEL);
+ err = luo_track_file(luo_file->fh, args.file);
if (err) {
/* A retrieved file is new, so it can't be tracked already. */
WARN_ON_ONCE(err == -EBUSY);
@@ -788,8 +797,7 @@ int luo_file_finish(struct luo_file_set *file_set)
luo_file_finish_one(file_set, luo_file);
if (luo_file->file) {
- xa_erase(&luo_preserved_files,
- luo_get_id(luo_file->fh, luo_file->file));
+ luo_untrack_file(luo_file->fh, luo_file->file);
fput(luo_file->file);
}
module_put(luo_file->fh->ops->owner);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 08/19] liveupdate: Let file handlers retrieve into an existing file
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (6 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 07/19] liveupdate: Add helpers to track the files that LUO manages David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 09/19] mm/shmem_luo: Leave freeing the preserved state to finish() David Matlack
` (10 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add liveupdate_claim_file() and liveupdate_unclaim_file(), which a file
handler's .retrieve() callback can use to make LUO manage a file before
restoring the preserved state into it. Also let the callback return the
file descriptor of that file, so that LIVEUPDATE_SESSION_RETRIEVE_FD
returns it instead of a new file descriptor.
LUO starts managing a retrieved file after .retrieve() returns it, and
fails the retrieve if it already manages the file. That works for new
files. The shmem file handler will restore tmpfs files into an existing
file, though, and such a handler has to know that LUO accepts the file
before it modifies it, not after the preserved state is consumed.
Otherwise, it could restore into a file that is preserved in an
outgoing session, or into a file that was already retrieved. Preserve
also makes LUO manage the file before the file handler touches it, so
claiming the file also keeps a preserve from racing with the retrieve.
If .retrieve() claimed the file it returns, LUO doesn't track it again.
A callback that fails after claiming a file must unclaim it.
A file that is restored into an existing file is already open in the
caller, so installing a new file descriptor for it would just leave
userspace with two file descriptors for the same open file. Retrieving
the token again later still returns a new file descriptor, like for any
other file that was already retrieved.
Signed-off-by: David Matlack <dmatlack@google.com>
---
include/linux/liveupdate.h | 26 +++++++++++
include/uapi/linux/liveupdate.h | 4 +-
kernel/liveupdate/luo_file.c | 80 ++++++++++++++++++++++++++++++--
kernel/liveupdate/luo_internal.h | 2 +-
kernel/liveupdate/luo_session.c | 25 +++++++---
5 files changed, 124 insertions(+), 13 deletions(-)
diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index e4ad0bf78a5c..87a894e269f2 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -79,6 +79,13 @@ struct liveupdate_arg_spec {
* 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.
+ * @claimed: For .retrieve(): Set while the callback has claimed a file
+ * with liveupdate_claim_file(). Private to LUO.
+ * @fd: For .retrieve(): [OUT] -1, or the file descriptor of
+ * @file in the calling task if the callback restored the
+ * preserved state into a file that is already open there.
+ * LUO then returns that file descriptor to userspace
+ * instead of installing a new one.
*
* This structure bundles all parameters for the file operation callbacks.
* The 'data' and 'file' fields are used for both input and output.
@@ -92,6 +99,8 @@ struct liveupdate_file_op_args {
u64 serialized_data;
void *private_data;
const void *arg;
+ bool claimed;
+ int fd;
};
/**
@@ -306,6 +315,12 @@ int liveupdate_get_file_incoming(struct liveupdate_session *s, u64 token,
int liveupdate_get_token_outgoing(struct liveupdate_session *s,
struct file *file, u64 *tokenp);
+/* For .retrieve() callbacks that restore into an existing file */
+int liveupdate_claim_file(struct liveupdate_file_op_args *args,
+ struct file *file);
+void liveupdate_unclaim_file(struct liveupdate_file_op_args *args,
+ struct file *file);
+
#else /* CONFIG_LIVEUPDATE */
static inline bool liveupdate_enabled(void)
@@ -370,5 +385,16 @@ static inline int liveupdate_get_token_outgoing(struct liveupdate_session *s,
return -EOPNOTSUPP;
}
+static inline int liveupdate_claim_file(struct liveupdate_file_op_args *args,
+ struct file *file)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline void liveupdate_unclaim_file(struct liveupdate_file_op_args *args,
+ struct file *file)
+{
+}
+
#endif /* CONFIG_LIVEUPDATE */
#endif /* _LINUX_LIVEUPDATE_H */
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 7420e2c5434b..4ebbd578d2d5 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -161,7 +161,9 @@ struct liveupdate_session_preserve_fd {
* struct liveupdate_session_retrieve_fd - ioctl(LIVEUPDATE_SESSION_RETRIEVE_FD)
* @size: Input; sizeof(struct liveupdate_session_retrieve_fd)
* @fd: Output; The new file descriptor representing the fully restored
- * kernel resource.
+ * kernel resource, or the file descriptor of the file it was restored
+ * into, if @arg passed one (see the documentation of the file
+ * handler).
* @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.
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index 91a2b74f71cb..95b17c980054 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -592,6 +592,10 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
* @arg: User-space pointer to the handler argument structure, or NULL.
* @filep: Output parameter; on success, this is populated with a pointer
* to the newly retrieved 'struct file'.
+ * @fdp: Output parameter; on success, this is set to -1, or to the file
+ * descriptor of @filep in the calling task if the file handler
+ * restored the preserved state into a file that is already open
+ * there.
*
* This function is the primary mechanism for recreating a file in the new
* kernel after a live update. It searches the file_set's list of deserialized
@@ -616,7 +620,7 @@ void luo_file_unfreeze(struct luo_file_set *file_set,
* Any error code returned by the handler's .retrieve() op.
*/
int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
- const void __user *arg, struct file **filep)
+ const void __user *arg, struct file **filep, int *fdp)
{
struct liveupdate_file_op_args args = {0};
struct luo_file *luo_file;
@@ -656,6 +660,7 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
*/
get_file(luo_file->file);
*filep = luo_file->file;
+ *fdp = -1;
return 0;
}
@@ -666,9 +671,13 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
args.handler = luo_file->fh;
args.session = luo_session_from_file_set(file_set);
args.serialized_data = luo_file->serialized_data;
+ args.fd = -1;
err = luo_file->fh->ops->retrieve(&args);
kfree(args.arg);
if (err) {
+ /* A handler that fails must unclaim the file it claimed. */
+ WARN_ON_ONCE(args.claimed);
+
/*
* Keep the error code for later use unless the handler marked
* the failure as retryable (e.g. invalid user argument before
@@ -679,9 +688,14 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
return err;
}
- err = luo_track_file(luo_file->fh, args.file);
+ /* A file that the handler claimed is already tracked. */
+ if (!args.claimed)
+ err = luo_track_file(luo_file->fh, args.file);
if (err) {
- /* A retrieved file is new, so it can't be tracked already. */
+ /*
+ * A retrieved file that the handler didn't claim is new, so it
+ * can't be tracked already.
+ */
WARN_ON_ONCE(err == -EBUSY);
/*
@@ -700,6 +714,7 @@ int luo_retrieve_file(struct luo_file_set *file_set, u64 token,
get_file(luo_file->file);
*filep = luo_file->file;
+ *fdp = args.fd;
luo_file->retrieve_status = 1;
return 0;
@@ -1081,7 +1096,64 @@ EXPORT_SYMBOL_GPL(liveupdate_get_token_outgoing);
int liveupdate_get_file_incoming(struct liveupdate_session *s, u64 token,
struct file **filep)
{
+ int fd;
+
return luo_retrieve_file(luo_file_set_from_session_locked(s),
- token, 0, filep);
+ token, 0, filep, &fd);
}
EXPORT_SYMBOL_GPL(liveupdate_get_file_incoming);
+
+/**
+ * liveupdate_claim_file - Claim the file that the preserved state is restored
+ * into.
+ * @args: The arguments of the .retrieve() callback.
+ * @file: The existing file that the callback restores the preserved state into.
+ *
+ * LUO starts managing the file that .retrieve() returns after the callback,
+ * and a file can only be managed once. A handler that restores the preserved
+ * state into an existing file, instead of a new one, must call this before
+ * modifying @file, so that a file that LUO already manages is rejected before
+ * the preserved state is consumed. For example, @file could be preserved in an
+ * outgoing session, or be another retrieved file.
+ *
+ * After a successful claim, the callback must either return @file in
+ * @args->file, or call liveupdate_unclaim_file() and fail.
+ *
+ * Context: Can only be called from a .retrieve() callback.
+ * Return: 0 on success, -EBUSY if LUO already manages @file, or another
+ * negative errno on failure.
+ */
+int liveupdate_claim_file(struct liveupdate_file_op_args *args,
+ struct file *file)
+{
+ int err;
+
+ if (WARN_ON_ONCE(args->claimed))
+ return -EINVAL;
+
+ err = luo_track_file(args->handler, file);
+ if (err)
+ return err;
+
+ args->claimed = true;
+ return 0;
+}
+EXPORT_SYMBOL_GPL(liveupdate_claim_file);
+
+/**
+ * liveupdate_unclaim_file - Undo liveupdate_claim_file().
+ * @args: The arguments of the .retrieve() callback.
+ * @file: The file that was claimed.
+ *
+ * Context: Can only be called from a .retrieve() callback.
+ */
+void liveupdate_unclaim_file(struct liveupdate_file_op_args *args,
+ struct file *file)
+{
+ if (WARN_ON_ONCE(!args->claimed))
+ return;
+
+ luo_untrack_file(args->handler, file);
+ args->claimed = false;
+}
+EXPORT_SYMBOL_GPL(liveupdate_unclaim_file);
diff --git a/kernel/liveupdate/luo_internal.h b/kernel/liveupdate/luo_internal.h
index e0be84acb33b..5e2f82f5a044 100644
--- a/kernel/liveupdate/luo_internal.h
+++ b/kernel/liveupdate/luo_internal.h
@@ -109,7 +109,7 @@ int luo_file_freeze(struct luo_file_set *file_set,
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,
- const void __user *arg, struct file **filep);
+ const void __user *arg, struct file **filep, int *fdp);
int luo_file_finish(struct luo_file_set *file_set);
int luo_file_deserialize(struct luo_file_set *file_set,
struct luo_file_set_ser *file_set_ser);
diff --git a/kernel/liveupdate/luo_session.c b/kernel/liveupdate/luo_session.c
index 0d7533f0bde6..2f3f0eeedf07 100644
--- a/kernel/liveupdate/luo_session.c
+++ b/kernel/liveupdate/luo_session.c
@@ -285,31 +285,42 @@ static int luo_session_retrieve_fd(struct luo_session *session,
{
struct liveupdate_session_retrieve_fd *argp = ucmd->cmd;
struct file *file;
- int err;
+ int fd, err;
- argp->fd = get_unused_fd_flags(O_CLOEXEC);
- if (argp->fd < 0)
- return argp->fd;
+ fd = get_unused_fd_flags(O_CLOEXEC);
+ if (fd < 0)
+ return fd;
mutex_lock(&session->mutex);
err = luo_retrieve_file(&session->file_set, argp->token,
- u64_to_user_ptr(argp->arg), &file);
+ u64_to_user_ptr(argp->arg), &file, &argp->fd);
mutex_unlock(&session->mutex);
if (err < 0)
goto err_put_fd;
+ /*
+ * If the file handler restored the preserved state into a file that is
+ * already open in this task, return its file descriptor.
+ */
+ if (argp->fd >= 0) {
+ fput(file);
+ put_unused_fd(fd);
+ return luo_ucmd_respond(ucmd, sizeof(*argp));
+ }
+
+ argp->fd = fd;
err = luo_ucmd_respond(ucmd, sizeof(*argp));
if (err)
goto err_put_file;
- fd_install(argp->fd, file);
+ fd_install(fd, file);
return 0;
err_put_file:
fput(file);
err_put_fd:
- put_unused_fd(argp->fd);
+ put_unused_fd(fd);
return err;
}
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 09/19] mm/shmem_luo: Leave freeing the preserved state to finish()
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (7 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 08/19] liveupdate: Let file handlers retrieve into an existing file David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 10/19] mm/shmem_luo: Don't make early retrieve failures permanent David Matlack
` (9 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Don't free the serialized state in shmem_luo_retrieve(), whether the
retrieve succeeds or fails, and let finish() free whatever is left of
the preserved state of every file. Once retrieve() has restored the
folios, it sets nr_folios in the serialized state to 0, so that
finish() only frees the rest.
This fixes leaking the preserved folios when the retrieve fails before
it restores them, e.g. because the file has seals that this kernel
doesn't support, or because memfd_alloc_file() fails. The retrieve freed
the serialized state but not the folios, and finish() then skipped the
file because its retrieve had failed, so the folios were never freed.
Preserved folios may also be in use until the session is finished, e.g.
by a device that was preserved with DMA mappings of them, so they must
not be freed before finish().
Fixes: f85b1c6af5bc ("liveupdate: luo_file: remember retrieve() status")
Signed-off-by: David Matlack <dmatlack@google.com>
---
mm/shmem_luo.c | 64 +++++++++++++++++++++++++++-----------------------
1 file changed, 35 insertions(+), 29 deletions(-)
diff --git a/mm/shmem_luo.c b/mm/shmem_luo.c
index e747a0442295..b48d7f460493 100644
--- a/mm/shmem_luo.c
+++ b/mm/shmem_luo.c
@@ -390,13 +390,10 @@ static void shmem_luo_finish(struct liveupdate_file_op_args *args)
struct shmem_luo_ser *ser;
/*
- * If retrieve was successful, nothing to do. If it failed, retrieve()
- * already cleaned up everything it could. So nothing to do there
- * either. Only need to clean up when retrieve was not called.
+ * Preserved folios may be in use until the session is finished, so
+ * retrieve() leaves freeing the preserved state to finish(), whether it
+ * succeeds or not.
*/
- if (args->retrieve_status)
- return;
-
ser = phys_to_virt(args->serialized_data);
if (!ser)
return;
@@ -515,9 +512,34 @@ static int shmem_luo_retrieve_folios(struct file *file,
return err;
}
-static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
+/*
+ * Restore the preserved folios into @file. This consumes them even if it fails,
+ * since they are then either in @file or freed, so make sure finish() doesn't
+ * free them again.
+ */
+static int shmem_luo_restore_folios(struct file *file,
+ struct shmem_luo_ser *ser)
{
struct shmem_luo_folio_ser *folios_ser;
+ u64 nr_folios = ser->nr_folios;
+ int err;
+
+ if (!nr_folios)
+ return 0;
+
+ ser->nr_folios = 0;
+
+ folios_ser = kho_restore_vmalloc(&ser->folios);
+ if (!folios_ser)
+ return -EINVAL;
+
+ err = shmem_luo_retrieve_folios(file, folios_ser, nr_folios);
+ vfree(folios_ser);
+ return err;
+}
+
+static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
+{
struct shmem_luo_ser *ser;
struct file *file;
int err;
@@ -527,10 +549,8 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
return -EINVAL;
/* Make sure the file only has seals supported by this version. */
- if (ser->seals & ~SHMEM_LUO_ALL_SEALS) {
- err = -EOPNOTSUPP;
- goto free_ser;
- }
+ if (ser->seals & ~SHMEM_LUO_ALL_SEALS)
+ return -EOPNOTSUPP;
/*
* The seals are preserved. Allow sealing here so they can be added
@@ -539,8 +559,7 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
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;
+ return PTR_ERR(file);
}
err = memfd_add_seals(file, ser->seals);
@@ -552,28 +571,15 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
i_size_write(file_inode(file), ser->size);
- if (ser->nr_folios) {
- folios_ser = kho_restore_vmalloc(&ser->folios);
- if (!folios_ser) {
- err = -EINVAL;
- goto put_file;
- }
-
- err = shmem_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
- vfree(folios_ser);
- if (err)
- goto put_file;
- }
+ err = shmem_luo_restore_folios(file, ser);
+ if (err)
+ goto put_file;
args->file = file;
- kho_restore_free(ser);
-
return 0;
put_file:
fput(file);
-free_ser:
- kho_restore_free(ser);
return err;
}
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 10/19] mm/shmem_luo: Don't make early retrieve failures permanent
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (8 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 09/19] mm/shmem_luo: Leave freeing the preserved state to finish() David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 11/19] mm/shmem_luo: Require permission to preserve a file David Matlack
` (8 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Mark failures in shmem_luo_retrieve() before it starts restoring the
folios as retryable. If no retrieve succeeds, finish() frees the
preserved state, like it does for a file that was never retrieved.
Until the folios are restored, nothing has consumed the preserved state.
Leave it for another retrieve, which may succeed, e.g. if
memfd_alloc_file() failed with -ENOMEM.
The exception is a preserved file that this kernel can't restore, e.g.
because it has seals that this kernel doesn't support. Retrying can't
help, so keep that failure permanent, so that every later retrieve
returns the same error.
Signed-off-by: David Matlack <dmatlack@google.com>
Acked-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
mm/shmem_luo.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/mm/shmem_luo.c b/mm/shmem_luo.c
index b48d7f460493..dc6ede528822 100644
--- a/mm/shmem_luo.c
+++ b/mm/shmem_luo.c
@@ -552,6 +552,14 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
if (ser->seals & ~SHMEM_LUO_ALL_SEALS)
return -EOPNOTSUPP;
+ /*
+ * Nothing consumes the preserved state until the folios are restored
+ * below. If the retrieve fails before that, leave the state for another
+ * retrieve, or for finish() to free. The checks above can't succeed on
+ * a retry, so they fail permanently.
+ */
+ args->retryable = true;
+
/*
* The seals are preserved. Allow sealing here so they can be added
* later.
@@ -571,6 +579,9 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
i_size_write(file_inode(file), ser->size);
+ /* The preserved state is consumed from here on. */
+ args->retryable = false;
+
err = shmem_luo_restore_folios(file, ser);
if (err)
goto put_file;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 11/19] mm/shmem_luo: Require permission to preserve a file
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (9 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 10/19] mm/shmem_luo: Don't make early retrieve failures permanent David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 12/19] mm/shmem_luo: Retrieve tmpfs files into a file created by userspace David Matlack
` (7 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Require the file to be opened for reading and the caller to have
permission to write to it in order to preserve it. Also let the owner
preserve a file that is immutable or on a read-only superblock, and
reject append-only files.
While a file is preserved, its size is frozen for all of its users:
truncating it, fallocate() and writes past its end fail with -EPERM.
Today any process with an fd for a memfd can do that, even one that was
only given a read-only fd. Requiring write permission limits this to
callers that could truncate the file themselves. A file descriptor
opened for writing isn't required, since that would rule out preserving
a running binary, which can't be opened for writing. Only the permission
of the inode is checked, so the mount doesn't need to be writable
either. The size of an immutable file or of a file on a read-only
superblock can't change anyway, so its owner may also preserve it.
Append-only files are rejected, since their writers can only append to
them, and freezing the size would stop all writes.
The retrieved file can be read, so also require the file to be opened
for reading. Otherwise a caller with a write-only fd could preserve the
file to read its contents.
Document the frozen size and the permission requirements.
Signed-off-by: David Matlack <dmatlack@google.com>
Acked-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
mm/shmem_luo.c | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/mm/shmem_luo.c b/mm/shmem_luo.c
index dc6ede528822..bdf90aac5820 100644
--- a/mm/shmem_luo.c
+++ b/mm/shmem_luo.c
@@ -27,6 +27,15 @@
* the file are preserved. All others are reset to default. The preserved
* properties are described below.
*
+ * Preserving a file requires it to be opened for reading and the caller to have
+ * permission to write to it, but it doesn't need to be opened for writing. The
+ * owner of a file that is immutable or on a read-only superblock may also
+ * preserve it. Append-only files can't be preserved.
+ *
+ * While a file is preserved, its size can't change and no holes can be punched
+ * into it: Truncating the file, ``fallocate()`` and writes past the end of the
+ * file fail with ``-EPERM``. This applies to all users of the file.
+ *
* .. 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.
@@ -255,6 +264,30 @@ static void shmem_luo_unpreserve_folios(struct kho_vmalloc *kho_vmalloc,
vfree(folios_ser);
}
+/*
+ * Preserving a file freezes its size for all of its users, and the retrieved
+ * file can be read. So require a file opened for reading, and permission to
+ * write to the file or ownership of a file whose size can't change anyway.
+ * Reject append-only files, since freezing their size would stop all writes.
+ */
+static int shmem_luo_may_preserve(struct file *file)
+{
+ const struct mnt_idmap *idmap = file_mnt_idmap(file);
+ struct inode *inode = file_inode(file);
+
+ if (!(file->f_mode & FMODE_READ))
+ return -EBADF;
+
+ if (IS_APPEND(inode))
+ return -EPERM;
+
+ if ((sb_rdonly(inode->i_sb) || IS_IMMUTABLE(inode)) &&
+ inode_owner_or_capable(idmap, inode))
+ return 0;
+
+ return inode_permission(idmap, inode, MAY_WRITE);
+}
+
static int shmem_luo_preserve(struct liveupdate_file_op_args *args)
{
struct inode *inode = file_inode(args->file);
@@ -264,6 +297,11 @@ static int shmem_luo_preserve(struct liveupdate_file_op_args *args)
int err = 0, seals;
inode_lock(inode);
+
+ err = shmem_luo_may_preserve(args->file);
+ if (err)
+ goto err_unlock_inode;
+
shmem_freeze(inode, true);
/* Allocate the main serialization structure in preserved memory */
@@ -317,6 +355,7 @@ static int shmem_luo_preserve(struct liveupdate_file_op_args *args)
kho_unpreserve_free(ser);
err_unlock:
shmem_freeze(inode, false);
+err_unlock_inode:
inode_unlock(inode);
return err;
}
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 12/19] mm/shmem_luo: Retrieve tmpfs files into a file created by userspace
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (10 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 11/19] mm/shmem_luo: Require permission to preserve a file David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 13/19] mm/shmem_luo: Allow preserving tmpfs files David Matlack
` (6 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add support for retrieving files that were preserved from a tmpfs mount
by restoring them into a file that userspace created on a tmpfs mount.
Add struct liveupdate_shmem_retrieve_arg, which userspace passes as the
retrieve argument of LIVEUPDATE_SESSION_RETRIEVE_FD with a file
descriptor for that file, and record which files were on a tmpfs mount
with a new SHMEM_LUO_FLAG_TMPFS flag in struct shmem_luo_ser. The next
commit allows preserving tmpfs files.
The mount a tmpfs file was on doesn't exist in the new kernel, and the
path, owner, mode, extended attributes and other metadata of the file
are not preserved. Userspace knows what they should be, so let it create
the file, e.g. with O_TMPFILE, and only restore the contents and size of
the preserved file into it. Userspace can then link the file at the path
it wants. LIVEUPDATE_SESSION_RETRIEVE_FD returns the file descriptor of
the file instead of a new one.
This also fixes a bug: shmem_luo_can_preserve() is meant to only accept
memfds, but it only checks that the file has no links, so it accepts
unlinked tmpfs files by mistake. They are then retrieved as memfds,
which moves them off their mount and out from under the size limit and
quotas of the mount. Retrieving such a file without an argument now
fails with -EINVAL if it was preserved by a kernel with this commit. The
new flag uses an existing field that is zero otherwise, so a kernel
without this commit ignores it, and still retrieves tmpfs files as
memfds.
The target must be an empty regular file on a tmpfs mount, opened for
writing. Like the preserved file, it must be on a tmpfs mount, so that
memfds stay memfds and tmpfs files stay tmpfs files. Files on tmpfs
mounts always have exactly F_SEAL_SEAL, so there are no seals to
restore. The file position isn't restored either, since the open file
belongs to userspace.
A preserved file with unknown flags or seals, or a tmpfs file with
seals other than F_SEAL_SEAL, can't be restored, so fail its retrieve
permanently.
Fill the target like a write does: with write access to the superblock
and the inode lock held, calling file_modified() before and
fsnotify_modify() after. Add the folios while i_size is still 0, and set
i_size last. Reads, page faults and memfd_pin_folios() don't look past
i_size, so nothing sees the folios until the file is complete, and if
adding them fails, they can be removed again to leave the target empty.
Claim the target with liveupdate_claim_file() before modifying it, so
that a file that LUO already manages is rejected, e.g. a file that is
preserved in an outgoing session and whose size is frozen.
Like the other failures before the folios are restored, problems with
the argument and the target can be retried.
Signed-off-by: David Matlack <dmatlack@google.com>
---
Documentation/mm/shmem_preservation.rst | 12 +-
include/linux/kho/abi/shmem.h | 22 +++-
include/uapi/linux/liveupdate.h | 37 ++++++
kernel/liveupdate/Kconfig | 6 +-
mm/internal.h | 1 +
mm/shmem.c | 9 ++
mm/shmem_luo.c | 152 +++++++++++++++++++++---
7 files changed, 210 insertions(+), 29 deletions(-)
diff --git a/Documentation/mm/shmem_preservation.rst b/Documentation/mm/shmem_preservation.rst
index e79bf8afaa9e..193c044ca637 100644
--- a/Documentation/mm/shmem_preservation.rst
+++ b/Documentation/mm/shmem_preservation.rst
@@ -1,14 +1,14 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
-==========================
-Memfd Preservation via LUO
-==========================
+====================================
+Memfd and Tmpfs Preservation via LUO
+====================================
.. kernel-doc:: mm/shmem_luo.c
- :doc: Memfd Preservation via LUO
+ :doc: Memfd and Tmpfs Preservation via LUO
-Memfd Preservation ABI
-======================
+Memfd and Tmpfs Preservation ABI
+================================
.. kernel-doc:: include/linux/kho/abi/shmem.h
:doc: shmem Live Update ABI
diff --git a/include/linux/kho/abi/shmem.h b/include/linux/kho/abi/shmem.h
index b61687e358a2..ec4f7594a7b6 100644
--- a/include/linux/kho/abi/shmem.h
+++ b/include/linux/kho/abi/shmem.h
@@ -18,8 +18,8 @@
* DOC: shmem Live Update ABI
*
* The shmem file handler, which preserves memfds created without
- * ``MFD_HUGETLB``, uses the ABI defined below for preserving their state across
- * a kexec reboot using the LUO.
+ * ``MFD_HUGETLB`` and tmpfs files, uses the ABI defined below for preserving
+ * their state across a kexec reboot using the LUO.
*
* The state is serialized into a packed structure `struct shmem_luo_ser`
* which is handed over to the next kernel via the KHO mechanism.
@@ -69,12 +69,24 @@ struct shmem_luo_folio_ser {
F_SEAL_EXEC)
/**
- * struct shmem_luo_ser - Main serialization structure for a memfd.
+ * SHMEM_LUO_FLAG_TMPFS - The file was on a tmpfs mount.
+ *
+ * Set for files on a user-visible tmpfs mount. Such files can only be retrieved
+ * into a file on a tmpfs mount, and never as a memfd.
+ */
+#define SHMEM_LUO_FLAG_TMPFS BIT(0)
+
+/* The set of flags this version supports. */
+#define SHMEM_LUO_ALL_FLAGS SHMEM_LUO_FLAG_TMPFS
+
+/**
+ * struct shmem_luo_ser - Serialized state of 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
+ * @seals: The seals of the file. The seals are uABI so it is safe
* to directly use them in the ABI.
- * @flags: Flags for the file. Unused flag bits must be set to 0.
+ * @flags: Flags for the file (SHMEM_LUO_FLAG_*). Unused flag bits must be
+ * set to 0.
* @nr_folios: Number of folios in the folios array.
* @folios: KHO vmalloc descriptor pointing to the array of
* struct shmem_luo_folio_ser.
diff --git a/include/uapi/linux/liveupdate.h b/include/uapi/linux/liveupdate.h
index 4ebbd578d2d5..8e86891d5a62 100644
--- a/include/uapi/linux/liveupdate.h
+++ b/include/uapi/linux/liveupdate.h
@@ -250,4 +250,41 @@ struct liveupdate_session_get_name {
#define LIVEUPDATE_SESSION_GET_NAME \
_IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_GET_NAME)
+/**
+ * struct liveupdate_shmem_retrieve_arg - Argument to retrieve a tmpfs file
+ * @size: Input; sizeof(struct liveupdate_shmem_retrieve_arg)
+ * @fd: Input; The file to restore the preserved file into.
+ *
+ * Passed via &struct liveupdate_session_retrieve_fd.arg to retrieve a file that
+ * was preserved from a tmpfs mount. Such files can only be retrieved with this
+ * argument, and memfds can only be retrieved without it. Otherwise, the
+ * retrieve fails with -EINVAL.
+ *
+ * The kernel restores the contents and size of the preserved file into @fd,
+ * which must be an empty regular file on a tmpfs mount, opened for writing,
+ * and returns @fd in &struct liveupdate_session_retrieve_fd.fd, instead of a
+ * new file descriptor. Everything else about the file, e.g. its path, owner,
+ * mode and file status flags, is up to userspace, which creates the file. To
+ * keep anyone from accessing the file before it is complete, userspace can
+ * create it with O_TMPFILE and link it with linkat() after the retrieve. The
+ * blocks of the file are charged to the mount of @fd.
+ *
+ * LUO keeps a reference to the file until the session is finished. Since the
+ * file is open for writing, it can't be executed until then (execve() fails
+ * with -ETXTBSY).
+ *
+ * If @fd is not a file descriptor opened for writing, the retrieve fails with
+ * -EBADF. If it isn't an empty regular file on a tmpfs mount, the retrieve
+ * fails with -EINVAL, and if it is immutable, with -EPERM. If LUO already
+ * manages @fd, e.g. because it is preserved in an outgoing session, the
+ * retrieve fails with -EBUSY. These errors can be retried with another file.
+ * Errors while restoring the contents, e.g. -ENOMEM, or -ENOSPC if the
+ * contents don't fit on the mount, are permanent, and retrieving the token
+ * again returns the same error. @fd is left empty in that case.
+ */
+struct liveupdate_shmem_retrieve_arg {
+ __u32 size;
+ __s32 fd;
+};
+
#endif /* _UAPI_LIVEUPDATE_H */
diff --git a/kernel/liveupdate/Kconfig b/kernel/liveupdate/Kconfig
index 79fbf4aa4842..98373af5fcd8 100644
--- a/kernel/liveupdate/Kconfig
+++ b/kernel/liveupdate/Kconfig
@@ -76,9 +76,9 @@ config LIVEUPDATE_SHMEM
depends on SHMEM
default LIVEUPDATE
help
- Enable live update support for shmem files, i.e. memfds created
- without MFD_HUGETLB. This allows preserving memfd-backed memory
- across kernel live updates.
+ Enable live update support for shmem files: memfds created without
+ MFD_HUGETLB and files on tmpfs mounts. This allows preserving their
+ contents across kernel live updates.
This can be used to back VM memory with memfds, allowing the guest
memory to persist, or for other user workloads needing to preserve
diff --git a/mm/internal.h b/mm/internal.h
index 566cbd2dce48..6cacbf1f5c7d 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1432,6 +1432,7 @@ int shmem_add_to_page_cache(struct folio *folio,
pgoff_t index, void *expected, gfp_t gfp);
int shmem_inode_acct_blocks(struct inode *inode, long pages);
bool shmem_recalc_inode(struct inode *inode, long alloced, long swapped);
+bool shmem_inode_on_internal_mount(const struct inode *inode);
#ifdef CONFIG_SHRINKER_DEBUG
static inline __printf(2, 0) int shrinker_debugfs_name_alloc(
diff --git a/mm/shmem.c b/mm/shmem.c
index af21a89abb0f..ad6beb328553 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -468,6 +468,15 @@ bool shmem_recalc_inode(struct inode *inode, long alloced, long swapped)
return first_swapped;
}
+/*
+ * Whether @inode is on the internal shmem mount, which is used by
+ * shmem_file_setup() and thus by memfd_create().
+ */
+bool shmem_inode_on_internal_mount(const struct inode *inode)
+{
+ return inode->i_sb == shm_mnt->mnt_sb;
+}
+
bool shmem_charge(struct inode *inode, long pages)
{
struct address_space *mapping = inode->i_mapping;
diff --git a/mm/shmem_luo.c b/mm/shmem_luo.c
index bdf90aac5820..7242664ee4ae 100644
--- a/mm/shmem_luo.c
+++ b/mm/shmem_luo.c
@@ -9,19 +9,20 @@
*/
/**
- * 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 (memfd) and tmpfs files 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.
*
* In this document, "memfd" means a memfd created without ``MFD_HUGETLB``, i.e.
- * a shmem file on the kernel's internal shmem mount. Memfds created with
- * ``MFD_HUGETLB`` are hugetlbfs files, which this file handler doesn't
- * preserve. Preserving them fails with ``-ENOENT``.
+ * a shmem file on the kernel's internal shmem mount, and "tmpfs file" means a
+ * file on any other tmpfs mount. Memfds created with ``MFD_HUGETLB`` and other
+ * files on hugetlbfs are not shmem files, and this file handler doesn't
+ * preserve them. Preserving them fails with ``-ENOENT``.
*
* The preservation is not intended to be transparent. Only select properties of
* the file are preserved. All others are reset to default. The preserved
@@ -37,13 +38,14 @@
* file fail with ``-EPERM``. This applies to all users of the file.
*
* .. 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.
+ * The serialization format is not stabilized yet, so the preserved
+ * properties of a memfd or tmpfs file are also not stable and are subject
+ * to backwards incompatible changes.
*
* Preserved Properties
* ====================
*
- * The following properties of the memfd are preserved across kexec:
+ * The following properties of the file are preserved across kexec:
*
* File Contents
* All data stored in the file is preserved.
@@ -53,17 +55,21 @@
* 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 current file position of a memfd is preserved, allowing applications to
+ * continue reading/writing from their last position. The position of a tmpfs
+ * file is not restored, since it is retrieved into a file that userspace
+ * opened, as described in "Retrieving Tmpfs Files" below.
*
* File Status Flags
* memfds are always opened with ``O_RDWR`` and ``O_LARGEFILE``. This property
- * is maintained.
+ * is maintained. The file status flags of tmpfs files are not preserved.
+ * They are those of the file that userspace retrieves them into.
*
* Seals
- * File seals set on the memfd are preserved and re-applied on restore.
+ * The seals of a memfd are preserved and re-applied on restore.
* Only seals known to this LUO version (see ``SHMEM_LUO_ALL_SEALS``) may
- * be present; preservation fails with ``-EOPNOTSUPP`` otherwise.
+ * be present; preservation fails with ``-EOPNOTSUPP`` otherwise. Files on
+ * tmpfs mounts always have exactly ``F_SEAL_SEAL``.
*
* Non-Preserved Properties
* ========================
@@ -76,13 +82,31 @@
* A memfd can be created with the ``MFD_CLOEXEC`` flag that sets the
* ``FD_CLOEXEC`` on the file. This flag is not preserved and must be set
* again after restore via ``fcntl()``.
+ *
+ * Inode Attributes
+ * The name, path, owner, mode, timestamps and extended attributes of the
+ * file are not preserved.
+ *
+ * Retrieving Tmpfs Files
+ * ======================
+ *
+ * A tmpfs file is retrieved into an empty file that userspace creates on a
+ * tmpfs mount of its choice, and passes in ``struct
+ * liveupdate_shmem_retrieve_arg`` to ``LIVEUPDATE_SESSION_RETRIEVE_FD`` (see
+ * its documentation). The kernel only restores the contents and size of the
+ * preserved file. Userspace decides everything else about the file, e.g. its
+ * path, owner and mode. The blocks of the file are charged to that mount, so
+ * its size limit and quotas apply.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/bits.h>
#include <linux/err.h>
+#include <linux/fcntl.h>
#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/fsnotify.h>
#include <linux/io.h>
#include <linux/kexec_handover.h>
#include <linux/kho/abi/shmem.h>
@@ -337,6 +361,8 @@ static int shmem_luo_preserve(struct liveupdate_file_op_args *args)
ser->size = inode_size;
ser->seals = seals;
+ if (!shmem_inode_on_internal_mount(inode))
+ ser->flags |= SHMEM_LUO_FLAG_TMPFS;
err = shmem_luo_preserve_folios(args->file, &ser->folios,
&folios_ser, &nr_folios);
@@ -577,6 +603,86 @@ static int shmem_luo_restore_folios(struct file *file,
return err;
}
+/*
+ * Restore a preserved tmpfs file into the file that userspace passed in the
+ * retrieve argument.
+ */
+static int shmem_luo_retrieve_into(struct liveupdate_file_op_args *args,
+ struct shmem_luo_ser *ser)
+{
+ const struct liveupdate_shmem_retrieve_arg *arg = args->arg;
+ struct inode *inode;
+ struct file *file;
+ int err;
+
+ if (!arg)
+ return -EINVAL;
+
+ CLASS(fd, target)(arg->fd);
+ if (fd_empty(target))
+ return -EBADF;
+
+ file = fd_file(target);
+ inode = file_inode(file);
+
+ /* Like the preserved file, the file must be on a tmpfs mount. */
+ if (!shmem_file(file) || shmem_inode_on_internal_mount(inode))
+ return -EINVAL;
+
+ if (!(file->f_mode & FMODE_WRITE))
+ return -EBADF;
+
+ /* Fill the file like a write, which also takes these locks. */
+ file_start_write(file);
+ inode_lock(inode);
+
+ err = -EPERM;
+ if (IS_IMMUTABLE(inode))
+ goto unlock;
+
+ err = -EINVAL;
+ if (i_size_read(inode) || !mapping_empty(inode->i_mapping))
+ goto unlock;
+
+ err = liveupdate_claim_file(args, file);
+ if (err)
+ goto unlock;
+
+ err = file_modified(file);
+ if (err)
+ goto unclaim;
+
+ /* The preserved state is consumed from here on. */
+ args->retryable = false;
+
+ /*
+ * The folios are past the end of the file until i_size is set below,
+ * so reads, page faults and memfd_pin_folios() don't see them, and
+ * they can be removed again if adding one fails.
+ */
+ err = shmem_luo_restore_folios(file, ser);
+ if (err) {
+ shmem_truncate_range(inode, 0, (uoff_t)-1);
+ goto unclaim;
+ }
+
+ i_size_write(inode, ser->size);
+ inode_unlock(inode);
+ file_end_write(file);
+ fsnotify_modify(file);
+
+ args->file = get_file(file);
+ args->fd = arg->fd;
+ return 0;
+
+unclaim:
+ liveupdate_unclaim_file(args, file);
+unlock:
+ inode_unlock(inode);
+ file_end_write(file);
+ return err;
+}
+
static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
{
struct shmem_luo_ser *ser;
@@ -587,6 +693,14 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
if (!ser)
return -EINVAL;
+ /* Make sure the file only has flags this version supports. */
+ if (ser->flags & ~SHMEM_LUO_ALL_FLAGS)
+ return -EOPNOTSUPP;
+
+ /* Files on tmpfs mounts can only have F_SEAL_SEAL. */
+ if ((ser->flags & SHMEM_LUO_FLAG_TMPFS) && ser->seals != F_SEAL_SEAL)
+ return -EINVAL;
+
/* Make sure the file only has seals supported by this version. */
if (ser->seals & ~SHMEM_LUO_ALL_SEALS)
return -EOPNOTSUPP;
@@ -599,6 +713,13 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
*/
args->retryable = true;
+ if (ser->flags & SHMEM_LUO_FLAG_TMPFS)
+ return shmem_luo_retrieve_into(args, ser);
+
+ /* Memfds are restored into a new memfd, so they take no argument. */
+ if (args->arg)
+ return -EINVAL;
+
/*
* The seals are preserved. Allow sealing here so they can be added
* later.
@@ -654,6 +775,7 @@ static const struct liveupdate_file_ops shmem_luo_file_ops = {
.unpreserve = shmem_luo_unpreserve,
.can_preserve = shmem_luo_can_preserve,
.get_id = shmem_luo_get_id,
+ .rarg = LIVEUPDATE_ARG(struct liveupdate_shmem_retrieve_arg, fd),
.owner = THIS_MODULE,
};
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 13/19] mm/shmem_luo: Allow preserving tmpfs files
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (11 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 12/19] mm/shmem_luo: Retrieve tmpfs files into a file created by userspace David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 14/19] selftests/liveupdate: Test which tmpfs files can be preserved David Matlack
` (5 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Allow preserving files on tmpfs mounts by dropping the link count check
from shmem_luo_can_preserve(). The check was meant to limit preservation
to memfds, which never have links. It isn't needed anymore, since tmpfs
files are now retrieved into a file on a tmpfs mount.
Userspace can keep using a linked file by its path until the Live
Update. The path of the file isn't preserved. Userspace creates the file
to retrieve it into, and can then link that file at the path it wants.
Preserving a linked file affects everyone who opens it by its path,
since its size is frozen while it is preserved. The permission check
added by "mm/shmem_luo: Require permission to preserve a file" covers
that: Callers need permission to write to the file, or must own it if
its size can't change anyway.
Signed-off-by: David Matlack <dmatlack@google.com>
Acked-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
mm/shmem_luo.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/mm/shmem_luo.c b/mm/shmem_luo.c
index 7242664ee4ae..94d617b15489 100644
--- a/mm/shmem_luo.c
+++ b/mm/shmem_luo.c
@@ -757,9 +757,7 @@ static int shmem_luo_retrieve(struct liveupdate_file_op_args *args)
static bool shmem_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 shmem_luo_get_id(struct file *file)
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 14/19] selftests/liveupdate: Test which tmpfs files can be preserved
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (12 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 13/19] mm/shmem_luo: Allow preserving tmpfs files David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 15/19] selftests/liveupdate: Add a test for retrieving tmpfs files David Matlack
` (4 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add tests to liveupdate.c for the checks done when preserving a tmpfs
file: A file can be preserved through a read-only fd, but not a
write-only fd. Preserving it requires permission to write to it, except
for the owner of an immutable file or of a file on a read-only
superblock. Append-only files can't be preserved, and files can be
preserved through a read-only mount. Also check that the size of a
preserved file is frozen until the file is unpreserved.
These checks don't need a kexec, so they run as part of the regular
liveupdate selftest.
Enable CONFIG_TMPFS_XATTR for FS_IOC_SETFLAGS. The test cases that use
it are skipped without it.
Signed-off-by: David Matlack <dmatlack@google.com>
---
tools/testing/selftests/liveupdate/config | 1 +
.../testing/selftests/liveupdate/liveupdate.c | 303 ++++++++++++++++++
2 files changed, 304 insertions(+)
diff --git a/tools/testing/selftests/liveupdate/config b/tools/testing/selftests/liveupdate/config
index 016d009dba13..37230f4c488f 100644
--- a/tools/testing/selftests/liveupdate/config
+++ b/tools/testing/selftests/liveupdate/config
@@ -9,4 +9,5 @@ CONFIG_LIVEUPDATE=y
CONFIG_LIVEUPDATE_TEST=y
CONFIG_MEMFD_CREATE=y
CONFIG_TMPFS=y
+CONFIG_TMPFS_XATTR=y
CONFIG_SHMEM=y
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 4c4272167635..3d943b5672e6 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -16,15 +16,24 @@
* - Resource preservation: successfully preserving individual and multiple memfds,
* verifying contents remain accessible.
* - Complex multi-session scenarios involving mixed empty and populated files.
+ * - Which tmpfs files can be preserved, and the frozen size of preserved files.
*/
#include <errno.h>
#include <fcntl.h>
+#include <grp.h>
#include <stddef.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
#include <unistd.h>
+#include <linux/falloc.h>
+#include <linux/fs.h>
+
#include <libliveupdate.h>
#include <linux/liveupdate.h>
@@ -574,4 +583,298 @@ TEST_F(liveupdate_device, preserve_memfd_with_arg)
ASSERT_EQ(close(mem_fd2), 0);
}
+#define NOBODY 65534
+
+FIXTURE(preserve_tmpfs) {
+ char dir[32];
+ int luo_fd;
+ int session_fd;
+ bool mounted;
+};
+
+FIXTURE_SETUP(preserve_tmpfs)
+{
+ self->luo_fd = -1;
+ self->session_fd = -1;
+ self->mounted = false;
+
+ self->luo_fd = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->luo_fd < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->luo_fd, 0);
+
+ self->session_fd = luo_create_session(self->luo_fd, "preserve-tmpfs");
+ ASSERT_GE(self->session_fd, 0);
+
+ strcpy(self->dir, "/tmp/luo-tmpfs-XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (mount("tmpfs", self->dir, "tmpfs", 0, "size=1M"))
+ SKIP(return, "Failed to mount tmpfs: %s", strerror(errno));
+ self->mounted = true;
+ ASSERT_EQ(chdir(self->dir), 0);
+}
+
+FIXTURE_TEARDOWN(preserve_tmpfs)
+{
+ if (self->session_fd >= 0)
+ close(self->session_fd);
+ if (self->luo_fd >= 0)
+ close(self->luo_fd);
+ if (chdir("/"))
+ return;
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* Create the file @path with some contents and the mode @mode. */
+static int create_file(const char *path, mode_t mode)
+{
+ int fd;
+
+ fd = open(path, O_CREAT | O_EXCL | O_RDWR, mode);
+ if (fd < 0)
+ return -1;
+
+ if (write(fd, "data", 4) != 4 || fchmod(fd, mode)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/* Open @path with @flags and preserve it. Return 0 or -errno. */
+static int preserve_path(int session_fd, const char *path, int flags,
+ __u64 token)
+{
+ int fd, ret;
+
+ fd = open(path, flags);
+ if (fd < 0)
+ return -errno;
+
+ ret = luo_session_preserve_fd(session_fd, fd, token);
+ close(fd);
+ return ret;
+}
+
+/* Like preserve_path(), but as the user nobody. */
+static int preserve_as_nobody(int session_fd, const char *path, __u64 token)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -errno;
+
+ if (!pid) {
+ if (setgroups(0, NULL) || setresgid(NOBODY, NOBODY, NOBODY) ||
+ setresuid(NOBODY, NOBODY, NOBODY))
+ _exit(255);
+ _exit(-preserve_path(session_fd, path, O_RDONLY, token));
+ }
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -ECHILD;
+
+ return -WEXITSTATUS(status);
+}
+
+/* Add the inode flag @flag, e.g. FS_IMMUTABLE_FL, to the file @fd. */
+static int add_inode_flag(int fd, int flag)
+{
+ int flags;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags))
+ return -errno;
+
+ flags |= flag;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &flags))
+ return -errno;
+
+ return 0;
+}
+
+/*
+ * Test Case: Preserve Through Read-Only and Write-Only Fds
+ *
+ * A file can be preserved through a read-only fd, but not through a
+ * write-only fd, since the retrieved file can be read.
+ */
+TEST_F(preserve_tmpfs, access_mode)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_WRONLY, 1), -EBADF);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve Without Write Permission
+ *
+ * Preserving a file freezes its size for all of its users, so it requires
+ * permission to write to the file.
+ */
+TEST_F(preserve_tmpfs, unwritable)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "file", 1), -EACCES);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve an Append-Only File
+ *
+ * Freezing the size of an append-only file would stop all writes to it, so
+ * append-only files can't be preserved.
+ */
+TEST_F(preserve_tmpfs, append_only)
+{
+ int fd, ret;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ret = add_inode_flag(fd, FS_APPEND_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 1), -EPERM);
+}
+
+/*
+ * Test Case: Preserve an Immutable File
+ *
+ * The size of an immutable file can't change anyway, so its owner may preserve
+ * it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, immutable)
+{
+ int fd, ret;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ ret = add_inode_flag(fd, FS_IMMUTABLE_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(add_inode_flag(fd, FS_IMMUTABLE_FL), 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EPERM);
+}
+
+/*
+ * Test Case: Preserve Through a Read-Only Mount
+ *
+ * Preserving a file only needs permission to write to the file, not a writable
+ * mount, so it works through a read-only mount.
+ */
+TEST_F(preserve_tmpfs, rdonly_mount)
+{
+ int fd;
+
+ ASSERT_EQ(mkdir("src", 0755), 0);
+ fd = create_file("src/file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mkdir("ro", 0755), 0);
+ ASSERT_EQ(mount("src", "ro", NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, "ro", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY,
+ NULL), 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "ro/file", O_RDONLY, 1), 0);
+ umount2("ro", MNT_DETACH);
+}
+
+/*
+ * Test Case: Preserve on a Read-Only Superblock
+ *
+ * The size of a file on a read-only superblock can't change anyway, so its
+ * owner may preserve it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, rdonly_sb)
+{
+ int fd;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ close(fd);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mount(NULL, self->dir, NULL, MS_REMOUNT | MS_RDONLY, NULL),
+ 0);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EROFS);
+}
+
+/*
+ * Test Case: Frozen Size of a Preserved File
+ *
+ * While a file is preserved, its size can't change and no holes can be punched
+ * into it. Writes within the file still work. Closing the session unpreserves
+ * the file, after which its size can change again.
+ */
+TEST_F(preserve_tmpfs, frozen_size)
+{
+ long page_size = getpagesize();
+ struct stat st;
+ char c;
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(ftruncate(fd, 2 * page_size), 0);
+ ASSERT_EQ(luo_session_preserve_fd(self->session_fd, fd, 1), 0);
+
+ EXPECT_EQ(ftruncate(fd, page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(ftruncate(fd, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, 0, 0, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
+ page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(pwrite(fd, "x", 1, 2 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+
+ /* The size didn't change, but writes within the file work. */
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_size, 2 * page_size);
+ EXPECT_EQ(pwrite(fd, "x", 1, page_size), 1);
+ EXPECT_EQ(pread(fd, &c, 1, page_size), 1);
+ EXPECT_EQ(c, 'x');
+
+ ASSERT_EQ(close(self->session_fd), 0);
+ self->session_fd = -1;
+ EXPECT_EQ(ftruncate(fd, page_size), 0);
+
+ close(fd);
+}
+
TEST_HARNESS_MAIN
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 15/19] selftests/liveupdate: Add a test for retrieving tmpfs files
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (13 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 14/19] selftests/liveupdate: Test which tmpfs files can be preserved David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 16/19] selftests/liveupdate: Test memfds and LUO interactions in luo_tmpfs David Matlack
` (3 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add luo_tmpfs, a kexec test for preserving tmpfs files and retrieving
them into a file on a tmpfs mount with struct
liveupdate_shmem_retrieve_arg.
Stage 1 preserves the source files of each test case. Stage 2 runs each
test case in its own directory, with its own tmpfs mounts, so that block
usage can be checked exactly with statfs() and st_blocks. Following
commits add more test cases.
The first test cases check the contents, size and holes of the retrieved
file, for linked source files and for files created with O_TMPFILE,
retrieved into files created with O_TMPFILE or with a path. They also
check that only the contents and size of the target change: It keeps its
owner, mode, flags and file position, and the retrieve returns the fd of
the target instead of a new one. Also check that the test doesn't
trigger kernel warnings.
Signed-off-by: David Matlack <dmatlack@google.com>
---
tools/testing/selftests/liveupdate/Makefile | 1 +
.../testing/selftests/liveupdate/luo_tmpfs.c | 728 ++++++++++++++++++
.../selftests/liveupdate/run-vmtests.sh | 1 +
3 files changed, 730 insertions(+)
create mode 100644 tools/testing/selftests/liveupdate/luo_tmpfs.c
diff --git a/tools/testing/selftests/liveupdate/Makefile b/tools/testing/selftests/liveupdate/Makefile
index 634211c66652..e217008db3a7 100644
--- a/tools/testing/selftests/liveupdate/Makefile
+++ b/tools/testing/selftests/liveupdate/Makefile
@@ -6,6 +6,7 @@ TEST_GEN_PROGS_EXTENDED += luo_kexec_simple
TEST_GEN_PROGS_EXTENDED += luo_multi_session
TEST_GEN_PROGS_EXTENDED += luo_stress_sessions
TEST_GEN_PROGS_EXTENDED += luo_stress_files
+TEST_GEN_PROGS_EXTENDED += luo_tmpfs
TEST_FILES += do_kexec.sh
diff --git a/tools/testing/selftests/liveupdate/luo_tmpfs.c b/tools/testing/selftests/liveupdate/luo_tmpfs.c
new file mode 100644
index 000000000000..4e6e035e8433
--- /dev/null
+++ b/tools/testing/selftests/liveupdate/luo_tmpfs.c
@@ -0,0 +1,728 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Copyright (c) 2026, Google LLC.
+ * David Matlack <dmatlack@google.com>
+ *
+ * Test the preservation of tmpfs files across a kexec, and retrieving them into
+ * a file on a tmpfs mount with struct liveupdate_shmem_retrieve_arg.
+ *
+ * Each test case preserves its source files in stage 1 and checks how they can
+ * be retrieved in stage 2. The cases use separate tokens, so that a failed case
+ * doesn't affect the others. In stage 2, each case mounts the tmpfs instances
+ * it needs, so that their block usage can be checked exactly.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+/*
+ * Include this before sys/mman.h, which only defines the MFD_* flags that
+ * aren't defined yet.
+ */
+#include <linux/memfd.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <linux/falloc.h>
+#include <linux/fs.h>
+
+#ifdef NOLIBC
+#include <asm/statfs.h>
+#else
+#include <sys/statfs.h>
+#endif
+
+#include <libliveupdate.h>
+
+#ifndef MNT_DETACH
+#define MNT_DETACH 2
+#endif
+#ifndef O_TMPFILE
+#define O_TMPFILE (020000000 | O_DIRECTORY)
+#endif
+
+#define STATE_SESSION_NAME "tmpfs_state"
+#define STATE_TOKEN 999
+#define SESSION_NAME "tmpfs"
+
+/*
+ * Everything happens on a tmpfs mounted at TEST_DIR. In stage 1, the source
+ * files are created on the tmpfs mounted at TEST_DIR/src. In stage 2, each
+ * case runs in the directory TEST_DIR/<name of the case>.
+ */
+#define TEST_DIR "/luo_tmpfs"
+
+#define SMALL_SIZE 8192
+#define SPARSE_PAGES 8
+#define TEST_UMASK 022
+
+/* TAINT_BAD_PAGE, TAINT_DIE and TAINT_WARN. */
+#define BAD_TAINT ((1L << 5) | (1L << 7) | (1L << 9))
+
+/*
+ * Checks for the test cases. On failure, they print the failed check and
+ * return KSFT_FAIL from the calling function.
+ */
+#define CHECK(cond) do { \
+ if (!(cond)) { \
+ ksft_print_msg("%s:%d: Check failed: %s (errno %d)\n", \
+ __func__, __LINE__, #cond, errno); \
+ return KSFT_FAIL; \
+ } \
+} while (0)
+
+#define CHECK_EQ(expr, expected) do { \
+ long long __val = (expr), __exp = (expected); \
+ \
+ if (__val != __exp) { \
+ ksft_print_msg("%s:%d: %s is %lld, expected %lld\n", \
+ __func__, __LINE__, #expr, __val, __exp); \
+ return KSFT_FAIL; \
+ } \
+} while (0)
+
+/* Call a function that returns a KSFT_* code, and return if it fails. */
+#define TRY(expr) do { \
+ int __ret = (expr); \
+ \
+ if (__ret != KSFT_PASS) { \
+ ksft_print_msg("%s:%d: %s failed\n", \
+ __func__, __LINE__, #expr); \
+ return __ret; \
+ } \
+} while (0)
+
+struct test_case {
+ const char *name;
+ /* Size and file position of the source file. */
+ size_t size;
+ off_t pos;
+ /* Stage 1: Preserve the source files. Defaults to preserve_file(). */
+ int (*preserve)(struct test_case *tc);
+ /* Stage 2: Retrieve the files and check them. */
+ int (*verify)(struct test_case *tc);
+ /* The tokens of the case are token to token + 0xff. */
+ __u64 token;
+};
+
+static unsigned long page_size;
+static int session_fd;
+
+/* nolibc doesn't have these, so use syscall() where needed. */
+static int do_fcntl(int fd, int cmd, long arg)
+{
+ return syscall(__NR_fcntl, fd, cmd, arg);
+}
+
+static int do_mount(const char *source, const char *target, const char *type,
+ unsigned long flags, const char *data)
+{
+ return syscall(__NR_mount, source, target, type, flags, data);
+}
+
+static int do_statfs(const char *path, struct statfs *buf)
+{
+#ifdef NOLIBC
+ return syscall(__NR_statfs, path, buf);
+#else
+ return statfs(path, buf);
+#endif
+}
+
+static unsigned long nr_pages(size_t size)
+{
+ return (size + page_size - 1) / page_size;
+}
+
+static long long file_size(int fd)
+{
+ struct stat st;
+
+ if (fstat(fd, &st))
+ return -1;
+
+ return st.st_size;
+}
+
+/* Return the number of pages charged to the file. */
+static long file_pages(int fd)
+{
+ struct stat st;
+
+ if (fstat(fd, &st))
+ return -1;
+
+ return st.st_blocks * 512 / page_size;
+}
+
+/* Return the number of pages used on the tmpfs mounted at @path. */
+static long mnt_used(const char *path)
+{
+ struct statfs buf;
+
+ if (do_statfs(path, &buf))
+ return -1;
+
+ return buf.f_blocks - buf.f_bfree;
+}
+
+/* Mount a tmpfs with room for @pages pages at @dir. */
+static int mount_tmpfs(const char *dir, unsigned long pages, const char *opts)
+{
+ char data[64];
+
+ if (mkdir(dir, 0755) && errno != EEXIST)
+ return -errno;
+
+ snprintf(data, sizeof(data), "size=%lu%s%s", pages * page_size,
+ opts ? "," : "", opts ? opts : "");
+ if (do_mount("tmpfs", dir, "tmpfs", 0, data))
+ return -errno;
+
+ return 0;
+}
+
+/* Read a number from the file at @path. Return -1 on failure. */
+static long read_long(const char *path)
+{
+ char buf[32] = {};
+ ssize_t len;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -1;
+
+ len = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (len <= 0)
+ return -1;
+
+ return strtol(buf, NULL, 10);
+}
+
+/* Retrieve with the argument @arg. Return the retrieved fd or -errno. */
+static int retrieve_arg(__u64 token, struct liveupdate_shmem_retrieve_arg *arg)
+{
+ return luo_session_retrieve_fd_arg(session_fd, token,
+ (__u64)(uintptr_t)arg);
+}
+
+/*
+ * Retrieve the file preserved with @token into the file @target. Return @target
+ * or -errno.
+ */
+static int retrieve_into(__u64 token, int target)
+{
+ struct liveupdate_shmem_retrieve_arg arg = {
+ .size = sizeof(arg),
+ .fd = target,
+ };
+
+ return retrieve_arg(token, &arg);
+}
+
+/* Create an empty, unlinked file on the tmpfs mount of the directory @dir. */
+static int new_target(const char *dir)
+{
+ return open(dir, O_TMPFILE | O_RDWR, 0644);
+}
+
+/*
+ * Retrieve the file preserved with @token into a new file created with
+ * new_target(@dir). Return the fd of the new file or -errno.
+ */
+static int retrieve_new(__u64 token, const char *dir)
+{
+ int target, ret;
+
+ target = new_target(dir);
+ if (target < 0)
+ return -errno;
+
+ ret = retrieve_into(token, target);
+ if (ret < 0)
+ close(target);
+ return ret;
+}
+
+/* Link the unlinked file @fd at @path. */
+static int link_fd(int fd, const char *path)
+{
+ char fd_path[64];
+
+ snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd);
+ return syscall(__NR_linkat, AT_FDCWD, fd_path, AT_FDCWD, path,
+ AT_SYMLINK_FOLLOW);
+}
+
+/* The contents of the files depend on their token. */
+static unsigned char pattern(__u64 token, size_t offset)
+{
+ unsigned int word;
+
+ word = (offset / 4 + 1) * 2654435761u ^ token * 2246822519u;
+ return word >> (offset % 4 * 8);
+}
+
+static int write_pattern(int fd, size_t offset, size_t len, __u64 token)
+{
+ static unsigned char buf[65536];
+ size_t i, n;
+
+ if (lseek(fd, offset, SEEK_SET) != (off_t)offset)
+ return -1;
+
+ for (; len; offset += n, len -= n) {
+ n = len < sizeof(buf) ? len : sizeof(buf);
+ for (i = 0; i < n; i++)
+ buf[i] = pattern(token, offset + i);
+ if (write(fd, buf, n) != (ssize_t)n)
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * Check that [start, end) of @p has the contents of the file preserved with
+ * @token, or zeroes if @token is 0.
+ */
+static int check_range(const unsigned char *p, size_t start, size_t end,
+ __u64 token)
+{
+ unsigned char expected;
+ size_t i;
+
+ for (i = start; i < end; i++) {
+ expected = token ? pattern(token, i) : 0;
+ if (p[i] != expected) {
+ ksft_print_msg("Byte %zu is %#x, expected %#x\n",
+ i, p[i], expected);
+ return KSFT_FAIL;
+ }
+ }
+
+ return KSFT_PASS;
+}
+
+/* Check that the file has the first @size bytes of the pattern for @token. */
+static int check_contents(int fd, size_t size, __u64 token)
+{
+ size_t len = nr_pages(size) * page_size;
+ unsigned char *p;
+ int ret;
+
+ if (!size)
+ return KSFT_PASS;
+
+ p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
+ CHECK(p != MAP_FAILED);
+
+ ret = check_range(p, 0, size, token);
+ /* The rest of the last page must be zero. */
+ if (ret == KSFT_PASS)
+ ret = check_range(p, size, len, 0);
+
+ munmap(p, len);
+ return ret;
+}
+
+/* Check that @fd has the size and contents of the source of @tc. */
+static int check_restored(struct test_case *tc, int fd)
+{
+ CHECK_EQ(file_size(fd), tc->size);
+ CHECK_EQ(file_pages(fd), nr_pages(tc->size));
+ return check_contents(fd, tc->size, tc->token);
+}
+
+/* Create a file with the first @size bytes of the pattern for @token. */
+static int create_source(const char *path, size_t size, __u64 token)
+{
+ int fd;
+
+ fd = open(path, O_CREAT | O_EXCL | O_RDWR, 0644);
+ if (fd < 0)
+ return -1;
+
+ if (write_pattern(fd, 0, size, token)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+static int preserve_fd(int fd, off_t pos, __u64 token)
+{
+ CHECK_EQ(lseek(fd, pos, SEEK_SET), pos);
+ CHECK_EQ(luo_session_preserve_fd(session_fd, fd, token), 0);
+ return KSFT_PASS;
+}
+
+static int preserve_source(const char *path, size_t size, off_t pos,
+ __u64 token)
+{
+ int fd, ret;
+
+ fd = create_source(path, size, token);
+ CHECK(fd >= 0);
+ ret = preserve_fd(fd, pos, token);
+ close(fd);
+
+ return ret;
+}
+
+/* The default for test_case.preserve(). */
+static int preserve_file(struct test_case *tc)
+{
+ char path[128];
+
+ snprintf(path, sizeof(path), "src/%s", tc->name);
+ return preserve_source(path, tc->size, tc->pos, tc->token);
+}
+
+/* Retrieve the file of @tc into a new file on a new mount at dst. */
+static int restore_verify(struct test_case *tc)
+{
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int basic_verify(struct test_case *tc)
+{
+ struct stat target_st, st, path_st;
+ int target, fd, path_fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = open("dst", O_TMPFILE | O_RDWR, 0640);
+ CHECK(target >= 0);
+ CHECK(!fstat(target, &target_st));
+ CHECK_EQ(lseek(target, 77, SEEK_SET), 77);
+
+ /* The retrieve returns the fd of the target instead of a new one. */
+ fd = retrieve_into(tc->token, target);
+ CHECK_EQ(fd, target);
+ TRY(check_restored(tc, fd));
+ CHECK_EQ(mnt_used("dst"), nr_pages(tc->size));
+
+ /*
+ * Only the contents and size are restored. Everything else is like
+ * userspace created the file, including its position.
+ */
+ CHECK(!fstat(fd, &st));
+ CHECK_EQ(st.st_ino, target_st.st_ino);
+ CHECK_EQ(st.st_mode & 07777, 0640);
+ CHECK(S_ISREG(st.st_mode));
+ CHECK_EQ(st.st_nlink, 0);
+ CHECK_EQ(st.st_uid, 0);
+ CHECK_EQ(st.st_gid, 0);
+ CHECK_EQ(do_fcntl(fd, F_GETFL, 0) & (O_ACCMODE | O_TMPFILE),
+ O_RDWR | O_TMPFILE);
+ CHECK_EQ(lseek(fd, 0, SEEK_CUR), 77);
+
+ /* Like all tmpfs files, the file only has F_SEAL_SEAL. */
+ CHECK_EQ(do_fcntl(fd, F_GET_SEALS, 0), F_SEAL_SEAL);
+
+ /* The file can be linked into place, and opened by its path. */
+ CHECK_EQ(link_fd(fd, "dst/f"), 0);
+ CHECK(!stat("dst/f", &path_st));
+ CHECK_EQ(path_st.st_ino, st.st_ino);
+ CHECK_EQ(path_st.st_nlink, 1);
+ path_fd = open("dst/f", O_RDONLY);
+ CHECK(path_fd >= 0);
+ TRY(check_contents(path_fd, tc->size, tc->token));
+ CHECK_EQ(mnt_used("dst"), nr_pages(tc->size));
+
+ close(path_fd);
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int empty_verify(struct test_case *tc)
+{
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 1, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+ CHECK_EQ(mnt_used("dst"), 0);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int sparse_preserve(struct test_case *tc)
+{
+ size_t size = SPARSE_PAGES * page_size;
+ int fd;
+
+ /* Only the first and the last page have data. */
+ fd = open("src/sparse", O_CREAT | O_EXCL | O_RDWR, 0644);
+ CHECK(fd >= 0);
+ CHECK_EQ(ftruncate(fd, size), 0);
+ CHECK_EQ(write_pattern(fd, 0, page_size, tc->token), 0);
+ CHECK_EQ(write_pattern(fd, size - page_size, page_size, tc->token), 0);
+ CHECK_EQ(file_pages(fd), 2);
+
+ CHECK_EQ(lseek(fd, 0, SEEK_SET), 0);
+ CHECK_EQ(luo_session_preserve_fd(session_fd, fd, tc->token), 0);
+
+ /* Preserving the file fills the holes. */
+ CHECK_EQ(file_pages(fd), SPARSE_PAGES);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int sparse_verify(struct test_case *tc)
+{
+ size_t size = SPARSE_PAGES * page_size;
+ unsigned char *p;
+ bool data;
+ int fd, i;
+
+ CHECK_EQ(mount_tmpfs("dst", SPARSE_PAGES, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), size);
+ CHECK_EQ(file_pages(fd), SPARSE_PAGES);
+ CHECK_EQ(mnt_used("dst"), SPARSE_PAGES);
+
+ /* The holes read as zeroes. */
+ p = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ CHECK(p != MAP_FAILED);
+ for (i = 0; i < SPARSE_PAGES; i++) {
+ data = i == 0 || i == SPARSE_PAGES - 1;
+ TRY(check_range(p, i * page_size, (i + 1) * page_size,
+ data ? tc->token : 0));
+ }
+
+ munmap(p, size);
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int tmpfile_source_preserve(struct test_case *tc)
+{
+ int fd, ret;
+
+ /* An unlinked tmpfs file is still a tmpfs file. */
+ fd = open("src", O_TMPFILE | O_RDWR, 0644);
+ CHECK(fd >= 0);
+ CHECK_EQ(write_pattern(fd, 0, tc->size, tc->token), 0);
+ ret = preserve_fd(fd, tc->pos, tc->token);
+ close(fd);
+
+ return ret;
+}
+
+static int linked_target_verify(struct test_case *tc)
+{
+ int target, path_fd;
+
+ /* The target can also be created with a path. */
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = open("dst/f", O_CREAT | O_EXCL | O_RDWR, 0644);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ TRY(check_restored(tc, target));
+
+ path_fd = open("dst/f", O_RDONLY);
+ CHECK(path_fd >= 0);
+ TRY(check_contents(path_fd, tc->size, tc->token));
+
+ close(path_fd);
+ close(target);
+ return KSFT_PASS;
+}
+
+static struct test_case test_cases[] = {
+ /* The restored file. */
+ {
+ .name = "basic",
+ .size = 10000,
+ .pos = 1234,
+ .verify = basic_verify,
+ },
+ {
+ .name = "empty",
+ .verify = empty_verify,
+ },
+ {
+ .name = "sparse",
+ .preserve = sparse_preserve,
+ .verify = sparse_verify,
+ },
+ {
+ .name = "tmpfile_source",
+ .size = SMALL_SIZE,
+ .preserve = tmpfile_source_preserve,
+ .verify = restore_verify,
+ },
+ {
+ .name = "linked_target",
+ .size = SMALL_SIZE,
+ .verify = linked_target_verify,
+ },
+};
+
+static long read_taint(void)
+{
+ return read_long("/proc/sys/kernel/tainted");
+}
+
+static int check_taint(long old_taint)
+{
+ long taint = read_taint();
+
+ if (old_taint < 0 || taint < 0) {
+ ksft_print_msg("Failed to read the taint flags\n");
+ return KSFT_SKIP;
+ }
+
+ if (old_taint & BAD_TAINT) {
+ ksft_print_msg("The kernel was already tainted\n");
+ return KSFT_SKIP;
+ }
+
+ CHECK_EQ(taint & BAD_TAINT, 0);
+ return KSFT_PASS;
+}
+
+static void setup(int luo_fd)
+{
+ unsigned int i;
+
+ page_size = getpagesize();
+ umask(TEST_UMASK);
+
+ for (i = 0; i < ARRAY_SIZE(test_cases); i++)
+ test_cases[i].token = (i + 1) * 0x100;
+
+ if (mount_tmpfs(TEST_DIR, 16, "mode=0755"))
+ fail_exit("Failed to mount %s", TEST_DIR);
+}
+
+static void enter_case_dir(struct test_case *tc)
+{
+ char dir[128];
+
+ snprintf(dir, sizeof(dir), TEST_DIR "/%s", tc->name);
+ if ((mkdir(dir, 0755) && errno != EEXIST) || chdir(dir))
+ fail_exit("Failed to create %s", dir);
+}
+
+static void run_stage_1(int luo_fd)
+{
+ long taint = read_taint();
+ struct test_case *tc;
+ unsigned int i;
+ int ret;
+
+ ksft_print_msg("[STAGE 1] Creating state file for next stage (2)...\n");
+ create_state_file(luo_fd, STATE_SESSION_NAME, STATE_TOKEN, 2);
+
+ setup(luo_fd);
+ if (chdir(TEST_DIR) || mount_tmpfs("src", 4096, "huge=never"))
+ fail_exit("Failed to mount the source tmpfs");
+
+ session_fd = luo_create_session(luo_fd, SESSION_NAME);
+ if (session_fd < 0)
+ fail_exit("luo_create_session for '%s'", SESSION_NAME);
+
+ ksft_print_msg("[STAGE 1] Preserving the source files...\n");
+ for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
+ tc = &test_cases[i];
+ ret = tc->preserve ? tc->preserve(tc) : preserve_file(tc);
+ if (ret == KSFT_SKIP)
+ ksft_print_msg("[STAGE 1] %s: Skipped\n", tc->name);
+ else if (ret != KSFT_PASS)
+ ksft_exit_fail_msg("%s: Failed to preserve\n",
+ tc->name);
+ }
+
+ if (check_taint(taint) == KSFT_FAIL)
+ ksft_exit_fail_msg("The kernel was tainted in stage 1\n");
+
+ close(luo_fd);
+ daemonize_and_wait();
+}
+
+static void run_stage_2(int luo_fd, int state_session_fd)
+{
+ long taint = read_taint();
+ unsigned int i, nr_tests;
+ struct test_case *tc;
+ int stage;
+
+ ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
+ restore_and_read_stage(state_session_fd, STATE_TOKEN, &stage);
+ if (stage != 2)
+ fail_exit("Expected stage 2, but state file contains %d",
+ stage);
+
+ setup(luo_fd);
+ session_fd = luo_retrieve_session(luo_fd, SESSION_NAME);
+ if (session_fd < 0)
+ fail_exit("luo_retrieve_session for '%s'", SESSION_NAME);
+
+ nr_tests = ARRAY_SIZE(test_cases) + 1;
+
+ ksft_print_header();
+ ksft_set_plan(nr_tests);
+
+ for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
+ tc = &test_cases[i];
+ enter_case_dir(tc);
+ ksft_test_result_report(tc->verify(tc), "%s\n", tc->name);
+ }
+
+ if (luo_session_finish(session_fd))
+ fail_exit("luo_session_finish for '%s'", SESSION_NAME);
+ close(session_fd);
+
+ if (chdir("/") || syscall(__NR_umount2, TEST_DIR, MNT_DETACH) ||
+ rmdir(TEST_DIR))
+ fail_exit("Failed to clean up %s", TEST_DIR);
+
+ ksft_test_result_report(check_taint(taint), "no_kernel_warnings\n");
+
+ if (luo_session_finish(state_session_fd))
+ fail_exit("luo_session_finish for state session");
+ close(state_session_fd);
+
+ if (!ksft_get_fail_cnt())
+ ksft_print_msg("--- TMPFS TEST PASSED ---\n");
+
+ ksft_finished();
+}
+
+int main(int argc, char *argv[])
+{
+ return luo_test(argc, argv, STATE_SESSION_NAME,
+ run_stage_1, run_stage_2);
+}
diff --git a/tools/testing/selftests/liveupdate/run-vmtests.sh b/tools/testing/selftests/liveupdate/run-vmtests.sh
index 6f592aec0f85..9bf79e6c8791 100755
--- a/tools/testing/selftests/liveupdate/run-vmtests.sh
+++ b/tools/testing/selftests/liveupdate/run-vmtests.sh
@@ -20,6 +20,7 @@ TEST_NAMES=(
"luo_multi_session"
"luo_stress_files"
"luo_stress_sessions"
+ "luo_tmpfs"
)
function usage() {
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 16/19] selftests/liveupdate: Test memfds and LUO interactions in luo_tmpfs
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (14 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 15/19] selftests/liveupdate: Add a test for retrieving tmpfs files David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 17/19] selftests/liveupdate: Test the targets of retrieved tmpfs files David Matlack
` (2 subsequent siblings)
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add test cases to luo_tmpfs that check that memfds are retrieved as
memfds, with their seals and file position, and only without a retrieve
argument, while tmpfs files can only be retrieved with one.
Also check that invalid retrieve arguments fail and can be retried after
fixing them. Retrieving a token again fails with -EBUSY with an
argument, and returns the same open file without one. Files can't be
retrieved from outgoing sessions. A file that LUO already manages can't
be the target, whether it is preserved in an outgoing session or was
already retrieved.
Signed-off-by: David Matlack <dmatlack@google.com>
---
.../testing/selftests/liveupdate/luo_tmpfs.c | 310 ++++++++++++++++++
1 file changed, 310 insertions(+)
diff --git a/tools/testing/selftests/liveupdate/luo_tmpfs.c b/tools/testing/selftests/liveupdate/luo_tmpfs.c
index 4e6e035e8433..91a607752ee2 100644
--- a/tools/testing/selftests/liveupdate/luo_tmpfs.c
+++ b/tools/testing/selftests/liveupdate/luo_tmpfs.c
@@ -96,6 +96,19 @@
} \
} while (0)
+/* Check that a system call fails with @err. */
+#define CHECK_ERRNO(expr, err) do { \
+ long __ret = (expr); \
+ int __err = errno, __exp = (err); \
+ \
+ if (__ret != -1 || __err != __exp) { \
+ ksft_print_msg("%s:%d: %s: %ld, errno %d, expected %d\n", \
+ __func__, __LINE__, #expr, \
+ __ret, __err, __exp); \
+ return KSFT_FAIL; \
+ } \
+} while (0)
+
/* Call a function that returns a KSFT_* code, and return if it fails. */
#define TRY(expr) do { \
int __ret = (expr); \
@@ -121,6 +134,7 @@ struct test_case {
};
static unsigned long page_size;
+static int dev_fd;
static int session_fd;
/* nolibc doesn't have these, so use syscall() where needed. */
@@ -271,6 +285,15 @@ static int link_fd(int fd, const char *path)
AT_SYMLINK_FOLLOW);
}
+/* Open the file @fd again with @flags. */
+static int reopen_fd(int fd, int flags)
+{
+ char fd_path[64];
+
+ snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd);
+ return open(fd_path, flags);
+}
+
/* The contents of the files depend on their token. */
static unsigned char pattern(__u64 token, size_t offset)
{
@@ -397,6 +420,23 @@ static int preserve_file(struct test_case *tc)
return preserve_source(path, tc->size, tc->pos, tc->token);
}
+/* Preserve a memfd with the contents of the source of @tc. */
+static int preserve_memfd(struct test_case *tc, unsigned int flags,
+ unsigned int seals)
+{
+ int fd, ret;
+
+ fd = memfd_create(tc->name, flags);
+ CHECK(fd >= 0);
+ CHECK_EQ(write_pattern(fd, 0, tc->size, tc->token), 0);
+ if (seals)
+ CHECK_EQ(do_fcntl(fd, F_ADD_SEALS, seals), 0);
+ ret = preserve_fd(fd, tc->pos, tc->token);
+ close(fd);
+
+ return ret;
+}
+
/* Retrieve the file of @tc into a new file on a new mount at dst. */
static int restore_verify(struct test_case *tc)
{
@@ -560,6 +600,234 @@ static int linked_target_verify(struct test_case *tc)
return KSFT_PASS;
}
+static int memfd_preserve(struct test_case *tc)
+{
+ return preserve_memfd(tc, 0, 0);
+}
+
+static int memfd_verify(struct test_case *tc)
+{
+ struct stat st;
+ int fd;
+
+ /* memfds are only retrieved as memfds. */
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ CHECK_EQ(retrieve_new(tc->token, "dst"), -EINVAL);
+ CHECK_EQ(mnt_used("dst"), 0);
+
+ fd = luo_session_retrieve_fd(session_fd, tc->token);
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+ CHECK(!fstat(fd, &st));
+ CHECK_EQ(st.st_nlink, 0);
+ CHECK_EQ(do_fcntl(fd, F_GETFL, 0) & O_ACCMODE, O_RDWR);
+
+ /* Unlike for tmpfs files, the file position is restored. */
+ CHECK_EQ(lseek(fd, 0, SEEK_CUR), tc->pos);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+#define MEMFD_SEALS (F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)
+
+static int sealed_memfd_preserve(struct test_case *tc)
+{
+ return preserve_memfd(tc, MFD_ALLOW_SEALING, MEMFD_SEALS);
+}
+
+static int sealed_memfd_verify(struct test_case *tc)
+{
+ void *p;
+ int fd;
+
+ fd = luo_session_retrieve_fd(session_fd, tc->token);
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+ CHECK_EQ(do_fcntl(fd, F_GET_SEALS, 0), MEMFD_SEALS);
+ CHECK_ERRNO(write(fd, "x", 1), EPERM);
+ CHECK_ERRNO(ftruncate(fd, 0), EPERM);
+ p = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ CHECK(p == MAP_FAILED && errno == EPERM);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int bad_args_verify(struct test_case *tc)
+{
+ struct {
+ struct liveupdate_shmem_retrieve_arg arg;
+ __u64 extra;
+ } big = {};
+ struct liveupdate_shmem_retrieve_arg arg;
+ int target, path_fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = new_target("dst");
+ CHECK(target >= 0);
+
+ /* Files from tmpfs mounts are only retrieved into files on tmpfs. */
+ CHECK_EQ(luo_session_retrieve_fd(session_fd, tc->token), -EINVAL);
+
+ memset(&arg, 0, sizeof(arg));
+ arg.size = sizeof(arg);
+ arg.fd = target;
+
+ /* Problems with the argument itself. */
+ arg.size = offsetof(struct liveupdate_shmem_retrieve_arg, fd);
+ CHECK_EQ(retrieve_arg(tc->token, &arg), -EINVAL);
+ arg.size = 2 * page_size;
+ CHECK_EQ(retrieve_arg(tc->token, &arg), -E2BIG);
+ arg.size = sizeof(arg);
+
+ big.arg = arg;
+ big.arg.size = sizeof(big);
+ big.extra = 1;
+ CHECK_EQ(retrieve_arg(tc->token, &big.arg), -E2BIG);
+
+ /* Problems with the file descriptor. */
+ arg.fd = -1;
+ CHECK_EQ(retrieve_arg(tc->token, &arg), -EBADF);
+ path_fd = reopen_fd(target, O_PATH);
+ CHECK(path_fd >= 0);
+ arg.fd = path_fd;
+ CHECK_EQ(retrieve_arg(tc->token, &arg), -EBADF);
+ close(path_fd);
+ arg.fd = target;
+
+ CHECK_EQ(retrieve_arg(tc->token + 1, &arg), -ENOENT);
+ CHECK_EQ(file_size(target), 0);
+ CHECK_EQ(mnt_used("dst"), 0);
+
+ /* The failures didn't consume the preserved file. */
+ CHECK_EQ(retrieve_arg(tc->token, &arg), target);
+ TRY(check_restored(tc, target));
+
+ close(target);
+ return KSFT_PASS;
+}
+
+static int retrieve_twice_verify(struct test_case *tc)
+{
+ struct stat st, new_st;
+ int target, fd, new_fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+
+ /* The argument only applies to the retrieve that restores the file. */
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EBUSY);
+ CHECK_EQ(file_size(target), 0);
+ close(target);
+
+ /* A plain retrieve returns the same open file. */
+ new_fd = luo_session_retrieve_fd(session_fd, tc->token);
+ CHECK(new_fd >= 0);
+ CHECK(!fstat(fd, &st));
+ CHECK(!fstat(new_fd, &new_st));
+ CHECK_EQ(new_st.st_dev, st.st_dev);
+ CHECK_EQ(new_st.st_ino, st.st_ino);
+ CHECK_EQ(lseek(new_fd, 77, SEEK_SET), 77);
+ CHECK_EQ(lseek(fd, 0, SEEK_CUR), 77);
+ CHECK_EQ(mnt_used("dst"), nr_pages(tc->size));
+
+ close(new_fd);
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int preserve_nothing(struct test_case *tc)
+{
+ return KSFT_PASS;
+}
+
+static int outgoing_session_verify(struct test_case *tc)
+{
+ struct liveupdate_shmem_retrieve_arg arg = {
+ .size = sizeof(arg),
+ };
+ int out_fd, source_fd;
+
+ out_fd = luo_create_session(dev_fd, SESSION_NAME "_outgoing_session");
+ CHECK(out_fd >= 0);
+ CHECK_EQ(mount_tmpfs("src", 16, NULL), 0);
+ source_fd = new_target("src");
+ CHECK(source_fd >= 0);
+ CHECK_EQ(luo_session_preserve_fd(out_fd, source_fd, tc->token), 0);
+
+ /* Files can only be retrieved from incoming sessions. */
+ arg.fd = new_target("src");
+ CHECK(arg.fd >= 0);
+ CHECK_EQ(luo_session_retrieve_fd_arg(out_fd, tc->token,
+ (__u64)(uintptr_t)&arg), -EINVAL);
+
+ close(arg.fd);
+ close(source_fd);
+ close(out_fd);
+ return KSFT_PASS;
+}
+
+static int target_preserved_verify(struct test_case *tc)
+{
+ int out_fd, target;
+
+ /* A file preserved in an outgoing session can't be the target. */
+ out_fd = luo_create_session(dev_fd, SESSION_NAME "_target_preserved");
+ CHECK(out_fd >= 0);
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(luo_session_preserve_fd(out_fd, target, tc->token), 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EBUSY);
+ CHECK_EQ(file_size(target), 0);
+
+ /* Once the session is closed, the file is no longer preserved. */
+ close(out_fd);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ TRY(check_restored(tc, target));
+
+ close(target);
+ return KSFT_PASS;
+}
+
+static int target_retrieved_preserve(struct test_case *tc)
+{
+ TRY(preserve_source("src/target_retrieved_empty", 0, 0, tc->token));
+ return preserve_source("src/target_retrieved", tc->size, 0,
+ tc->token + 1);
+}
+
+static int target_retrieved_verify(struct test_case *tc)
+{
+ int target, other_fd;
+
+ /* Retrieving an empty file leaves the target empty... */
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ CHECK_EQ(file_size(target), 0);
+
+ /* ... but LUO manages the file now, so it can't be a target again. */
+ CHECK_EQ(retrieve_into(tc->token + 1, target), -EBUSY);
+ CHECK_EQ(file_size(target), 0);
+
+ /* The failure didn't consume the preserved file. */
+ other_fd = retrieve_new(tc->token + 1, "dst");
+ CHECK(other_fd >= 0);
+ CHECK_EQ(file_size(other_fd), tc->size);
+ TRY(check_contents(other_fd, tc->size, tc->token + 1));
+
+ close(other_fd);
+ close(target);
+ return KSFT_PASS;
+}
+
static struct test_case test_cases[] = {
/* The restored file. */
{
@@ -588,6 +856,47 @@ static struct test_case test_cases[] = {
.size = SMALL_SIZE,
.verify = linked_target_verify,
},
+ /* memfds. */
+ {
+ .name = "memfd",
+ .size = SMALL_SIZE,
+ .pos = 700,
+ .preserve = memfd_preserve,
+ .verify = memfd_verify,
+ },
+ {
+ .name = "sealed_memfd",
+ .size = SMALL_SIZE,
+ .preserve = sealed_memfd_preserve,
+ .verify = sealed_memfd_verify,
+ },
+ /* Interactions with LUO. */
+ {
+ .name = "bad_args",
+ .size = SMALL_SIZE,
+ .verify = bad_args_verify,
+ },
+ {
+ .name = "retrieve_twice",
+ .size = SMALL_SIZE,
+ .verify = retrieve_twice_verify,
+ },
+ {
+ .name = "outgoing_session",
+ .preserve = preserve_nothing,
+ .verify = outgoing_session_verify,
+ },
+ {
+ .name = "target_preserved",
+ .size = SMALL_SIZE,
+ .verify = target_preserved_verify,
+ },
+ {
+ .name = "target_retrieved",
+ .size = SMALL_SIZE,
+ .preserve = target_retrieved_preserve,
+ .verify = target_retrieved_verify,
+ },
};
static long read_taint(void)
@@ -617,6 +926,7 @@ static void setup(int luo_fd)
{
unsigned int i;
+ dev_fd = luo_fd;
page_size = getpagesize();
umask(TEST_UMASK);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 17/19] selftests/liveupdate: Test the targets of retrieved tmpfs files
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (15 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 16/19] selftests/liveupdate: Test memfds and LUO interactions in luo_tmpfs David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 18/19] selftests/liveupdate: Test retrieving tmpfs files preserved read-only David Matlack
2026-11-11 0:23 ` [PATCH v4 19/19] selftests/liveupdate: Test block accounting of retrieved tmpfs files David Matlack
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add test cases to luo_tmpfs for the file that a tmpfs file is retrieved
into. It must be opened for writing, but can be write-only, and it keeps
its file status flags. It must be an empty regular file on a tmpfs
mount, so files with data, a size or blocks past the end of the file are
rejected, and so are memfds, pipes, directories and immutable files. All
of these failures can be retried with another file.
Also check that an unprivileged process can retrieve into a file it
created, which keeps its owner and mode, that a running binary can be
preserved through a read-only fd, and that the retrieved binary can't be
executed until the session is finished, since LUO keeps it open for
writing until then. The exec test case runs a copy of the test binary.
Signed-off-by: David Matlack <dmatlack@google.com>
---
.../testing/selftests/liveupdate/luo_tmpfs.c | 490 +++++++++++++++++-
1 file changed, 488 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/liveupdate/luo_tmpfs.c b/tools/testing/selftests/liveupdate/luo_tmpfs.c
index 91a607752ee2..2d6dd8f0e138 100644
--- a/tools/testing/selftests/liveupdate/luo_tmpfs.c
+++ b/tools/testing/selftests/liveupdate/luo_tmpfs.c
@@ -69,8 +69,13 @@
#define SMALL_SIZE 8192
#define SPARSE_PAGES 8
+#define NOBODY 65534
#define TEST_UMASK 022
+/* The exec case runs a copy of this test with EXEC_ARG, see exec_main(). */
+#define EXEC_ARG "--exec"
+#define EXEC_STATUS 200
+
/* TAINT_BAD_PAGE, TAINT_DIE and TAINT_WARN. */
#define BAD_TAINT ((1L << 5) | (1L << 7) | (1L << 9))
@@ -129,8 +134,12 @@ struct test_case {
int (*preserve)(struct test_case *tc);
/* Stage 2: Retrieve the files and check them. */
int (*verify)(struct test_case *tc);
+ /* Stage 2: Optional checks after the session is finished. */
+ int (*after_finish)(struct test_case *tc);
/* The tokens of the case are token to token + 0xff. */
__u64 token;
+ /* State kept from verify() for after_finish(). */
+ int result;
};
static unsigned long page_size;
@@ -143,6 +152,11 @@ static int do_fcntl(int fd, int cmd, long arg)
return syscall(__NR_fcntl, fd, cmd, arg);
}
+static int do_fallocate(int fd, int mode, off_t offset, off_t len)
+{
+ return syscall(__NR_fallocate, fd, mode, offset, len);
+}
+
static int do_mount(const char *source, const char *target, const char *type,
unsigned long flags, const char *data)
{
@@ -437,6 +451,200 @@ static int preserve_memfd(struct test_case *tc, unsigned int flags,
return ret;
}
+static int drop_privileges(void)
+{
+ CHECK_EQ(syscall(__NR_setgroups, 0, NULL), 0);
+ CHECK_EQ(syscall(__NR_setresgid, NOBODY, NOBODY, NOBODY), 0);
+ CHECK_EQ(syscall(__NR_setresuid, NOBODY, NOBODY, NOBODY), 0);
+ return KSFT_PASS;
+}
+
+/* Run @fn(@tc) in a child process, and return its result. */
+static int run_child(int (*fn)(struct test_case *tc), struct test_case *tc)
+{
+ int status, ret;
+ pid_t pid;
+
+ fflush(stdout);
+ pid = fork();
+ CHECK(pid >= 0);
+ if (!pid) {
+ ret = fn(tc);
+ fflush(stdout);
+ _exit(ret);
+ }
+
+ CHECK_EQ(waitpid(pid, &status, 0), pid);
+ CHECK(WIFEXITED(status));
+ return WEXITSTATUS(status);
+}
+
+/*
+ * A copy of this test that is executed with EXEC_ARG writes a 0 byte to stdout
+ * once it runs, and exits with EXEC_STATUS once stdin is closed.
+ */
+static int exec_main(void)
+{
+ char c;
+
+ if (write(STDOUT_FILENO, "", 1) != 1)
+ return 1;
+
+ while (read(STDIN_FILENO, &c, 1) > 0)
+ continue;
+
+ return EXEC_STATUS;
+}
+
+/* Copy the binary of this test to a new file at @path. */
+static int copy_self(const char *path)
+{
+ static char buf[65536];
+ int in_fd, out_fd;
+ ssize_t len;
+
+ in_fd = open("/proc/self/exe", O_RDONLY);
+ CHECK(in_fd >= 0);
+ out_fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0755);
+ CHECK(out_fd >= 0);
+
+ while ((len = read(in_fd, buf, sizeof(buf))) > 0)
+ CHECK_EQ(write(out_fd, buf, len), len);
+ CHECK_EQ(len, 0);
+
+ close(out_fd);
+ close(in_fd);
+ return KSFT_PASS;
+}
+
+/* Return the number of pages of the binary of this test. */
+static long self_pages(void)
+{
+ struct stat st;
+
+ if (stat("/proc/self/exe", &st))
+ return -1;
+
+ return nr_pages(st.st_size);
+}
+
+/* Check that @fd has the contents of the binary of this test. */
+static int check_self(int fd)
+{
+ long long size = file_size(fd);
+ void *p, *self;
+ int self_fd;
+
+ self_fd = open("/proc/self/exe", O_RDONLY);
+ CHECK(self_fd >= 0);
+ CHECK(size > 0);
+ CHECK_EQ(file_size(self_fd), size);
+
+ p = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ CHECK(p != MAP_FAILED);
+ self = mmap(NULL, size, PROT_READ, MAP_SHARED, self_fd, 0);
+ CHECK(self != MAP_FAILED);
+ CHECK(!memcmp(p, self, size));
+
+ munmap(self, size);
+ munmap(p, size);
+ close(self_fd);
+ return KSFT_PASS;
+}
+
+/*
+ * Execute the copy of this test at @path and wait until it runs. Return the
+ * write end of its stdin, which ends it when closed, or -errno if it can't be
+ * executed.
+ */
+static int start_exe(const char *path, pid_t *pid)
+{
+ char *const argv[] = { (char *)path, EXEC_ARG, NULL };
+ char *const envp[] = { NULL };
+ unsigned char err = EIO;
+ int in[2], out[2], status;
+
+ if (pipe2(in, O_CLOEXEC) || pipe2(out, O_CLOEXEC))
+ return -errno;
+
+ fflush(stdout);
+ *pid = fork();
+ if (!*pid) {
+ if (dup2(in[0], STDIN_FILENO) >= 0 &&
+ dup2(out[1], STDOUT_FILENO) >= 0)
+ execve(path, argv, envp);
+
+ /* Report the error instead of the 0 byte from exec_main(). */
+ err = errno;
+ if (write(out[1], &err, 1) != 1)
+ _exit(2);
+ _exit(1);
+ }
+
+ close(in[0]);
+ close(out[1]);
+ if (*pid > 0 && read(out[0], &err, 1) != 1)
+ err = EIO;
+ close(out[0]);
+ if (!err)
+ return in[1];
+
+ close(in[1]);
+ if (*pid > 0)
+ waitpid(*pid, &status, 0);
+ return -err;
+}
+
+/* End a program started by start_exe() and check its exit status. */
+static int stop_exe(pid_t pid, int stdin_fd)
+{
+ int status;
+
+ close(stdin_fd);
+ CHECK_EQ(waitpid(pid, &status, 0), pid);
+ CHECK(WIFEXITED(status));
+ CHECK_EQ(WEXITSTATUS(status), EXEC_STATUS);
+ return KSFT_PASS;
+}
+
+/* Execute the copy of this test at @path. Return 0 or -errno. */
+static int run_exe(const char *path)
+{
+ pid_t pid = -1;
+ int stdin_fd;
+
+ stdin_fd = start_exe(path, &pid);
+ if (stdin_fd < 0)
+ return stdin_fd;
+
+ return stop_exe(pid, stdin_fd) == KSFT_PASS ? 0 : -ECHILD;
+}
+
+/*
+ * Preserve a copy of the binary of this test while it runs, through a
+ * read-only fd, since it can't be opened for writing.
+ */
+static int preserve_running_exe(struct test_case *tc)
+{
+ int fd, stdin_fd;
+ char path[128];
+ pid_t pid;
+
+ snprintf(path, sizeof(path), "src/%s", tc->name);
+ TRY(copy_self(path));
+
+ stdin_fd = start_exe(path, &pid);
+ CHECK(stdin_fd >= 0);
+ CHECK_ERRNO(open(path, O_RDWR), ETXTBSY);
+
+ fd = open(path, O_RDONLY);
+ CHECK(fd >= 0);
+ CHECK_EQ(luo_session_preserve_fd(session_fd, fd, tc->token), 0);
+ close(fd);
+
+ return stop_exe(pid, stdin_fd);
+}
+
/* Retrieve the file of @tc into a new file on a new mount at dst. */
static int restore_verify(struct test_case *tc)
{
@@ -600,6 +808,229 @@ static int linked_target_verify(struct test_case *tc)
return KSFT_PASS;
}
+/* Set the inode flag @flag, e.g. FS_IMMUTABLE_FL, on the file at @path. */
+static int set_inode_flag(const char *path, int flag)
+{
+ int fd, flags, ret = 0;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) {
+ ret = -errno;
+ } else {
+ flags |= flag;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &flags))
+ ret = -errno;
+ }
+
+ close(fd);
+ return ret;
+}
+
+static int target_rdonly_verify(struct test_case *tc)
+{
+ int target, rd_fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = new_target("dst");
+ CHECK(target >= 0);
+ rd_fd = reopen_fd(target, O_RDONLY);
+ CHECK(rd_fd >= 0);
+
+ /* The target must be opened for writing. */
+ CHECK_EQ(retrieve_into(tc->token, rd_fd), -EBADF);
+ CHECK_EQ(file_size(target), 0);
+
+ /* The failure didn't consume the preserved file. */
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ TRY(check_restored(tc, target));
+
+ close(rd_fd);
+ close(target);
+ return KSFT_PASS;
+}
+
+static int target_wronly_verify(struct test_case *tc)
+{
+ int target, rd_fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = open("dst", O_TMPFILE | O_WRONLY | O_APPEND, 0644);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+
+ /* The target keeps its flags. */
+ CHECK_EQ(do_fcntl(target, F_GETFL, 0) & (O_ACCMODE | O_APPEND),
+ O_WRONLY | O_APPEND);
+ CHECK_EQ(file_size(target), tc->size);
+
+ rd_fd = reopen_fd(target, O_RDONLY);
+ CHECK(rd_fd >= 0);
+ TRY(check_contents(rd_fd, tc->size, tc->token));
+
+ close(rd_fd);
+ close(target);
+ return KSFT_PASS;
+}
+
+static int target_not_empty_verify(struct test_case *tc)
+{
+ int target, fd;
+
+ /* The target must be empty: It can't have data... */
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(write(target, "x", 1), 1);
+ CHECK_EQ(retrieve_into(tc->token, target), -EINVAL);
+ close(target);
+
+ /* ... a size... */
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(ftruncate(target, page_size), 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EINVAL);
+ close(target);
+
+ /* ... or blocks past the end of the file. */
+ target = new_target("dst");
+ CHECK(target >= 0);
+ CHECK_EQ(do_fallocate(target, FALLOC_FL_KEEP_SIZE, 0, page_size), 0);
+ CHECK_EQ(file_size(target), 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EINVAL);
+ close(target);
+
+ /* The failures didn't consume the preserved file. */
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int target_not_tmpfs_verify(struct test_case *tc)
+{
+ int target, pipe_fds[2], fd;
+
+ /* Like the preserved file, the target must be on a tmpfs mount. */
+ target = memfd_create(tc->name, 0);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EINVAL);
+ close(target);
+
+ CHECK_EQ(pipe2(pipe_fds, O_CLOEXEC), 0);
+ CHECK_EQ(retrieve_into(tc->token, pipe_fds[1]), -EINVAL);
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+
+ /* It must also be a regular file. */
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = open("dst", O_RDONLY | O_DIRECTORY);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -EINVAL);
+ close(target);
+
+ /* The failures didn't consume the preserved file. */
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int target_immutable_verify(struct test_case *tc)
+{
+ int target, fd, ret;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ target = open("dst/f", O_CREAT | O_EXCL | O_RDWR, 0644);
+ CHECK(target >= 0);
+ ret = set_inode_flag("dst/f", FS_IMMUTABLE_FL);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP) {
+ ksft_print_msg("%s: FS_IOC_SETFLAGS is not supported\n",
+ tc->name);
+ close(target);
+ return KSFT_SKIP;
+ }
+ CHECK_EQ(ret, 0);
+
+ /* An immutable file can't be the target, even if opened before. */
+ CHECK_EQ(retrieve_into(tc->token, target), -EPERM);
+ CHECK_EQ(file_size(target), 0);
+ close(target);
+
+ /* The failure didn't consume the preserved file. */
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int unprivileged_child(struct test_case *tc)
+{
+ struct stat st;
+ int target;
+
+ TRY(drop_privileges());
+
+ /* The file keeps the owner and mode it was created with. */
+ target = open("dst/nobody", O_TMPFILE | O_RDWR, 0600);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ TRY(check_restored(tc, target));
+ CHECK(!fstat(target, &st));
+ CHECK_EQ(st.st_uid, NOBODY);
+ CHECK_EQ(st.st_gid, NOBODY);
+ CHECK_EQ(st.st_mode & 07777, 0600);
+
+ close(target);
+ return KSFT_PASS;
+}
+
+static int unprivileged_verify(struct test_case *tc)
+{
+ CHECK_EQ(mount_tmpfs("dst", 16, "mode=0755"), 0);
+ CHECK_EQ(mkdir("dst/nobody", 0755), 0);
+ CHECK_EQ(chown("dst/nobody", NOBODY, NOBODY), 0);
+
+ return run_child(unprivileged_child, tc);
+}
+
+static int exec_verify(struct test_case *tc)
+{
+ long pages = self_pages();
+ int target;
+
+ CHECK(pages > 0);
+ CHECK_EQ(mount_tmpfs("dst", pages, NULL), 0);
+ target = open("dst", O_TMPFILE | O_RDWR, 0755);
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), target);
+ TRY(check_self(target));
+ CHECK_EQ(mnt_used("dst"), pages);
+ CHECK_EQ(link_fd(target, "dst/exe"), 0);
+ close(target);
+
+ /*
+ * LUO keeps the file open for writing until the session is finished,
+ * so the binary can't be executed before that.
+ */
+ CHECK_EQ(run_exe("dst/exe"), -ETXTBSY);
+ return KSFT_PASS;
+}
+
+static int exec_after_finish(struct test_case *tc)
+{
+ CHECK_EQ(run_exe("dst/exe"), 0);
+ return KSFT_PASS;
+}
+
static int memfd_preserve(struct test_case *tc)
{
return preserve_memfd(tc, 0, 0);
@@ -856,6 +1287,43 @@ static struct test_case test_cases[] = {
.size = SMALL_SIZE,
.verify = linked_target_verify,
},
+ /* The target file. */
+ {
+ .name = "target_rdonly",
+ .size = SMALL_SIZE,
+ .verify = target_rdonly_verify,
+ },
+ {
+ .name = "target_wronly",
+ .size = SMALL_SIZE,
+ .verify = target_wronly_verify,
+ },
+ {
+ .name = "target_not_empty",
+ .size = SMALL_SIZE,
+ .verify = target_not_empty_verify,
+ },
+ {
+ .name = "target_not_tmpfs",
+ .size = SMALL_SIZE,
+ .verify = target_not_tmpfs_verify,
+ },
+ {
+ .name = "target_immutable",
+ .size = SMALL_SIZE,
+ .verify = target_immutable_verify,
+ },
+ {
+ .name = "unprivileged",
+ .size = SMALL_SIZE,
+ .verify = unprivileged_verify,
+ },
+ {
+ .name = "exec",
+ .preserve = preserve_running_exe,
+ .verify = exec_verify,
+ .after_finish = exec_after_finish,
+ },
/* memfds. */
{
.name = "memfd",
@@ -987,7 +1455,7 @@ static void run_stage_2(int luo_fd, int state_session_fd)
long taint = read_taint();
unsigned int i, nr_tests;
struct test_case *tc;
- int stage;
+ int stage, ret;
ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
restore_and_read_stage(state_session_fd, STATE_TOKEN, &stage);
@@ -1001,6 +1469,8 @@ static void run_stage_2(int luo_fd, int state_session_fd)
fail_exit("luo_retrieve_session for '%s'", SESSION_NAME);
nr_tests = ARRAY_SIZE(test_cases) + 1;
+ for (i = 0; i < ARRAY_SIZE(test_cases); i++)
+ nr_tests += !!test_cases[i].after_finish;
ksft_print_header();
ksft_set_plan(nr_tests);
@@ -1008,13 +1478,26 @@ static void run_stage_2(int luo_fd, int state_session_fd)
for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
tc = &test_cases[i];
enter_case_dir(tc);
- ksft_test_result_report(tc->verify(tc), "%s\n", tc->name);
+ tc->result = tc->verify(tc);
+ ksft_test_result_report(tc->result, "%s\n", tc->name);
}
if (luo_session_finish(session_fd))
fail_exit("luo_session_finish for '%s'", SESSION_NAME);
close(session_fd);
+ for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
+ tc = &test_cases[i];
+ if (!tc->after_finish)
+ continue;
+
+ enter_case_dir(tc);
+ ret = KSFT_SKIP;
+ if (tc->result == KSFT_PASS)
+ ret = tc->after_finish(tc);
+ ksft_test_result_report(ret, "%s_after_finish\n", tc->name);
+ }
+
if (chdir("/") || syscall(__NR_umount2, TEST_DIR, MNT_DETACH) ||
rmdir(TEST_DIR))
fail_exit("Failed to clean up %s", TEST_DIR);
@@ -1033,6 +1516,9 @@ static void run_stage_2(int luo_fd, int state_session_fd)
int main(int argc, char *argv[])
{
+ if (argc == 2 && !strcmp(argv[1], EXEC_ARG))
+ return exec_main();
+
return luo_test(argc, argv, STATE_SESSION_NAME,
run_stage_1, run_stage_2);
}
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 18/19] selftests/liveupdate: Test retrieving tmpfs files preserved read-only
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (16 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 17/19] selftests/liveupdate: Test the targets of retrieved tmpfs files David Matlack
@ 2026-11-11 0:23 ` David Matlack
2026-11-11 0:23 ` [PATCH v4 19/19] selftests/liveupdate: Test block accounting of retrieved tmpfs files David Matlack
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add test cases to luo_tmpfs for source files preserved through a
read-only fd, through a read-only mount, on a read-only superblock, and
immutable files. Each of these cases except the first also preserves a
sparse file, whose holes are filled when it is preserved. Check that
the files are retrieved with their contents. Also check that the size of
a preserved file is frozen, even when the file is opened by its path,
and that writes within the file are preserved.
Signed-off-by: David Matlack <dmatlack@google.com>
---
.../testing/selftests/liveupdate/luo_tmpfs.c | 305 ++++++++++++++++++
1 file changed, 305 insertions(+)
diff --git a/tools/testing/selftests/liveupdate/luo_tmpfs.c b/tools/testing/selftests/liveupdate/luo_tmpfs.c
index 2d6dd8f0e138..0bd9b95f0575 100644
--- a/tools/testing/selftests/liveupdate/luo_tmpfs.c
+++ b/tools/testing/selftests/liveupdate/luo_tmpfs.c
@@ -451,6 +451,13 @@ static int preserve_memfd(struct test_case *tc, unsigned int flags,
return ret;
}
+/* Skip a case whose source file couldn't be preserved in stage 1. */
+static int skip_not_preserved(struct test_case *tc)
+{
+ ksft_print_msg("%s: The source file was not preserved\n", tc->name);
+ return KSFT_SKIP;
+}
+
static int drop_privileges(void)
{
CHECK_EQ(syscall(__NR_setgroups, 0, NULL), 0);
@@ -808,6 +815,175 @@ static int linked_target_verify(struct test_case *tc)
return KSFT_PASS;
}
+static int rdonly_source_preserve(struct test_case *tc)
+{
+ int fd, ret;
+
+ fd = create_source("src/rdonly_source", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+
+ fd = open("src/rdonly_source", O_RDONLY);
+ CHECK(fd >= 0);
+ ret = preserve_fd(fd, tc->pos, tc->token);
+ close(fd);
+
+ return ret;
+}
+
+/* Create a sparse file at @path, with a hole after the first page. */
+static int create_sparse_source(const char *path, __u64 token)
+{
+ int fd;
+
+ fd = create_source(path, page_size, token);
+ if (fd < 0)
+ return -1;
+
+ if (ftruncate(fd, 2 * page_size)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/* Open @path with O_RDONLY and try to preserve it with @token. */
+static int try_preserve_path(const char *path, __u64 token)
+{
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ ret = luo_session_preserve_fd(session_fd, fd, token);
+ close(fd);
+ return ret;
+}
+
+/* Check that the file preserved with @token has the source of @tc. */
+static int check_preserved(struct test_case *tc, __u64 token, size_t size)
+{
+ int fd;
+
+ fd = retrieve_new(token, "dst");
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), size);
+ TRY(check_contents(fd, size, tc->token));
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+/* Check that the file preserved with @token is a create_sparse_source(). */
+static int check_sparse_preserved(struct test_case *tc, __u64 token)
+{
+ unsigned char *p;
+ int fd;
+
+ fd = retrieve_new(token, "dst");
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), 2 * page_size);
+
+ /* Preserving the file filled the hole. */
+ CHECK_EQ(file_pages(fd), 2);
+ p = mmap(NULL, 2 * page_size, PROT_READ, MAP_SHARED, fd, 0);
+ CHECK(p != MAP_FAILED);
+ TRY(check_range(p, 0, page_size, tc->token));
+ TRY(check_range(p, page_size, 2 * page_size, 0));
+
+ munmap(p, 2 * page_size);
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int rdonly_mount_source_preserve(struct test_case *tc)
+{
+ int fd;
+
+ fd = create_source("src/rdonly_mount_source", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+ fd = create_sparse_source("src/rdonly_mount_sparse", tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+
+ CHECK_EQ(mkdir("src_ro", 0755), 0);
+ CHECK_EQ(do_mount("src", "src_ro", NULL, MS_BIND, NULL), 0);
+ CHECK_EQ(do_mount("none", "src_ro", NULL,
+ MS_REMOUNT | MS_BIND | MS_RDONLY, NULL), 0);
+
+ /*
+ * Files can be preserved through a read-only mount. Their holes are
+ * filled as if by read faults, which work through read-only mounts too.
+ */
+ CHECK_EQ(try_preserve_path("src_ro/rdonly_mount_source", tc->token), 0);
+ CHECK_EQ(try_preserve_path("src_ro/rdonly_mount_sparse", tc->token + 1),
+ 0);
+ return KSFT_PASS;
+}
+
+static int rdonly_mount_source_verify(struct test_case *tc)
+{
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ TRY(check_preserved(tc, tc->token, tc->size));
+ TRY(check_sparse_preserved(tc, tc->token + 1));
+ return KSFT_PASS;
+}
+
+static int rdonly_sb_source_child(struct test_case *tc)
+{
+ TRY(drop_privileges());
+
+ /* The owner can preserve a file whose size can't change anyway... */
+ CHECK_EQ(try_preserve_path("src_rosb/own", tc->token + 1), 0);
+
+ /* ... but nobody else can. */
+ CHECK_EQ(try_preserve_path("src_rosb/other", tc->token + 2), -EROFS);
+ return KSFT_PASS;
+}
+
+static int rdonly_sb_source_preserve(struct test_case *tc)
+{
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("src_rosb", 16, NULL), 0);
+ fd = create_source("src_rosb/root", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+ fd = create_source("src_rosb/own", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+ CHECK_EQ(chown("src_rosb/own", NOBODY, NOBODY), 0);
+ fd = create_source("src_rosb/other", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+ fd = create_sparse_source("src_rosb/sparse", tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+
+ CHECK_EQ(do_mount(NULL, "src_rosb", NULL, MS_REMOUNT | MS_RDONLY, NULL),
+ 0);
+
+ CHECK_EQ(try_preserve_path("src_rosb/root", tc->token), 0);
+ TRY(run_child(rdonly_sb_source_child, tc));
+
+ /* Holes are filled as if by read faults, even on a read-only sb. */
+ CHECK_EQ(try_preserve_path("src_rosb/sparse", tc->token + 3), 0);
+ return KSFT_PASS;
+}
+
+static int rdonly_sb_source_verify(struct test_case *tc)
+{
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ TRY(check_preserved(tc, tc->token, tc->size));
+ TRY(check_preserved(tc, tc->token + 1, tc->size));
+ CHECK_EQ(luo_session_retrieve_fd(session_fd, tc->token + 2), -ENOENT);
+ TRY(check_sparse_preserved(tc, tc->token + 3));
+ return KSFT_PASS;
+}
+
/* Set the inode flag @flag, e.g. FS_IMMUTABLE_FL, on the file at @path. */
static int set_inode_flag(const char *path, int flag)
{
@@ -829,6 +1005,104 @@ static int set_inode_flag(const char *path, int flag)
return ret;
}
+static int immutable_source_preserve(struct test_case *tc)
+{
+ int fd, ret;
+
+ fd = create_source("src/immutable_source", tc->size, tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+ fd = create_sparse_source("src/immutable_sparse", tc->token);
+ CHECK(fd >= 0);
+ close(fd);
+
+ ret = set_inode_flag("src/immutable_source", FS_IMMUTABLE_FL);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP) {
+ ksft_print_msg("%s: FS_IOC_SETFLAGS is not supported\n",
+ tc->name);
+ return KSFT_SKIP;
+ }
+ CHECK_EQ(ret, 0);
+ CHECK_EQ(set_inode_flag("src/immutable_sparse", FS_IMMUTABLE_FL), 0);
+
+ /*
+ * The size of an immutable file can't change anyway, so its owner can
+ * preserve it. Its holes are filled as if by read faults.
+ */
+ CHECK_EQ(try_preserve_path("src/immutable_source", tc->token), 0);
+ CHECK_EQ(try_preserve_path("src/immutable_sparse", tc->token + 1), 0);
+ return KSFT_PASS;
+}
+
+static int immutable_source_verify(struct test_case *tc)
+{
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ if (fd == -ENOENT)
+ return skip_not_preserved(tc);
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), tc->size);
+ TRY(check_contents(fd, tc->size, tc->token));
+ close(fd);
+
+ TRY(check_sparse_preserved(tc, tc->token + 1));
+ return KSFT_PASS;
+}
+
+#define FROZEN_OFFSET 100
+#define FROZEN_DATA "frozen"
+
+static int frozen_source_preserve(struct test_case *tc)
+{
+ size_t len = strlen(FROZEN_DATA);
+ int fd;
+
+ TRY(preserve_file(tc));
+
+ /* The size of a preserved file can't change, even through its path. */
+ fd = open("src/frozen_source", O_RDWR);
+ CHECK(fd >= 0);
+ CHECK_ERRNO(ftruncate(fd, tc->size / 2), EPERM);
+ CHECK_ERRNO(ftruncate(fd, tc->size * 2), EPERM);
+ CHECK_ERRNO(do_fallocate(fd, 0, 0, tc->size * 2), EPERM);
+ CHECK_ERRNO(do_fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
+ 0, page_size), EPERM);
+ CHECK_EQ(lseek(fd, tc->size, SEEK_SET), tc->size);
+ CHECK_ERRNO(write(fd, "x", 1), EPERM);
+ CHECK_EQ(file_size(fd), tc->size);
+
+ /* Writes within the file work, and the data is preserved. */
+ CHECK_EQ(lseek(fd, FROZEN_OFFSET, SEEK_SET), FROZEN_OFFSET);
+ CHECK_EQ(write(fd, FROZEN_DATA, len), len);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int frozen_source_verify(struct test_case *tc)
+{
+ size_t len = strlen(FROZEN_DATA);
+ unsigned char *p;
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 16, NULL), 0);
+ fd = retrieve_new(tc->token, "dst");
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), tc->size);
+
+ p = mmap(NULL, tc->size, PROT_READ, MAP_SHARED, fd, 0);
+ CHECK(p != MAP_FAILED);
+ TRY(check_range(p, 0, FROZEN_OFFSET, tc->token));
+ CHECK(!memcmp(p + FROZEN_OFFSET, FROZEN_DATA, len));
+ TRY(check_range(p, FROZEN_OFFSET + len, tc->size, tc->token));
+
+ munmap(p, tc->size);
+ close(fd);
+ return KSFT_PASS;
+}
+
static int target_rdonly_verify(struct test_case *tc)
{
int target, rd_fd;
@@ -1287,6 +1561,37 @@ static struct test_case test_cases[] = {
.size = SMALL_SIZE,
.verify = linked_target_verify,
},
+ /* Preserving files. */
+ {
+ .name = "rdonly_source",
+ .size = SMALL_SIZE,
+ .preserve = rdonly_source_preserve,
+ .verify = restore_verify,
+ },
+ {
+ .name = "rdonly_mount_source",
+ .size = SMALL_SIZE,
+ .preserve = rdonly_mount_source_preserve,
+ .verify = rdonly_mount_source_verify,
+ },
+ {
+ .name = "rdonly_sb_source",
+ .size = SMALL_SIZE,
+ .preserve = rdonly_sb_source_preserve,
+ .verify = rdonly_sb_source_verify,
+ },
+ {
+ .name = "immutable_source",
+ .size = SMALL_SIZE,
+ .preserve = immutable_source_preserve,
+ .verify = immutable_source_verify,
+ },
+ {
+ .name = "frozen_source",
+ .size = SMALL_SIZE,
+ .preserve = frozen_source_preserve,
+ .verify = frozen_source_verify,
+ },
/* The target file. */
{
.name = "target_rdonly",
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v4 19/19] selftests/liveupdate: Test block accounting of retrieved tmpfs files
2026-11-11 0:23 [PATCH v4 00/19] liveupdate: Preserve tmpfs files across Live Update David Matlack
` (17 preceding siblings ...)
2026-11-11 0:23 ` [PATCH v4 18/19] selftests/liveupdate: Test retrieving tmpfs files preserved read-only David Matlack
@ 2026-11-11 0:23 ` David Matlack
18 siblings, 0 replies; 20+ messages in thread
From: David Matlack @ 2026-11-11 0:23 UTC (permalink / raw)
To: kexec, linux-doc, linux-kernel, linux-kselftest, linux-mm
Cc: Alexander Graf, Andrew Morton, Baolin Wang, Christian Brauner,
David Hildenbrand, Hugh Dickins, Jonathan Corbet,
Liam R. Howlett, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Pasha Tatashin, Pratyush Yadav, Randy Dunlap, Samiullah Khawaja,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, David Matlack
Add test cases to luo_tmpfs for how the blocks of a retrieved file are
charged: The file fits a mount of exactly its size, and a large folio
that extends past the end of the file is charged in full. Running out of
space on the mount, or hitting the memcg limit, fails permanently,
leaves the target empty and nothing charged to the mount or the memcg,
and the session can still be finished.
Enable CONFIG_TRANSPARENT_HUGEPAGE for the large folio test cases, and
CONFIG_CGROUPS and CONFIG_MEMCG for the memcg test case. They are
skipped without them.
tmpfs quotas are not tested.
Signed-off-by: David Matlack <dmatlack@google.com>
---
tools/testing/selftests/liveupdate/config | 3 +
.../testing/selftests/liveupdate/luo_tmpfs.c | 278 +++++++++++++++++-
2 files changed, 279 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/liveupdate/config b/tools/testing/selftests/liveupdate/config
index 37230f4c488f..7f38291c979d 100644
--- a/tools/testing/selftests/liveupdate/config
+++ b/tools/testing/selftests/liveupdate/config
@@ -1,4 +1,5 @@
CONFIG_BLK_DEV_INITRD=y
+CONFIG_CGROUPS=y
CONFIG_DEVTMPFS=y
CONFIG_KEXEC_FILE=y
CONFIG_KEXEC_HANDOVER=y
@@ -7,7 +8,9 @@ CONFIG_KEXEC_HANDOVER_DEBUGFS=y
CONFIG_KEXEC_HANDOVER_DEBUG=y
CONFIG_LIVEUPDATE=y
CONFIG_LIVEUPDATE_TEST=y
+CONFIG_MEMCG=y
CONFIG_MEMFD_CREATE=y
CONFIG_TMPFS=y
CONFIG_TMPFS_XATTR=y
CONFIG_SHMEM=y
+CONFIG_TRANSPARENT_HUGEPAGE=y
diff --git a/tools/testing/selftests/liveupdate/luo_tmpfs.c b/tools/testing/selftests/liveupdate/luo_tmpfs.c
index 0bd9b95f0575..783c46715fc1 100644
--- a/tools/testing/selftests/liveupdate/luo_tmpfs.c
+++ b/tools/testing/selftests/liveupdate/luo_tmpfs.c
@@ -62,8 +62,9 @@
/*
* Everything happens on a tmpfs mounted at TEST_DIR. In stage 1, the source
- * files are created on the tmpfs mounted at TEST_DIR/src. In stage 2, each
- * case runs in the directory TEST_DIR/<name of the case>.
+ * files are created on the tmpfs mounted at TEST_DIR/src, or at
+ * TEST_DIR/src_huge for files that need large folios. In stage 2, each case
+ * runs in the directory TEST_DIR/<name of the case>.
*/
#define TEST_DIR "/luo_tmpfs"
@@ -145,6 +146,7 @@ struct test_case {
static unsigned long page_size;
static int dev_fd;
static int session_fd;
+static bool have_huge_src;
/* nolibc doesn't have these, so use syscall() where needed. */
static int do_fcntl(int fd, int cmd, long arg)
@@ -209,6 +211,16 @@ static long mnt_used(const char *path)
return buf.f_blocks - buf.f_bfree;
}
+static long mnt_free(const char *path)
+{
+ struct statfs buf;
+
+ if (do_statfs(path, &buf))
+ return -1;
+
+ return buf.f_bfree;
+}
+
/* Mount a tmpfs with room for @pages pages at @dir. */
static int mount_tmpfs(const char *dir, unsigned long pages, const char *opts)
{
@@ -244,6 +256,26 @@ static long read_long(const char *path)
return strtol(buf, NULL, 10);
}
+/* Write @str to the file at @path. Return 0 or -errno. */
+static int write_file(const char *path, const char *str)
+{
+ ssize_t len = strlen(str), ret;
+ int fd;
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, str, len);
+ if (ret != len)
+ ret = ret < 0 ? -errno : -EIO;
+ else
+ ret = 0;
+
+ close(fd);
+ return ret;
+}
+
/* Retrieve with the argument @arg. Return the retrieved fd or -errno. */
static int retrieve_arg(__u64 token, struct liveupdate_shmem_retrieve_arg *arg)
{
@@ -451,6 +483,33 @@ static int preserve_memfd(struct test_case *tc, unsigned int flags,
return ret;
}
+/* Preserve a file from the tmpfs mounted with huge=always. */
+static int preserve_huge_file(struct test_case *tc)
+{
+ char path[128];
+ int fd, ret;
+
+ if (!have_huge_src)
+ return KSFT_SKIP;
+
+ snprintf(path, sizeof(path), "src_huge/%s", tc->name);
+ fd = create_source(path, tc->size, tc->token);
+ CHECK(fd >= 0);
+
+ /* The test needs the last, partial page to be in a large folio. */
+ if (tc->size % page_size && file_pages(fd) <= nr_pages(tc->size)) {
+ ksft_print_msg("%s: No large folio for the source file\n",
+ tc->name);
+ close(fd);
+ return KSFT_SKIP;
+ }
+
+ ret = preserve_fd(fd, tc->pos, tc->token);
+ close(fd);
+
+ return ret;
+}
+
/* Skip a case whose source file couldn't be preserved in stage 1. */
static int skip_not_preserved(struct test_case *tc)
{
@@ -1305,6 +1364,181 @@ static int exec_after_finish(struct test_case *tc)
return KSFT_PASS;
}
+static int mount_fits_verify(struct test_case *tc)
+{
+ unsigned long pages = nr_pages(tc->size);
+ int fd;
+
+ /* The file can be retrieved into a mount it fits exactly. */
+ CHECK_EQ(mount_tmpfs("fit", pages, NULL), 0);
+ fd = retrieve_new(tc->token, "fit");
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+ CHECK_EQ(mnt_used("fit"), pages);
+ CHECK_EQ(mnt_free("fit"), 0);
+
+ CHECK_EQ(ftruncate(fd, 0), 0);
+ CHECK_EQ(mnt_used("fit"), 0);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int mount_too_small_verify(struct test_case *tc)
+{
+ unsigned long pages = nr_pages(tc->size);
+ int target;
+
+ /* The failure leaves the target empty, and the mount untouched... */
+ CHECK_EQ(mount_tmpfs("small", pages - 1, NULL), 0);
+ target = new_target("small");
+ CHECK(target >= 0);
+ CHECK_EQ(retrieve_into(tc->token, target), -ENOSPC);
+ CHECK_EQ(file_size(target), 0);
+ CHECK_EQ(file_pages(target), 0);
+ CHECK_EQ(mnt_used("small"), 0);
+ close(target);
+
+ /*
+ * ... but the preserved contents were restored before it was detected,
+ * so the failure is permanent. Finishing the session frees them.
+ */
+ CHECK_EQ(mount_tmpfs("big", pages, NULL), 0);
+ CHECK_EQ(retrieve_new(tc->token, "big"), -ENOSPC);
+ CHECK_EQ(luo_session_retrieve_fd(session_fd, tc->token), -ENOSPC);
+ CHECK_EQ(mnt_used("big"), 0);
+
+ return KSFT_PASS;
+}
+
+static int huge_folios_verify(struct test_case *tc)
+{
+ unsigned long pages = nr_pages(tc->size);
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", pages, "huge=never"), 0);
+ fd = retrieve_new(tc->token, "dst");
+ if (fd == -ENOENT)
+ return skip_not_preserved(tc);
+ CHECK(fd >= 0);
+ TRY(check_restored(tc, fd));
+ CHECK_EQ(mnt_used("dst"), pages);
+
+ /* LUO keeps a reference to the file until the session is finished. */
+ close(fd);
+ CHECK_EQ(mnt_used("dst"), pages);
+
+ return KSFT_PASS;
+}
+
+static int huge_folios_after_finish(struct test_case *tc)
+{
+ CHECK_EQ(mnt_used("dst"), 0);
+ return KSFT_PASS;
+}
+
+static int huge_folio_tail_verify(struct test_case *tc)
+{
+ long pages;
+ int fd;
+
+ CHECK_EQ(mount_tmpfs("dst", 4096, "huge=never"), 0);
+ fd = retrieve_new(tc->token, "dst");
+ if (fd == -ENOENT)
+ return skip_not_preserved(tc);
+ CHECK(fd >= 0);
+ CHECK_EQ(file_size(fd), tc->size);
+ TRY(check_contents(fd, tc->size, tc->token));
+
+ /* The whole folio is charged, including the part past EOF. */
+ pages = file_pages(fd);
+ CHECK(pages > 1);
+ CHECK_EQ(mnt_used("dst"), pages);
+
+ CHECK_EQ(ftruncate(fd, 0), 0);
+ CHECK_EQ(mnt_used("dst"), 0);
+
+ close(fd);
+ return KSFT_PASS;
+}
+
+static int huge_folio_tail_enospc_verify(struct test_case *tc)
+{
+ int ret;
+
+ /* There is room for the file, but not for its last folio. */
+ CHECK_EQ(mount_tmpfs("small", 1, "huge=never"), 0);
+ ret = retrieve_new(tc->token, "small");
+ if (ret == -ENOENT)
+ return skip_not_preserved(tc);
+ CHECK_EQ(ret, -ENOSPC);
+ CHECK_EQ(mnt_used("small"), 0);
+
+ return KSFT_PASS;
+}
+
+static int memcg_limit_child(struct test_case *tc)
+{
+ long limit = tc->size / 2, usage;
+ int ret;
+
+ /*
+ * Charging small folios may invoke the memcg OOM killer. Protect the
+ * child, so that the retrieve fails with -ENOMEM instead. Charging a
+ * large folio doesn't invoke it.
+ */
+ CHECK_EQ(write_file("/proc/self/oom_score_adj", "-1000"), 0);
+ CHECK_EQ(write_file("memcg/small/cgroup.procs", "0"), 0);
+
+ /* The folios are charged to the memcg of the caller, which is full. */
+ ret = retrieve_new(tc->token, "dst");
+ if (ret == -ENOENT)
+ return skip_not_preserved(tc);
+ CHECK_EQ(ret, -ENOMEM);
+
+ /* The failure leaves the mount and the memcg untouched. */
+ CHECK_EQ(mnt_used("dst"), 0);
+ usage = read_long("memcg/small/memory.current");
+ CHECK(usage >= 0 && usage < limit);
+
+ return KSFT_PASS;
+}
+
+static int memcg_limit_verify(struct test_case *tc)
+{
+ unsigned long pages = nr_pages(tc->size);
+ char limit[32];
+ int ret;
+
+ CHECK_EQ(mkdir("memcg", 0755), 0);
+ if (do_mount("cgroup2", "memcg", "cgroup2", 0, NULL)) {
+ ksft_print_msg("%s: cgroup2 is not available\n", tc->name);
+ return KSFT_SKIP;
+ }
+ if (write_file("memcg/cgroup.subtree_control", "+memory")) {
+ ksft_print_msg("%s: The memory controller is not available\n",
+ tc->name);
+ return KSFT_SKIP;
+ }
+
+ /* A memcg without room for the file. */
+ CHECK_EQ(mkdir("memcg/small", 0755), 0);
+ snprintf(limit, sizeof(limit), "%zu", tc->size * 3 / 4);
+ CHECK_EQ(write_file("memcg/small/memory.max", limit), 0);
+ CHECK_EQ(mount_tmpfs("dst", pages, "huge=never"), 0);
+
+ ret = run_child(memcg_limit_child, tc);
+ rmdir("memcg/small");
+ if (ret != KSFT_PASS)
+ return ret;
+
+ /* The preserved contents were restored, so the failure is permanent. */
+ CHECK_EQ(retrieve_new(tc->token, "dst"), -ENOMEM);
+ CHECK_EQ(mnt_used("dst"), 0);
+
+ return KSFT_PASS;
+}
+
static int memfd_preserve(struct test_case *tc)
{
return preserve_memfd(tc, 0, 0);
@@ -1629,6 +1863,42 @@ static struct test_case test_cases[] = {
.verify = exec_verify,
.after_finish = exec_after_finish,
},
+ /* Block accounting. */
+ {
+ .name = "mount_fits",
+ .size = 2 << 20,
+ .verify = mount_fits_verify,
+ },
+ {
+ .name = "mount_too_small",
+ .size = 2 << 20,
+ .verify = mount_too_small_verify,
+ },
+ {
+ .name = "huge_folios",
+ .size = 4 << 20,
+ .preserve = preserve_huge_file,
+ .verify = huge_folios_verify,
+ .after_finish = huge_folios_after_finish,
+ },
+ {
+ .name = "huge_folio_tail",
+ .size = 100,
+ .preserve = preserve_huge_file,
+ .verify = huge_folio_tail_verify,
+ },
+ {
+ .name = "huge_folio_tail_enospc",
+ .size = 100,
+ .preserve = preserve_huge_file,
+ .verify = huge_folio_tail_enospc_verify,
+ },
+ {
+ .name = "memcg_limit",
+ .size = 4 << 20,
+ .preserve = preserve_huge_file,
+ .verify = memcg_limit_verify,
+ },
/* memfds. */
{
.name = "memfd",
@@ -1733,6 +2003,10 @@ static void run_stage_1(int luo_fd)
if (chdir(TEST_DIR) || mount_tmpfs("src", 4096, "huge=never"))
fail_exit("Failed to mount the source tmpfs");
+ have_huge_src = !mount_tmpfs("src_huge", 4096, "huge=always");
+ if (!have_huge_src)
+ ksft_print_msg("[STAGE 1] huge=always is not supported\n");
+
session_fd = luo_create_session(luo_fd, SESSION_NAME);
if (session_fd < 0)
fail_exit("luo_create_session for '%s'", SESSION_NAME);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 20+ messages in thread