mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH 0/6] luo: tmpfs preservation
@ 2026-09-23 22:43 Pratyush Yadav
  2026-09-23 22:44 ` [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id Pratyush Yadav
                   ` (5 more replies)
  0 siblings, 6 replies; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:43 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

Hi,

I brought this idea up in this week's Hypervisor Live Update bi-weekly.
I decided to try using an LLM to see if it can produce a
proof-of-concept quickly. This series is the end result.

The main use case is preserving in-memory files that have a filesystem
path. We already support memfd preservation, but memfds can't be linked
to a (user-visible) filesystem. This is needed for storing VMM packages
for live update on hosts that don't have a disk. A cold boot fetches the
binaries from network, but that is too slow for a live update.

David tried to solve the problem by introducing
LIVEUPDATE_SESSION_RETRIEVE_INTO_FD [0], which lets you provide a FD for
LUO to retrieve into. This is an alternative to the idea. It uses the
standard preservation and retrieval API that LUO already provides.

The core idea is to allow userspace to preserve a tmpfs mount FD. Once
the mount is preserved, userspace can pass in regular files in that
mount for preservation. The files take a dependency on the mount token,
and that is used for retrieving the files in the right mount. This saves
us from doing a full FS preservation and makes preservation of each file
explicit.

Currently only files in the root are supported. Files in subdirectories
will be rejected. This is mainly for simplicity. Complex mount features
like memory policies, id mappings, or casefolding are also not
supported. All these can be reconfigured after retrieve if really
needed.

The code re-uses a lot of the preservation and retrieval logic from
memfd preservation. It only adds some extra file and mount metadata on
top.

As I mentioned earlier, this is heavily LLM generated. The code is not
very polished and has some rough edges. That said, I have read all the
code and did significant cleanups of the LLM output. This includes
turning the 1600 or so lines it generated to a more modest 977 lines.
So (I think) it isn't complete AI garbage. And I think it does get the
core idea across.

The big exception to this is the changes to and use of VFS APIs. I am
not competent with VFS at all so I have mostly taken the LLM at its word
and haven't done my homework to see if the usage even makes sense.
That's why I have not Cced any of the VFS maintainers. I'd rather spare
them the pain.

[0] https://lore.kernel.org/kexec/20260901180713.4185641-1-dmatlack@google.com/T/#u

Regards,
Pratyush Yadav

Pratyush Yadav (Google) (6):
  liveupdate: luo_file: look up outgoing tokens by id
  shmem: add tmpfs_create_mount() to create tmpfs mounts internally
  fs/namespace: Add vfs_open_detached_mount()
  mm/memfd_luo: allow preserving a tmpfs mount
  mm/memfd_luo: allow preserving a tmpfs file
  selftests/liveupdate: add tmpfs kexec test

 Documentation/core-api/liveupdate.rst         |   1 +
 Documentation/mm/index.rst                    |   1 +
 Documentation/mm/tmpfs_preservation.rst       |  24 +
 MAINTAINERS                                   |   1 +
 fs/namespace.c                                |  59 ++
 include/linux/kho/abi/tmpfs.h                 |  83 +++
 include/linux/liveupdate.h                    |   6 +-
 include/linux/mount.h                         |   1 +
 include/linux/shmem_fs.h                      |   5 +
 kernel/liveupdate/luo_file.c                  |  21 +-
 mm/internal.h                                 |   1 +
 mm/memfd_luo.c                                | 558 +++++++++++++++++-
 mm/shmem.c                                    |  43 +-
 tools/testing/selftests/liveupdate/Makefile   |   1 +
 .../selftests/liveupdate/luo_kexec_tmpfs.c    | 191 ++++++
 .../selftests/liveupdate/run-vmtests.sh       |   1 +
 16 files changed, 977 insertions(+), 20 deletions(-)
 create mode 100644 Documentation/mm/tmpfs_preservation.rst
 create mode 100644 include/linux/kho/abi/tmpfs.h
 create mode 100644 tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c


base-commit: db3db4c33a1cf89a201ad1a11c63f503b248ee32
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 22:55   ` sashiko-bot
  2026-09-23 22:44 ` [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally Pratyush Yadav
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

liveupdate_get_token_outgoing() is used to look up tokens of
dependencies. It does the search based on struct file. LUO preserves
resources; the files are only handles to those resources.

LUO provides file handlers a ->get_id() callback that can return the
unique identifier of a resource. LUO uses it to ensure the same resource
can't be preserved twice if it can be referred to by multiple files. The
default ID is the struct file, but file handlers can implement the
callback and return something else. For example, memfd returns the inode
of the file, not the struct file itself.

Use ID to look up dependencies in liveupdate_get_token_outgoing()
instead of struct file. This allows the lookup to work with handlers
that use things other than the struct file.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
 include/linux/liveupdate.h   |  6 +++---
 kernel/liveupdate/luo_file.c | 21 ++++++++++++---------
 2 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/include/linux/liveupdate.h b/include/linux/liveupdate.h
index 6051abc0612c..1cddbab50b98 100644
--- a/include/linux/liveupdate.h
+++ b/include/linux/liveupdate.h
@@ -253,9 +253,9 @@ void liveupdate_flb_put_outgoing(struct liveupdate_flb *flb);
 int liveupdate_get_file_incoming(struct liveupdate_session *s, u64 token,
 				 struct file **filep);
 
-/* Get a token for an outgoing file, or -ENOENT if file is not preserved */
+/* Get a token for a preserved object, or -ENOENT if it is not preserved */
 int liveupdate_get_token_outgoing(struct liveupdate_session *s,
-				  struct file *file, u64 *tokenp);
+				  unsigned long id, u64 *tokenp);
 
 #else /* CONFIG_LIVEUPDATE */
 
@@ -316,7 +316,7 @@ static inline int liveupdate_get_file_incoming(struct liveupdate_session *s,
 }
 
 static inline int liveupdate_get_token_outgoing(struct liveupdate_session *s,
-						struct file *file, u64 *tokenp)
+						unsigned long id, u64 *tokenp)
 {
 	return -EOPNOTSUPP;
 }
diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
index a39992da504f..51817d202ce2 100644
--- a/kernel/liveupdate/luo_file.c
+++ b/kernel/liveupdate/luo_file.c
@@ -950,29 +950,32 @@ void liveupdate_unregister_file_handler(struct liveupdate_file_handler *fh)
 }
 
 /**
- * liveupdate_get_token_outgoing - Get the token for a preserved file.
+ * liveupdate_get_token_outgoing - Get the token for a preserved object.
  * @s:      The outgoing liveupdate session.
- * @file:   The file object to search for.
+ * @id:     The identifier of the preserved object to search for, as returned
+ *          by the owning handler's ->get_id().
  * @tokenp: Output parameter for the found token.
  *
- * Searches the list of preserved files in an outgoing session for a matching
- * file object. If found, the corresponding user-provided token is returned.
+ * Searches the list of preserved files in an outgoing session for an object
+ * with a matching identifier. If found, the corresponding user-provided token
+ * is returned.
  *
- * This function is intended for in-kernel callers that need to correlate a
- * file with its liveupdate token.
+ * For handlers that do not implement ->get_id() use the 'struct file' pointer
+ * as their identifier.
  *
  * Context: It must be called with session mutex acquired.
- * Return: 0 on success, -ENOENT if the file is not preserved in this session.
+ * Return: 0 on success, -ENOENT if no such object is preserved in this
+ *         session.
  */
 int liveupdate_get_token_outgoing(struct liveupdate_session *s,
-				  struct file *file, u64 *tokenp)
+				  unsigned long id, u64 *tokenp)
 {
 	struct luo_file_set *file_set = luo_file_set_from_session_locked(s);
 	struct luo_file *luo_file;
 	int err = -ENOENT;
 
 	list_for_each_entry(luo_file, &file_set->files_list, list) {
-		if (luo_file->file == file) {
+		if (luo_get_id(luo_file->fh, luo_file->file) == id) {
 			if (tokenp)
 				*tokenp = luo_file->token;
 			err = 0;
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
  2026-09-23 22:44 ` [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 22:54   ` sashiko-bot
  2026-09-23 22:44 ` [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount() Pratyush Yadav
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

Upcoming commits will add support for preserving a tmpfs mount via LUO.
While the retrieve logic can create the mount by constructing an options
string from its preserved data, it is simpler to let it directly pass
the parameters.

tmpfs_create_mount() allows creating a mount with the specified
max_blocks and mode. The function only takes max_blocks and mode for now
because that is all the tmpfs preservation will use. All other values
are left to their defaults.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
 mm/internal.h |  1 +
 mm/shmem.c    | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+)

diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c9..d82c92c8fddd 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1436,6 +1436,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);
+struct vfsmount *tmpfs_create_mount(unsigned long max_blocks, umode_t mode);
 
 #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 897fa2b61346..8660405b6ab8 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -5313,6 +5313,44 @@ static struct file_system_type shmem_fs_type = {
 	.fs_flags	= FS_USERNS_MOUNT | FS_ALLOW_IDMAP | FS_MGTIME,
 };
 
+/**
+ * tmpfs_create_mount - Create a new tmpfs instance from explicit parameters.
+ * @max_blocks: Block limit of the filesystem, in PAGE_SIZE units, as held by
+ *              shmem_sb_info::max_blocks.
+ * @mode:       Mode of the root directory.
+ *
+ * Creates a new tmpfs superblock and returns a mount for it. Everything not
+ * named above is left at its default.
+ *
+ * The returned mount belongs to no mount namespace and is owned by the caller,
+ * who must either mntput() it or hand it to something that takes it over.
+ *
+ * Return: the new mount, or an ERR_PTR.
+ */
+struct vfsmount *tmpfs_create_mount(unsigned long max_blocks, umode_t mode)
+{
+	struct shmem_options *ctx;
+	struct fs_context *fc;
+	struct vfsmount *mnt;
+
+	if (!IS_ENABLED(CONFIG_TMPFS))
+		return ERR_PTR(-EOPNOTSUPP);
+
+	fc = fs_context_for_mount(&shmem_fs_type, 0);
+	if (IS_ERR(fc))
+		return ERR_CAST(fc);
+
+	ctx = fc->fs_private;
+	ctx->blocks = max_blocks;
+	ctx->seen |= SHMEM_SEEN_BLOCKS;
+	ctx->mode = mode;
+
+	mnt = fc_mount(fc);
+	put_fs_context(fc);
+
+	return mnt;
+}
+
 #if defined(CONFIG_SYSFS) && defined(CONFIG_TMPFS)
 
 #define __INIT_KOBJ_ATTR(_name, _mode, _show, _store)			\
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount()
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
  2026-09-23 22:44 ` [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id Pratyush Yadav
  2026-09-23 22:44 ` [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 23:01   ` sashiko-bot
  2026-09-23 22:44 ` [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount Pratyush Yadav
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

A mount created in the kernel by fc_mount() and friends has mnt_ns ==
NULL, and userspace cannot attach such a mount anywhere.

The missing step is the tail of fsmount(2): allocate an anonymous mount
namespace, insert the mount as its root, open an O_PATH file on it and
mark the file FMODE_NEED_UNMOUNT so the mount is torn down on the final
fput() if it is never attached. The anonymous namespace is what gives
the detached mount an owning user namespace for permission checks, a
place for child mounts, and a lifetime tied to the file.

None of the pieces are reachable from outside fs/namespace.c:
alloc_mnt_ns() and mnt_add_to_ns() are static, and the sequence is
already open-coded twice, in fsmount(2) and in open_detached_copy().

Factor it out so an in-kernel creator of a mount can hand it to
userspace. The Live Update Orchestrator needs this to return a tmpfs it
re-created from state preserved across a kexec; userspace attaches the
resulting fd with move_mount(), exactly as it would an fsmount(2) fd.

Only add the helper. Converting fsmount() and open_detached_copy() to it
is an obvious follow-up but is left out to keep this small. It is not
exported, as the only caller is built in.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>

---

This patch is entirely LLM generated. It works, but I am not competent
enough with VFS APIs to even guess if it is sensible. So please don't
take it too seriously. It makes the RFC testable, but for a proper
series I will do a lot more homework to make sure this doesn't
completely abuse VFS APIs.
---
 fs/namespace.c        | 59 +++++++++++++++++++++++++++++++++++++++++++
 include/linux/mount.h |  1 +
 2 files changed, 60 insertions(+)

diff --git a/fs/namespace.c b/fs/namespace.c
index 1ecd96c918b3..5e7ba2b61dd9 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -4435,6 +4435,65 @@ static unsigned int attr_flags_to_mnt_flags(u64 attr_flags)
 	return mnt_flags;
 }
 
+/**
+ * vfs_open_detached_mount - Publish a new mount as a detached mount file.
+ * @mnt: The mount to publish. Must not be attached to a mount namespace. The
+ *       caller's reference is consumed on success.
+ *
+ * Places @mnt into a new anonymous mount namespace and opens an O_PATH file on
+ * its root, marked FMODE_NEED_UNMOUNT. This is what fsmount(2) hands back, and
+ * the resulting file behaves the same way: it can be attached to the
+ * filesystem hierarchy with
+ *
+ *	move_mount(fd, "", dfd, path, MOVE_MOUNT_F_EMPTY_PATH)
+ *
+ * and the mount is torn down on the final fput() if it never was.
+ *
+ * A mount that is not the root of an anonymous mount namespace cannot be
+ * attached by userspace at all, so a mount freshly made by fc_mount() and
+ * friends has to go through here before it can be given away.
+ *
+ * Return: the new file, or an ERR_PTR. On failure the caller's reference to
+ *         @mnt is dropped, as the mount cannot be published.
+ */
+struct file *vfs_open_detached_mount(struct vfsmount *mnt)
+{
+	struct path path __free(path_put) = {};
+	struct mnt_namespace *ns;
+	struct file *file;
+
+	if (WARN_ON_ONCE(real_mount(mnt)->mnt_ns))
+		return ERR_PTR(-EINVAL);
+
+	ns = alloc_mnt_ns(current->nsproxy->mnt_ns->user_ns, true);
+	if (IS_ERR(ns)) {
+		mntput(mnt);
+		return ERR_CAST(ns);
+	}
+
+	/* The caller's reference becomes the namespace's reference. */
+	ns->root = real_mount(mnt);
+	ns->nr_mounts = 1;
+	mnt_add_to_ns(ns, real_mount(mnt));
+
+	path.mnt = mntget(mnt);
+	path.dentry = dget(mnt->mnt_root);
+
+	file = dentry_open(&path, O_PATH, current_cred());
+	if (IS_ERR(file)) {
+		dissolve_on_fput(mnt);
+		return file;
+	}
+
+	/*
+	 * An apparent O_PATH fd, with a note that the mount needs to be
+	 * unmounted on the final fput() rather than simply put.
+	 */
+	file->f_mode |= FMODE_NEED_UNMOUNT;
+
+	return file;
+}
+
 /*
  * Create a kernel mount representation for a new, prepared superblock
  * (specified by fs_fd) and attach to an open_tree-like file descriptor.
diff --git a/include/linux/mount.h b/include/linux/mount.h
index acfe7ef86a1b..44c1141bc574 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -89,6 +89,7 @@ extern struct vfsmount *vfs_create_mount(struct fs_context *fc);
 extern struct vfsmount *vfs_kern_mount(struct file_system_type *type,
 				      int flags, const char *name,
 				      void *data);
+struct file *vfs_open_detached_mount(struct vfsmount *mnt);
 
 extern void mnt_set_expiry(struct vfsmount *mnt, struct list_head *expiry_list);
 extern void mark_mounts_for_expiry(struct list_head *mounts);
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
                   ` (2 preceding siblings ...)
  2026-09-23 22:44 ` [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount() Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 23:06   ` sashiko-bot
  2026-09-23 22:44 ` [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file Pratyush Yadav
  2026-09-23 22:44 ` [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test Pratyush Yadav
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

A memfd can be preserved across a live update, but it has no name, so
anything that needs a filesystem path cannot use one.

The main use case is to fetch VMM binaries that need to be kept on the
host memory during a live update to avoid the latency of fetching them
from network. The host does not have any attached local storage to store
the binaries.

Allow preserving a tmpfs mount. The mount is identified with a file
pointing to it. Only preserve the number of blocks and root directory
mode. All other options are reset to their default values and can be set
up again by a remount. The retrieved file points to a detached mount
that can be attached to a path with move_mount(2).

Mounts with memory policies, quotas, ID mappings, and casefolding are
not supported.

Move SHMEM_SB() into shmem_fs.h to it can be used in memfd_luo.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
 Documentation/core-api/liveupdate.rst   |   1 +
 Documentation/mm/index.rst              |   1 +
 Documentation/mm/tmpfs_preservation.rst |  24 +++
 MAINTAINERS                             |   1 +
 include/linux/kho/abi/tmpfs.h           |  48 +++++
 include/linux/shmem_fs.h                |   5 +
 mm/memfd_luo.c                          | 238 +++++++++++++++++++++++-
 mm/shmem.c                              |   5 -
 8 files changed, 315 insertions(+), 8 deletions(-)
 create mode 100644 Documentation/mm/tmpfs_preservation.rst
 create mode 100644 include/linux/kho/abi/tmpfs.h

diff --git a/Documentation/core-api/liveupdate.rst b/Documentation/core-api/liveupdate.rst
index 5a292d0f3706..c92a0a8f6b90 100644
--- a/Documentation/core-api/liveupdate.rst
+++ b/Documentation/core-api/liveupdate.rst
@@ -34,6 +34,7 @@ The following types of file descriptors can be preserved
    :maxdepth: 1
 
    ../mm/memfd_preservation
+   ../mm/tmpfs_preservation
 
 Public API
 ==========
diff --git a/Documentation/mm/index.rst b/Documentation/mm/index.rst
index 13a79f5d092c..46c83fa8db8a 100644
--- a/Documentation/mm/index.rst
+++ b/Documentation/mm/index.rst
@@ -72,6 +72,7 @@ documentation, or deleted if it has served its purpose.
    page_table_check
    remap_file_pages
    split_page_table_lock
+   tmpfs_preservation
    transhuge
    unevictable-lru
    vmalloced-kernel-stacks
diff --git a/Documentation/mm/tmpfs_preservation.rst b/Documentation/mm/tmpfs_preservation.rst
new file mode 100644
index 000000000000..0c7f5bdaf7ee
--- /dev/null
+++ b/Documentation/mm/tmpfs_preservation.rst
@@ -0,0 +1,24 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+==========================
+tmpfs Preservation via LUO
+==========================
+
+.. kernel-doc:: mm/memfd_luo.c
+   :doc: tmpfs Preservation via LUO
+
+tmpfs Preservation ABI
+======================
+
+.. kernel-doc:: include/linux/kho/abi/tmpfs.h
+   :doc: tmpfs Live Update ABI
+
+.. kernel-doc:: include/linux/kho/abi/tmpfs.h
+   :internal:
+
+See Also
+========
+
+- :doc:`/mm/memfd_preservation`
+- :doc:`/core-api/liveupdate`
+- :doc:`/core-api/kho/index`
diff --git a/MAINTAINERS b/MAINTAINERS
index 3a19da74d00c..0527e92fd7ad 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15185,6 +15185,7 @@ 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/tmpfs_preservation.rst
 F:	Documentation/userspace-api/liveupdate.rst
 F:	include/linux/kho/abi/
 F:	include/linux/liveupdate.h
diff --git a/include/linux/kho/abi/tmpfs.h b/include/linux/kho/abi/tmpfs.h
new file mode 100644
index 000000000000..bed41048a19b
--- /dev/null
+++ b/include/linux/kho/abi/tmpfs.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Pratyush Yadav <pratyush@kernel.org>
+ */
+
+#ifndef _LINUX_KHO_ABI_TMPFS_H
+#define _LINUX_KHO_ABI_TMPFS_H
+
+#include <linux/limits.h>
+#include <linux/types.h>
+#include <linux/kho/abi/kexec_handover.h>
+#include <linux/kho/abi/memfd.h>
+
+/**
+ * DOC: tmpfs Live Update ABI
+ *
+ * tmpfs uses the ABI defined below for preserving a mount and the regular
+ * files in it across a kexec reboot using the LUO.
+ *
+ * The mount metadata is preserved via `tmpfs_luo_mnt_ser`.
+ *
+ * This interface is a contract. Any modification to the structure layout
+ * constitutes a breaking change. Such changes require incrementing the version
+ * number in the corresponding compatible string.
+ */
+
+/**
+ * struct tmpfs_luo_mnt_ser - Serialized state of a preserved tmpfs mount.
+ * @max_blocks: The block limit of the filesystem in PAGE_SIZE units. 0 means
+ *              unlimited.
+ * @mode:       The mode of the root directory.
+ * @flags:      Flags for the mount. Unused flag bits must be set to 0.
+ *
+ * Ownership is not preserved; the restored root directory belongs to whoever
+ * retrieves the mount.
+ */
+struct tmpfs_luo_mnt_ser {
+	u64 max_blocks;
+	u32 mode;
+	u32 flags;
+} __packed;
+
+/* The compatibility string for the tmpfs mount file handler */
+#define TMPFS_LUO_MNT_FH_COMPATIBLE	"tmpfs-mnt-v1"
+
+#endif /* _LINUX_KHO_ABI_TMPFS_H */
diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index 5663dff53186..d41d38541c73 100644
--- a/include/linux/shmem_fs.h
+++ b/include/linux/shmem_fs.h
@@ -95,6 +95,11 @@ static inline struct shmem_inode_info *SHMEM_I(struct inode *inode)
 	return container_of(inode, struct shmem_inode_info, vfs_inode);
 }
 
+static inline struct shmem_sb_info *SHMEM_SB(struct super_block *sb)
+{
+	return sb->s_fs_info;
+}
+
 /*
  * Functions in mm/shmem.c called directly from elsewhere:
  */
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 59de210bee5f..f29571666364 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -71,12 +71,19 @@
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #include <linux/bits.h>
+#include <linux/dcache.h>
 #include <linux/err.h>
 #include <linux/file.h>
+#include <linux/fs.h>
 #include <linux/io.h>
 #include <linux/kexec_handover.h>
 #include <linux/kho/abi/memfd.h>
+#include <linux/kho/abi/tmpfs.h>
+#include <linux/limits.h>
 #include <linux/liveupdate.h>
+#include <linux/magic.h>
+#include <linux/mount.h>
+#include <linux/quotaops.h>
 #include <linux/shmem_fs.h>
 #include <linux/vmalloc.h>
 #include <linux/memfd.h>
@@ -606,17 +613,242 @@ static struct liveupdate_file_handler memfd_luo_handler = {
 	.compatible = MEMFD_LUO_FH_COMPATIBLE,
 };
 
+/**
+ * DOC: tmpfs Preservation via LUO
+ *
+ * Overview
+ * ========
+ *
+ * tmpfs mounts can be preserved via LUO. This allows userspace to preserve an
+ * in-memory filesystem across kexec.
+ *
+ * Preservation is not transparent. Only the properties listed below survive;
+ * everything else comes back at its default.
+ *
+ * Preserving
+ * ==========
+ *
+ * A mount is preserved through a file descriptor for its root. An ``O_PATH``
+ * descriptor is rejected, so one obtained from fsmount(2) has to be reopened
+ * first with ``openat(fsmount_fd, ".", O_RDONLY | O_DIRECTORY)``.
+ *
+ * Mounts with memory policies, id mappings, quotas, and casefolding are not
+ * supported.
+ *
+ * Attaching a memory policy after preserve will fail the freeze.
+ *
+ * Restoring
+ * =========
+ *
+ * The mount comes back as a detached mount file descriptor with the same
+ * semantics as fsmount(2). Userspace attaches it with::
+ *
+ *	move_mount(fd, "", AT_FDCWD, "/some/path", MOVE_MOUNT_F_EMPTY_PATH);
+ *
+ * Closing the mount descriptor without ever attaching it tears the mount down,
+ * and everything in it goes too.
+ *
+ * Like fsmount(2)'s, the restored mount descriptor is an ``O_PATH`` one, so
+ * preserving the same mount again for a second live update means reopening its
+ * root.
+ *
+ * Preserved Properties
+ * ====================
+ *
+ * ``nr_blocks=``:
+ *   The block limit. Restoring file contents is charged against it.
+ *
+ * Root directory mode
+ *   Including the sticky and setgid bits.
+ *
+ * Not Preserved
+ * =============
+ *
+ * All properties which are not preserved must be assumed to be reset to
+ * default. This section describes some of those properties which may be more of
+ * note.
+ *
+ * Ownership
+ *   Everything comes back owned by whoever retrieves it. If different ownership
+ *   is needed, userspace must do that after retrieving the mount.
+ *
+ * Timestamps, inode numbers, extended attributes and ACLs
+ *   A restored mount root is a new inode with fresh timestamps and a new
+ *   number. utimensat(2) can set the timestamps again once the mount is
+ *   attached.
+ */
+
+static unsigned long tmpfs_luo_mnt_id(struct super_block *sb)
+{
+	return (unsigned long)sb;
+}
+
+static bool tmpfs_luo_mnt_can_preserve(struct liveupdate_file_handler *fh,
+				       struct file *file)
+{
+	struct vfsmount *mnt = file->f_path.mnt;
+	struct super_block *sb = mnt->mnt_sb;
+
+	if (sb->s_magic != TMPFS_MAGIC)
+		return false;
+
+	/* The root of a whole filesystem, not a bind mount of a subdirectory. */
+	if (file->f_path.dentry != mnt->mnt_root || mnt->mnt_root != sb->s_root)
+		return false;
+
+	/* These features are not supported. */
+	if (SHMEM_SB(sb)->mpol || sb_has_quota_active(sb, USRQUOTA) ||
+	    sb_has_encoding(sb) || is_idmapped_mnt(mnt))
+		return false;
+
+	return true;
+}
+
+static unsigned long tmpfs_luo_mnt_get_id(struct file *file)
+{
+	return tmpfs_luo_mnt_id(file->f_path.mnt->mnt_sb);
+}
+
+static int tmpfs_luo_mnt_preserve(struct liveupdate_file_op_args *args)
+{
+	struct tmpfs_luo_mnt_ser *ser;
+
+	ser = kho_alloc_preserve(sizeof(*ser));
+	if (IS_ERR(ser))
+		return PTR_ERR(ser);
+
+	/*
+	 * ser only saves mode and max_blocks. Since they can change by a
+	 * remount, save them on freeze(). So nothing to save for now.
+	 */
+	args->serialized_data = virt_to_phys(ser);
+
+	return 0;
+}
+
+static int tmpfs_luo_mnt_freeze(struct liveupdate_file_op_args *args)
+{
+	struct super_block *sb = args->file->f_path.mnt->mnt_sb;
+	struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
+	struct tmpfs_luo_mnt_ser *ser;
+
+	/*
+	 * A remount can install a memory policy after can_preserve() accepted
+	 * the mount.
+	 */
+	if (sbinfo->mpol)
+		return -EOPNOTSUPP;
+
+	ser = phys_to_virt(args->serialized_data);
+	ser->max_blocks = sbinfo->max_blocks;
+	ser->mode = sbinfo->mode & 07777;
+
+	return 0;
+}
+
+static void tmpfs_luo_mnt_unpreserve(struct liveupdate_file_op_args *args)
+{
+	kho_unpreserve_free(phys_to_virt(args->serialized_data));
+}
+
+static int tmpfs_luo_mnt_retrieve(struct liveupdate_file_op_args *args)
+{
+	struct tmpfs_luo_mnt_ser *ser;
+	struct vfsmount *mnt;
+	struct file *file;
+	int err;
+
+	if (!args->serialized_data)
+		return -EINVAL;
+
+	ser = phys_to_virt(args->serialized_data);
+
+	if ((ser->mode & ~07777) || ser->flags) {
+		err = -EINVAL;
+		goto free_ser;
+	}
+
+	mnt = tmpfs_create_mount(ser->max_blocks, ser->mode);
+	if (IS_ERR(mnt)) {
+		pr_err("failed to create tmpfs mount: %pe\n", mnt);
+		err = PTR_ERR(mnt);
+		goto free_ser;
+	}
+
+	file = vfs_open_detached_mount(mnt);
+	if (IS_ERR(file)) {
+		pr_err("failed to open detached tmpfs mount: %pe\n", file);
+		err = PTR_ERR(file);
+		goto free_ser;
+	}
+
+	args->file = file;
+	kho_restore_free(ser);
+
+	return 0;
+
+free_ser:
+	kho_restore_free(ser);
+	return err;
+}
+
+static void tmpfs_luo_mnt_finish(struct liveupdate_file_op_args *args)
+{
+	/*
+	 * A successful retrieve() already freed the serialized state, and a
+	 * failed one cleaned up everything it could. Only an un-retrieved
+	 * mount is left to clean up here.
+	 */
+	if (args->retrieve_status || !args->serialized_data)
+		return;
+
+	kho_restore_free(phys_to_virt(args->serialized_data));
+}
+
+static const struct liveupdate_file_ops tmpfs_luo_mnt_ops = {
+	.freeze = tmpfs_luo_mnt_freeze,
+	.finish = tmpfs_luo_mnt_finish,
+	.retrieve = tmpfs_luo_mnt_retrieve,
+	.preserve = tmpfs_luo_mnt_preserve,
+	.unpreserve = tmpfs_luo_mnt_unpreserve,
+	.can_preserve = tmpfs_luo_mnt_can_preserve,
+	.get_id = tmpfs_luo_mnt_get_id,
+	.owner = THIS_MODULE,
+};
+
+static struct liveupdate_file_handler tmpfs_luo_mnt_handler = {
+	.ops = &tmpfs_luo_mnt_ops,
+	.compatible = TMPFS_LUO_MNT_FH_COMPATIBLE,
+};
+
 static int __init memfd_luo_init(void)
 {
-	int err = liveupdate_register_file_handler(&memfd_luo_handler);
+	int err;
 
-	if (err && err != -EOPNOTSUPP) {
-		pr_err("Could not register luo filesystem handler: %pe\n",
+	err = liveupdate_register_file_handler(&memfd_luo_handler);
+	if (err) {
+		if (err == -EOPNOTSUPP)
+			return 0;
+
+		pr_err("Could not register luo memfd handler: %pe\n",
 		       ERR_PTR(err));
 
 		return err;
 	}
 
+	err = liveupdate_register_file_handler(&tmpfs_luo_mnt_handler);
+	if (err) {
+		pr_err("Could not register luo tmpfs mount handler: %pe\n",
+		       ERR_PTR(err));
+
+		goto err_unregister_memfd;
+	}
+
 	return 0;
+
+err_unregister_memfd:
+	liveupdate_unregister_file_handler(&memfd_luo_handler);
+
+	return err;
 }
 late_initcall(memfd_luo_init);
diff --git a/mm/shmem.c b/mm/shmem.c
index 8660405b6ab8..c76dd3646f6f 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -162,11 +162,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 			struct folio **foliop, enum sgp_type sgp, gfp_t gfp,
 			struct vm_fault *vmf, vm_fault_t *fault_type);
 
-static inline struct shmem_sb_info *SHMEM_SB(struct super_block *sb)
-{
-	return sb->s_fs_info;
-}
-
 /*
  * shmem_file_setup pre-accounts the whole fixed size of a VM object,
  * for shared memory and for shared anonymous (/dev/zero) mappings
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
                   ` (3 preceding siblings ...)
  2026-09-23 22:44 ` [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 23:24   ` sashiko-bot
  2026-09-23 22:44 ` [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test Pratyush Yadav
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

A memfd can be preserved across a live update, but it has no name, so
anything that needs a filesystem path cannot use one.

The main use case is to fetch VMM binaries that need to be kept on the
host memory during a live update to avoid the latency of fetching them
from network. The host does not have any attached local storage to store
the binaries.

Allow preserving files in a tmpfs mount. Only regular files in the mount
root are supported to keep things simple. Files in subdirectories will
be rejected. The mount must be preserved before the files because the
file takes a dependency on the mount. Restoration can be done in any
order.

Files are allowed to be renamed after preservation, but they can not be
unlinked. Unlinked files will cause the freeze() callback to fail and
result in a failed live update.

Re-use the memfd preservation logic and ABI since it already knows how
to preserve a shmem file. The same restrictions around file resizing
apply.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>

---

The usage of liveupdate_get_file_incoming() makes lockdep scream. The
locking itself is fine, but two locks of the same class are taken.
Likely need some annotation in liveupdate_get_file_incoming(). That is
separate work and not done by this series.
---
 include/linux/kho/abi/tmpfs.h |  35 ++++
 mm/memfd_luo.c                | 342 ++++++++++++++++++++++++++++++++--
 2 files changed, 366 insertions(+), 11 deletions(-)

diff --git a/include/linux/kho/abi/tmpfs.h b/include/linux/kho/abi/tmpfs.h
index bed41048a19b..cd47e2eb5041 100644
--- a/include/linux/kho/abi/tmpfs.h
+++ b/include/linux/kho/abi/tmpfs.h
@@ -19,6 +19,11 @@
  * tmpfs uses the ABI defined below for preserving a mount and the regular
  * files in it across a kexec reboot using the LUO.
  *
+ * Regular files in the root of such a mount are preserved individually into
+ * `struct tmpfs_luo_file_ser` and reference their mount by its LUO token. Only
+ * the folios holding the file contents are true handover payload; they use the
+ * memfd ABI (`struct memfd_luo_folio_ser`) unchanged.
+ *
  * The mount metadata is preserved via `tmpfs_luo_mnt_ser`.
  *
  * This interface is a contract. Any modification to the structure layout
@@ -42,7 +47,37 @@ struct tmpfs_luo_mnt_ser {
 	u32 flags;
 } __packed;
 
+/**
+ * struct tmpfs_luo_file_ser - Serialized state of a preserved tmpfs file.
+ * @mnt_token: The LUO token of the tmpfs mount this file lives in.
+ * @pos:       The file's current position (f_pos).
+ * @size:      The total size of the file in bytes (i_size).
+ * @mode:      The permission bits of the file (i_mode). The file type is
+ *             always S_IFREG.
+ * @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.
+ * @name:      The NUL-terminated name of the file in the root of the mount,
+ *             This is a single path component: it never contains '/', is never
+ *             "." or "..", and all bytes after the terminator must be 0.
+ */
+struct tmpfs_luo_file_ser {
+	u64 mnt_token;
+	u64 pos;
+	u64 size;
+	u32 mode;
+	u32 flags;
+	u64 nr_folios;
+	struct kho_vmalloc folios;
+	/* NAME_MAX is uAPI. */
+	char name[NAME_MAX + 1];
+} __packed;
+
 /* The compatibility string for the tmpfs mount file handler */
 #define TMPFS_LUO_MNT_FH_COMPATIBLE	"tmpfs-mnt-v1"
 
+/* The compatibility string for the tmpfs file handler */
+#define TMPFS_LUO_FILE_FH_COMPATIBLE	"tmpfs-file-v1"
+
 #endif /* _LINUX_KHO_ABI_TMPFS_H */
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index f29571666364..aacae90218cc 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -619,8 +619,8 @@ static struct liveupdate_file_handler memfd_luo_handler = {
  * Overview
  * ========
  *
- * tmpfs mounts can be preserved via LUO. This allows userspace to preserve an
- * in-memory filesystem across kexec.
+ * tmpfs mounts and the files contained can be preserved via LUO. This allows
+ * userspace to preserve in-memory files with a filesystem path across kexec.
  *
  * Preservation is not transparent. Only the properties listed below survive;
  * everything else comes back at its default.
@@ -632,10 +632,16 @@ static struct liveupdate_file_handler memfd_luo_handler = {
  * descriptor is rejected, so one obtained from fsmount(2) has to be reopened
  * first with ``openat(fsmount_fd, ".", O_RDONLY | O_DIRECTORY)``.
  *
- * Mounts with memory policies, id mappings, quotas, and casefolding are not
- * supported.
+ * Each file is then preserved through its own FD. A file depends on its mount,
+ * so the mount must already be preserved in the same session; otherwise
+ * preserving the file fails with ``-ENOENT``.
  *
- * Attaching a memory policy after preserve will fail the freeze.
+ * Only regular files linked at the root are supported. Files in subdirectories
+ * are not supported. Mounts with memory policies, id mappings, quotas, and
+ * casefolding are also not supported.
+ *
+ * Unlinking a file or attaching a memory policy after preserve will fail the
+ * freeze.
  *
  * Restoring
  * =========
@@ -645,8 +651,12 @@ static struct liveupdate_file_handler memfd_luo_handler = {
  *
  *	move_mount(fd, "", AT_FDCWD, "/some/path", MOVE_MOUNT_F_EMPTY_PATH);
  *
- * Closing the mount descriptor without ever attaching it tears the mount down,
- * and everything in it goes too.
+ * Until then the restored files are reachable only through the descriptors LUO
+ * hands back. Closing the mount descriptor without ever attaching it tears the
+ * mount down, and everything in it goes too.
+ *
+ * Unlike preservation, files may be retrieved before their mount. If the mount
+ * cannot be retrieved, every file in it fails with the same error.
  *
  * Like fsmount(2)'s, the restored mount descriptor is an ``O_PATH`` one, so
  * preserving the same mount again for a second live update means reopening its
@@ -655,12 +665,30 @@ static struct liveupdate_file_handler memfd_luo_handler = {
  * Preserved Properties
  * ====================
  *
+ * Of the mount:
+ *
  * ``nr_blocks=``:
  *   The block limit. Restoring file contents is charged against it.
  *
  * Root directory mode
  *   Including the sticky and setgid bits.
  *
+ * Of each file:
+ *
+ * Contents and size
+ *   Holes are filled by allocating pages for them during preservation, as with
+ *   memfd.
+ *
+ * Name
+ *   A single component in the root of the mount.
+ *
+ * Permission bits
+ *   Not including the setuid and setgid bits; see below.
+ *
+ * File position
+ *   So that the returned descriptor can be read from or written to where the
+ *   old one left off.
+ *
  * Not Preserved
  * =============
  *
@@ -670,12 +698,11 @@ static struct liveupdate_file_handler memfd_luo_handler = {
  *
  * Ownership
  *   Everything comes back owned by whoever retrieves it. If different ownership
- *   is needed, userspace must do that after retrieving the mount.
+ *   is needed, userspace must do that after retrieving the files.
  *
  * Timestamps, inode numbers, extended attributes and ACLs
- *   A restored mount root is a new inode with fresh timestamps and a new
- *   number. utimensat(2) can set the timestamps again once the mount is
- *   attached.
+ *   A restored file is a new inode with fresh timestamps and a new number.
+ *   utimensat(2) can set the timestamps again once the mount is attached.
  */
 
 static unsigned long tmpfs_luo_mnt_id(struct super_block *sb)
@@ -821,6 +848,289 @@ static struct liveupdate_file_handler tmpfs_luo_mnt_handler = {
 	.compatible = TMPFS_LUO_MNT_FH_COMPATIBLE,
 };
 
+static bool tmpfs_luo_file_can_preserve(struct liveupdate_file_handler *fh,
+					struct file *file)
+{
+	struct inode *inode = file_inode(file);
+	struct dentry *dentry = file->f_path.dentry;
+	struct super_block *sb = inode->i_sb;
+
+	if (!shmem_file(file) || (sb->s_flags & SB_NOUSER))
+		return false;
+
+	/* Only linked regular files allowed. */
+	if (!S_ISREG(inode->i_mode) || d_unlinked(dentry))
+		return false;
+
+	/* Only files in the root of the mount are supported for now. */
+	if (dentry->d_parent != sb->s_root)
+		return false;
+
+	return true;
+}
+
+static unsigned long tmpfs_luo_file_get_id(struct file *file)
+{
+	return (unsigned long)file_inode(file);
+}
+
+static int tmpfs_luo_file_preserve(struct liveupdate_file_op_args *args)
+{
+	struct inode *inode = file_inode(args->file);
+	struct memfd_luo_folio_ser *folios_ser;
+	u64 nr_folios, inode_size, mnt_token;
+	struct tmpfs_luo_file_ser *ser;
+	int err;
+
+	/* Find the token of the mount. It is identified by its superblock. */
+	err = liveupdate_get_token_outgoing(args->session, tmpfs_luo_mnt_id(inode->i_sb),
+					    &mnt_token);
+	if (err)
+		return err;
+
+	ser = kho_alloc_preserve(sizeof(*ser));
+	if (IS_ERR(ser))
+		return PTR_ERR(ser);
+
+	inode_lock(inode);
+	shmem_freeze(inode, true);
+
+	inode_size = i_size_read(inode);
+
+	/*
+	 * memfd_pin_folios() caps at UINT_MAX folios; refuse larger files to
+	 * avoid silently preserving only a prefix.
+	 */
+	if (DIV_ROUND_UP_ULL(inode_size, PAGE_SIZE) > UINT_MAX) {
+		err = -EFBIG;
+		goto err_free_ser;
+	}
+
+	ser->mnt_token = mnt_token;
+
+	/*
+	 * Preserve only folios now. Name, mode, etc. can change later and are
+	 * cheap to preserve. They will be preserved on freeze().
+	 */
+	err = memfd_luo_preserve_folios(args->file, &ser->folios, &folios_ser,
+					&nr_folios);
+	if (err)
+		goto err_free_ser;
+
+	ser->nr_folios = nr_folios;
+	inode_unlock(inode);
+
+	args->private_data = folios_ser;
+	args->serialized_data = virt_to_phys(ser);
+
+	return 0;
+
+err_free_ser:
+	kho_unpreserve_free(ser);
+	shmem_freeze(inode, false);
+	inode_unlock(inode);
+	return err;
+}
+
+static int tmpfs_luo_file_freeze(struct liveupdate_file_op_args *args)
+{
+	struct dentry *dentry = args->file->f_path.dentry;
+	struct inode *inode = file_inode(args->file);
+	struct dentry *root = inode->i_sb->s_root;
+	struct tmpfs_luo_file_ser *ser;
+	int err;
+
+	/*
+	 * Lock the mount root to block renames and unlinks, then the inode
+	 * itself to stabilize its mode and size.
+	 */
+	inode_lock_shared_nested(d_inode(root), I_MUTEX_PARENT);
+	inode_lock_shared(inode);
+
+	/* Renamed into a subdirectory. */
+	if (dentry->d_parent != root) {
+		err = -EOPNOTSUPP;
+		goto unlock;
+	}
+
+	/* Unlinked after preserve. */
+	if (d_unlinked(dentry)) {
+		err = -ENOENT;
+		goto unlock;
+	}
+
+	ser = phys_to_virt(args->serialized_data);
+
+	/*
+	 * Zero-pads the tail, which the ABI requires. Cannot fail, the dentry
+	 * name is bounded by NAME_MAX, but check anyway.
+	 */
+	if (strscpy_pad(ser->name, dentry->d_name.name) < 0) {
+		err = -ENAMETOOLONG;
+		goto unlock;
+	}
+
+	ser->mode = inode->i_mode & 0777;
+	ser->size = i_size_read(inode);
+	ser->pos = args->file->f_pos;
+
+	err = 0;
+
+unlock:
+	inode_unlock_shared(inode);
+	inode_unlock_shared(d_inode(root));
+	return err;
+}
+
+static void tmpfs_luo_file_unpreserve(struct liveupdate_file_op_args *args)
+{
+	struct inode *inode = file_inode(args->file);
+	struct tmpfs_luo_file_ser *ser;
+
+	inode_lock(inode);
+	shmem_freeze(inode, false);
+
+	ser = phys_to_virt(args->serialized_data);
+	memfd_luo_unpreserve_folios(&ser->folios, args->private_data,
+				    ser->nr_folios);
+
+	kho_unpreserve_free(ser);
+	inode_unlock(inode);
+}
+
+/*
+ * A name from the previous kernel is untrusted input. There is no separate
+ * length, so the NUL terminator is the only bound.
+ */
+static bool tmpfs_luo_name_valid(const char *name, size_t size)
+{
+	size_t len = strnlen(name, size);
+
+	/* Non-empty, NUL terminated, and a single component. */
+	if (!len || len == size || memchr(name, '/', len))
+		return false;
+
+	return strcmp(name, ".") && strcmp(name, "..");
+}
+
+static int tmpfs_luo_file_retrieve(struct liveupdate_file_op_args *args)
+{
+	struct memfd_luo_folio_ser *folios_ser;
+	struct file *mnt_file, *file;
+	struct tmpfs_luo_file_ser *ser;
+	struct inode *inode;
+	int err;
+
+	if (!args->serialized_data)
+		return -EINVAL;
+
+	ser = phys_to_virt(args->serialized_data);
+
+	if ((ser->mode & ~0777) || ser->flags ||
+	    !tmpfs_luo_name_valid(ser->name, sizeof(ser->name))) {
+		err = -EINVAL;
+		goto free_ser;
+	}
+
+	err = liveupdate_get_file_incoming(args->session, ser->mnt_token,
+					   &mnt_file);
+	if (err) {
+		pr_err("failed to retrieve tmpfs mount: %pe\n", ERR_PTR(err));
+		goto free_ser;
+	}
+
+	/*
+	 * TODO: This is racy. This will link the file in the mount so after
+	 * this call userspace can already open the file and write to it.
+	 * Ideally we should first create the inode and set it up, and only
+	 * then link it to the root.
+	 */
+	file = file_open_root(&mnt_file->f_path, ser->name,
+			      O_RDWR | O_CREAT | O_EXCL | O_LARGEFILE,
+			      ser->mode);
+	fput(mnt_file);
+	if (IS_ERR(file)) {
+		pr_err("failed to create '%s': %pe\n", ser->name, file);
+		err = PTR_ERR(file);
+		goto free_ser;
+	}
+
+	inode = file_inode(file);
+
+	inode_lock(inode);
+	i_size_write(inode, ser->size);
+	inode_unlock(inode);
+
+	vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
+
+	if (ser->nr_folios) {
+		folios_ser = kho_restore_vmalloc(&ser->folios);
+		if (!folios_ser) {
+			err = -EINVAL;
+			goto put_file;
+		}
+
+		err = memfd_luo_retrieve_folios(file, folios_ser,
+						ser->nr_folios);
+		vfree(folios_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;
+}
+
+static void tmpfs_luo_file_finish(struct liveupdate_file_op_args *args)
+{
+	struct memfd_luo_folio_ser *folios_ser;
+	struct tmpfs_luo_file_ser *ser;
+
+	/*
+	 * A successful retrieve() already consumed the preserved memory, and a
+	 * failed one cleaned up what it could. Only a file that was never
+	 * retrieved is left to discard here.
+	 */
+	if (args->retrieve_status || !args->serialized_data)
+		return;
+
+	ser = phys_to_virt(args->serialized_data);
+
+	if (ser->nr_folios) {
+		folios_ser = kho_restore_vmalloc(&ser->folios);
+		if (folios_ser) {
+			memfd_luo_discard_folios(folios_ser, ser->nr_folios);
+			vfree(folios_ser);
+		}
+	}
+
+	kho_restore_free(ser);
+}
+
+static const struct liveupdate_file_ops tmpfs_luo_file_ops = {
+	.freeze = tmpfs_luo_file_freeze,
+	.finish = tmpfs_luo_file_finish,
+	.retrieve = tmpfs_luo_file_retrieve,
+	.preserve = tmpfs_luo_file_preserve,
+	.unpreserve = tmpfs_luo_file_unpreserve,
+	.can_preserve = tmpfs_luo_file_can_preserve,
+	.get_id = tmpfs_luo_file_get_id,
+	.owner = THIS_MODULE,
+};
+
+static struct liveupdate_file_handler tmpfs_luo_file_handler = {
+	.ops = &tmpfs_luo_file_ops,
+	.compatible = TMPFS_LUO_FILE_FH_COMPATIBLE,
+};
+
 static int __init memfd_luo_init(void)
 {
 	int err;
@@ -844,8 +1154,18 @@ static int __init memfd_luo_init(void)
 		goto err_unregister_memfd;
 	}
 
+	err = liveupdate_register_file_handler(&tmpfs_luo_file_handler);
+	if (err) {
+		pr_err("Could not register luo tmpfs file handler: %pe\n",
+		       ERR_PTR(err));
+
+		goto err_unregister_tmpfs_mnt;
+	}
+
 	return 0;
 
+err_unregister_tmpfs_mnt:
+	liveupdate_unregister_file_handler(&tmpfs_luo_mnt_handler);
 err_unregister_memfd:
 	liveupdate_unregister_file_handler(&memfd_luo_handler);
 
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test
  2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
                   ` (4 preceding siblings ...)
  2026-09-23 22:44 ` [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file Pratyush Yadav
@ 2026-09-23 22:44 ` Pratyush Yadav
  2026-09-23 23:14   ` sashiko-bot
  5 siblings, 1 reply; 13+ messages in thread
From: Pratyush Yadav @ 2026-09-23 22:44 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
	David Hildenbrand, Lorenzo Stoakes, Alexander Graf, Hugh Dickins,
	Baolin Wang, David Matlack, Samiullah Khawaja
  Cc: kexec, linux-kernel, linux-mm

From: "Pratyush Yadav (Google)" <pratyush@kernel.org>

Add a two-stage kexec test for tmpfs preservation, in the same shape as
luo_kexec_simple.

The first stage create a tmpfs mount, creates a file in it, write some
data to it, and preserves both. In the second stage, it retrieves both
and verifies the contents of the file. It also verifies it can be
reached by a filesystem path.

Add the test to run_vmtests.sh to allow automated runs.

Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>
---
 tools/testing/selftests/liveupdate/Makefile   |   1 +
 .../selftests/liveupdate/luo_kexec_tmpfs.c    | 191 ++++++++++++++++++
 .../selftests/liveupdate/run-vmtests.sh       |   1 +
 3 files changed, 193 insertions(+)
 create mode 100644 tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c

diff --git a/tools/testing/selftests/liveupdate/Makefile b/tools/testing/selftests/liveupdate/Makefile
index 634211c66652..c35aee04b4a3 100644
--- a/tools/testing/selftests/liveupdate/Makefile
+++ b/tools/testing/selftests/liveupdate/Makefile
@@ -3,6 +3,7 @@
 TEST_GEN_PROGS += liveupdate
 
 TEST_GEN_PROGS_EXTENDED += luo_kexec_simple
+TEST_GEN_PROGS_EXTENDED += luo_kexec_tmpfs
 TEST_GEN_PROGS_EXTENDED += luo_multi_session
 TEST_GEN_PROGS_EXTENDED += luo_stress_sessions
 TEST_GEN_PROGS_EXTENDED += luo_stress_files
diff --git a/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c b/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c
new file mode 100644
index 000000000000..608a6eddbd7b
--- /dev/null
+++ b/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Copyright (C) 2026, Google LLC.
+ * Pratyush Yadav <pratyush@kernel.org>
+ *
+ * Validate preservation of a tmpfs mount and a file in it across a kexec
+ * reboot. Stage 1 creates a tmpfs, puts a file in it and preserves both.
+ * Stage 2 retrieves the mount, attaches it with move_mount(2), and checks that
+ * the file's contents are intact and that it is reachable by path.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include <libliveupdate.h>
+
+#define TEST_SESSION_NAME	"tmpfs-session"
+#define TMPFS_MNT_TOKEN		0x2A
+#define TMPFS_FILE_TOKEN	0x2B
+
+/* Constants for the state-tracking mechanism, specific to this test file. */
+#define STATE_SESSION_NAME	"kexec_tmpfs_state"
+#define STATE_MEMFD_TOKEN	998
+
+#define TMPFS_DIR		"/tmpfs"
+#define TMPFS_FILE_NAME		"state.bin"
+#define TMPFS_FILE_PATH		TMPFS_DIR "/" TMPFS_FILE_NAME
+#define TMPFS_DATA		"hello tmpfs kexec world"
+
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH	0x00000004
+#endif
+
+/* nolibc has no move_mount() wrapper. */
+static int move_mount_empty_from(int from_fd, const char *to_path)
+{
+	return syscall(__NR_move_mount, from_fd, "", AT_FDCWD, to_path,
+		       MOVE_MOUNT_F_EMPTY_PATH);
+}
+
+static void write_file(int fd, const char *data, size_t len)
+{
+	ssize_t written = write(fd, data, len);
+
+	if (written < 0 || (size_t)written != len)
+		fail_exit("write of %zu bytes returned %zd", len, written);
+}
+
+static void verify_contents(int fd, const char *expected, size_t len)
+{
+	char buf[128];
+	ssize_t got;
+
+	if (len > sizeof(buf))
+		fail_exit("test data too large for buffer");
+
+	/* nolibc has no pread(). */
+	if (lseek(fd, 0, SEEK_SET) < 0)
+		fail_exit("lseek to the start of the file");
+
+	got = read(fd, buf, len);
+	if (got < 0 || (size_t)got != len)
+		fail_exit("read of %zu bytes returned %zd", len, got);
+
+	if (memcmp(buf, expected, len))
+		fail_exit("file contents do not match");
+}
+
+/* Stage 1: Executed before the kexec reboot. */
+static void run_stage_1(int luo_fd)
+{
+	int session_fd, mnt_fd, file_fd;
+
+	ksft_print_msg("[STAGE 1] Starting pre-kexec setup...\n");
+
+	ksft_print_msg("[STAGE 1] Creating state file for next stage (2)...\n");
+	create_state_file(luo_fd, STATE_SESSION_NAME, STATE_MEMFD_TOKEN, 2);
+
+	session_fd = luo_create_session(luo_fd, TEST_SESSION_NAME);
+	if (session_fd < 0)
+		fail_exit("luo_create_session for '%s'", TEST_SESSION_NAME);
+
+	ksft_print_msg("[STAGE 1] Mounting tmpfs at %s...\n", TMPFS_DIR);
+	if (mkdir(TMPFS_DIR, 0755) < 0)
+		fail_exit("mkdir %s", TMPFS_DIR);
+	if (mount("tmpfs", TMPFS_DIR, "tmpfs", 0, NULL) < 0)
+		fail_exit("mount tmpfs at %s", TMPFS_DIR);
+
+	/*
+	 * A plain open of the mount root. LUO's preserve uses fget(), which
+	 * refuses O_PATH descriptors, so an fsmount(2) fd cannot be handed to
+	 * it directly either.
+	 */
+	mnt_fd = open(TMPFS_DIR, O_RDONLY | O_DIRECTORY);
+	if (mnt_fd < 0)
+		fail_exit("open %s", TMPFS_DIR);
+
+	file_fd = open(TMPFS_FILE_PATH, O_RDWR | O_CREAT, 0644);
+	if (file_fd < 0)
+		fail_exit("open %s", TMPFS_FILE_PATH);
+
+	write_file(file_fd, TMPFS_DATA, sizeof(TMPFS_DATA));
+
+	/* The mount must be preserved before any file that lives in it. */
+	ksft_print_msg("[STAGE 1] Preserving mount (token %#x)...\n",
+		       TMPFS_MNT_TOKEN);
+	if (luo_session_preserve_fd(session_fd, mnt_fd, TMPFS_MNT_TOKEN) < 0)
+		fail_exit("luo_session_preserve_fd for the mount");
+
+	ksft_print_msg("[STAGE 1] Preserving file (token %#x)...\n",
+		       TMPFS_FILE_TOKEN);
+	if (luo_session_preserve_fd(session_fd, file_fd, TMPFS_FILE_TOKEN) < 0)
+		fail_exit("luo_session_preserve_fd for the file");
+
+	close(file_fd);
+	close(mnt_fd);
+	close(luo_fd);
+	daemonize_and_wait();
+}
+
+/* Stage 2: Executed after the kexec reboot. */
+static void run_stage_2(int luo_fd, int state_session_fd)
+{
+	int session_fd, mnt_fd, file_fd, path_fd, stage;
+
+	ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
+
+	restore_and_read_stage(state_session_fd, STATE_MEMFD_TOKEN, &stage);
+	if (stage != 2)
+		fail_exit("Expected stage 2, but state file contains %d", stage);
+
+	session_fd = luo_retrieve_session(luo_fd, TEST_SESSION_NAME);
+	if (session_fd < 0)
+		fail_exit("luo_retrieve_session for '%s'", TEST_SESSION_NAME);
+
+	ksft_print_msg("[STAGE 2] Retrieving mount (token %#x)...\n",
+		       TMPFS_MNT_TOKEN);
+	mnt_fd = luo_session_retrieve_fd(session_fd, TMPFS_MNT_TOKEN);
+	if (mnt_fd < 0)
+		fail_exit("luo_session_retrieve_fd for the mount");
+
+	ksft_print_msg("[STAGE 2] Attaching the restored mount at %s...\n",
+		       TMPFS_DIR);
+	if (mkdir(TMPFS_DIR, 0755) < 0)
+		fail_exit("mkdir %s", TMPFS_DIR);
+	if (move_mount_empty_from(mnt_fd, TMPFS_DIR) < 0)
+		fail_exit("move_mount of the restored mount to %s", TMPFS_DIR);
+	close(mnt_fd);
+
+	ksft_print_msg("[STAGE 2] Retrieving file (token %#x)...\n",
+		       TMPFS_FILE_TOKEN);
+	file_fd = luo_session_retrieve_fd(session_fd, TMPFS_FILE_TOKEN);
+	if (file_fd < 0)
+		fail_exit("luo_session_retrieve_fd for the file");
+
+	/* The contents are intact through the retrieved fd. */
+	verify_contents(file_fd, TMPFS_DATA, sizeof(TMPFS_DATA));
+
+	/* And the file is reachable by path, which is the point. */
+	path_fd = open(TMPFS_FILE_PATH, O_RDONLY);
+	if (path_fd < 0)
+		fail_exit("open restored %s by path", TMPFS_FILE_PATH);
+
+	verify_contents(path_fd, TMPFS_DATA, sizeof(TMPFS_DATA));
+
+	close(path_fd);
+	close(file_fd);
+
+	ksft_print_msg("[STAGE 2] Test data verified successfully.\n");
+	if (luo_session_finish(session_fd) < 0)
+		fail_exit("luo_session_finish for test session");
+	close(session_fd);
+
+	if (luo_session_finish(state_session_fd) < 0)
+		fail_exit("luo_session_finish for state session");
+	close(state_session_fd);
+
+	ksft_print_msg("\n--- TMPFS KEXEC TEST PASSED ---\n");
+}
+
+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 d656ce58c5a7..624d1916eeda 100755
--- a/tools/testing/selftests/liveupdate/run-vmtests.sh
+++ b/tools/testing/selftests/liveupdate/run-vmtests.sh
@@ -17,6 +17,7 @@ skipped=0
 
 TEST_NAMES=(
 	"luo_kexec_simple"
+	"luo_kexec_tmpfs"
 	"luo_multi_session"
 	"luo_stress_files"
 	"luo_stress_sessions"
-- 
2.56.0.rc1.310.g51773c2048-goog


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally
  2026-09-23 22:44 ` [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally Pratyush Yadav
@ 2026-09-23 22:54   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 22:54 UTC (permalink / raw)
  To: Pratyush Yadav, David Hildenbrand, Alexander Graf,
	Lorenzo Stoakes, Hugh Dickins, Pasha Tatashin, Baolin Wang,
	David Matlack, Mike Rapoport, Samiullah Khawaja, Andrew Morton
  Cc: kexec, linux-mm, linux-kernel

> From: "Pratyush Yadav (Google)" <pratyush@kernel.org>
> 
> Upcoming commits will add support for preserving a tmpfs mount via LUO.
> While the retrieve logic can create the mount by constructing an options
> string from its preserved data, it is simpler to let it directly pass
> the parameters.
> 
> tmpfs_create_mount() allows creating a mount with the specified
> max_blocks and mode. The function only takes max_blocks and mode for now
> because that is all the tmpfs preservation will use. All other values
> are left to their defaults.
> 
> Signed-off-by: Pratyush Yadav (Google) <pratyush@kernel.org>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=2


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id
  2026-09-23 22:44 ` [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id Pratyush Yadav
@ 2026-09-23 22:55   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 22:55 UTC (permalink / raw)
  To: Pasha Tatashin, Andrew Morton, Samiullah Khawaja, Pratyush Yadav,
	David Hildenbrand, Alexander Graf, Baolin Wang, Mike Rapoport,
	David Matlack, Hugh Dickins, Lorenzo Stoakes
  Cc: linux-kernel, kexec, linux-mm

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] liveupdate: NULL pointer dereference in liveupdate_get_token_outgoing() on incoming sessions
- [Medium] liveupdate: flat ID namespace in liveupdate_get_token_outgoing() risks cross-handler collisions
- [Low] liveupdate: O(N) indirect function calls in liveupdate_get_token_outgoing() list traversal
--

commit 17b042e4901cfc25a86dec1038092fbe1eb5d060
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

liveupdate: luo_file: look up outgoing tokens by id

liveupdate_get_token_outgoing() is used to look up tokens of dependencies.
It does the search based on struct file. LUO provides file handlers a
get_id callback that can return the unique identifier of a resource. This
patch uses ID to look up dependencies in liveupdate_get_token_outgoing()
instead of struct file.

> diff --git a/kernel/liveupdate/luo_file.c b/kernel/liveupdate/luo_file.c
> index a39992da504fb..51817d202ce26 100644
> --- a/kernel/liveupdate/luo_file.c
> +++ b/kernel/liveupdate/luo_file.c
[ ... ]
>  int liveupdate_get_token_outgoing(struct liveupdate_session *s,
> -				  struct file *file, u64 *tokenp)
> +				  unsigned long id, u64 *tokenp)

[Severity: Medium]
Will this flat ID namespace risk cross-handler collisions?

Because liveupdate_get_token_outgoing() searches across all preserved files
using just an unsigned long id, it loses the context of which file handler
the ID belongs to.

If different handlers use overlapping namespaces, such as IDR indices or
pointers that might alias depending on object lifetime, a lookup intended
for one handler's object could incorrectly match an object from a different
handler that happens to appear earlier in the list.

Could the API require the expected file handler context to scope the search
safely?

>  {
>  	struct luo_file_set *file_set = luo_file_set_from_session_locked(s);
>  	struct luo_file *luo_file;
>  	int err = -ENOENT;
>  
>  	list_for_each_entry(luo_file, &file_set->files_list, list) {
> -		if (luo_file->file == file) {
> +		if (luo_get_id(luo_file->fh, luo_file->file) == id) {

[Severity: High]
Can this cause a NULL pointer dereference on incoming sessions?

In an incoming session after deserialization, luo_file->file is initialized
to NULL and remains NULL until the file is explicitly retrieved.

Because liveupdate_get_token_outgoing() is an exported API that accepts a
generic struct liveupdate_session, an in-kernel caller could mistakenly pass
an incoming session.

Prior to this patch, doing so would safely return -ENOENT because NULL == file
is false. With this patch, luo_get_id() will blindly pass the NULL file
pointer to the handler's get_id callback.

Handlers like memfd_luo call file_inode(file), which would cause an
immediate NULL pointer dereference and kernel crash.

[Severity: Low]
Are these indirect function calls necessary during the list traversal?

Replacing the direct pointer comparison with luo_get_id() performs an indirect
function call (fh->ops->get_id) if defined, on every iteration of the
files_list traversal.

Because this function is used to look up tokens for dependencies, it can be
called for every dependency of every preserved object, resulting in O(N * M)
indirect calls.

Since the ID is a constant property of the preserved file, could it be
evaluated exactly once during luo_preserve_file() and cached directly in
struct luo_file?

>  			if (tokenp)
>  				*tokenp = luo_file->token;
>  			err = 0;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=1

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount()
  2026-09-23 22:44 ` [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount() Pratyush Yadav
@ 2026-09-23 23:01   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 23:01 UTC (permalink / raw)
  To: Andrew Morton, Samiullah Khawaja, Alexander Graf, Mike Rapoport,
	Lorenzo Stoakes, Baolin Wang, Pratyush Yadav, David Hildenbrand,
	Pasha Tatashin, Hugh Dickins, David Matlack
  Cc: kexec, linux-mm, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] fs: mount reference leak in vfs_open_detached_mount() error path
--

commit 7bbd41a97d183f30490f0856f394037fd1846a6f
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

fs/namespace: Add vfs_open_detached_mount()

This commit factors out the creation of an anonymous mount namespace and an
O_PATH file for a detached mount into a new helper, vfs_open_detached_mount.
This allows in-kernel mount creators to hand a detached mount file to
userspace, similar to what fsmount does.

> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1ecd96c918b33..5e7ba2b61dd9a 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
[ ... ]
> +/**
> + * vfs_open_detached_mount - Publish a new mount as a detached mount file.
> + * @mnt: The mount to publish. Must not be attached to a mount namespace. The
> + *       caller's reference is consumed on success.
[ ... ]
> + * Return: the new file, or an ERR_PTR. On failure the caller's reference to
> + *         @mnt is dropped, as the mount cannot be published.
> + */
> +struct file *vfs_open_detached_mount(struct vfsmount *mnt)
> +{
> +	struct path path __free(path_put) = {};
> +	struct mnt_namespace *ns;
> +	struct file *file;
> +
> +	if (WARN_ON_ONCE(real_mount(mnt)->mnt_ns))
> +		return ERR_PTR(-EINVAL);

[Severity: Medium]
Does this error path leak the vfsmount reference?

The function documentation explicitly guarantees that on failure, the
caller's reference to mnt is dropped. If a caller inadvertently passes a
mount already attached to a mount namespace, this WARN_ON_ONCE condition
evaluates to true and the function returns an error without dropping the
reference.

For comparison, the error path immediately below for alloc_mnt_ns
failure correctly calls mntput(mnt) before returning.

> +
> +	ns = alloc_mnt_ns(current->nsproxy->mnt_ns->user_ns, true);
> +	if (IS_ERR(ns)) {
> +		mntput(mnt);
> +		return ERR_CAST(ns);
> +	}
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=3

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount
  2026-09-23 22:44 ` [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount Pratyush Yadav
@ 2026-09-23 23:06   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 23:06 UTC (permalink / raw)
  To: Baolin Wang, Hugh Dickins, Mike Rapoport, Pratyush Yadav,
	Samiullah Khawaja, Alexander Graf, David Matlack, Pasha Tatashin,
	Andrew Morton, David Hildenbrand, Lorenzo Stoakes
  Cc: kexec, linux-mm, linux-kernel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] mm: tmpfs LUO drops group quotas due to incomplete USRQUOTA check
- [Medium] mm: TOCTOU race allows unsupported ID mappings and quotas to bypass LUO validation
--

commit 20e62ff0b162f0ffc76cd0d972cc6c760a83d003
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

mm/memfd_luo: allow preserving a tmpfs mount

This commit adds support for preserving a tmpfs mount across a live update.
It preserves the filesystem block limit and root directory mode, while
resetting other options to default, identifying the mount via a file
pointing to it.

> diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
> --- a/mm/memfd_luo.c
> +++ b/mm/memfd_luo.c
[ ... ]
> +static bool tmpfs_luo_mnt_can_preserve(struct liveupdate_file_handler *fh,
> +				       struct file *file)
> +{
[ ... ]
> +	/* These features are not supported. */
> +	if (SHMEM_SB(sb)->mpol || sb_has_quota_active(sb, USRQUOTA) ||
> +	    sb_has_encoding(sb) || is_idmapped_mnt(mnt))
> +		return false;

[Severity: Medium]
Does this condition improperly accept tmpfs mounts with group or project
quotas active? 

The check only tests for USRQUOTA, which means mounts with grpquota or 
prjquota will bypass this rejection and their quota state will be silently
lost across kexec. 

Should this use sb_any_quota_loaded(sb) to properly detect all quota types?

[ ... ]
> +static int tmpfs_luo_mnt_freeze(struct liveupdate_file_op_args *args)
> +{
> +	struct super_block *sb = args->file->f_path.mnt->mnt_sb;
> +	struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
> +	struct tmpfs_luo_mnt_ser *ser;
> +
> +	/*
> +	 * A remount can install a memory policy after can_preserve() accepted
> +	 * the mount.
> +	 */
> +	if (sbinfo->mpol)
> +		return -EOPNOTSUPP;

[Severity: Medium]
Does this leave a TOCTOU window for ID mappings and quotas? 

While mpol is re-validated here to handle concurrent remounts, checks for
is_idmapped_mnt() and quotas are missing. If a user concurrently adds an ID
mapping via mount_setattr() or enables quotas via remount after the
tmpfs_luo_mnt_can_preserve() check passes, those unsupported properties will
bypass validation and be silently dropped across kexec.

Should this re-validate the ID mapping and quota state as well?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=4

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test
  2026-09-23 22:44 ` [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test Pratyush Yadav
@ 2026-09-23 23:14   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 23:14 UTC (permalink / raw)
  To: Pasha Tatashin, Andrew Morton, Baolin Wang, David Matlack,
	Hugh Dickins, Samiullah Khawaja, David Hildenbrand,
	Mike Rapoport, Alexander Graf, Lorenzo Stoakes, Pratyush Yadav
  Cc: linux-mm, linux-kernel, kexec

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/liveupdate: luo_kexec_tmpfs fails on persistent rootfs
--

commit 656981de91db4b31da49ddcf77d65f446841090b
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

selftests/liveupdate: add tmpfs kexec test

This commit adds a two-stage kexec test for tmpfs preservation. The first
stage creates a tmpfs mount, creates a file, and preserves both. The second
stage retrieves both and verifies the contents.

> diff --git a/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c b/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c
> new file mode 100644
> index 0000000000000..608a6eddbd7ba
> --- /dev/null
> +++ b/tools/testing/selftests/liveupdate/luo_kexec_tmpfs.c
[ ... ]
> +/* Stage 1: Executed before the kexec reboot. */
> +static void run_stage_1(int luo_fd)
> +{
[ ... ]
> +	ksft_print_msg("[STAGE 1] Mounting tmpfs at %s...\n", TMPFS_DIR);
> +	if (mkdir(TMPFS_DIR, 0755) < 0)
> +		fail_exit("mkdir %s", TMPFS_DIR);

[Severity: Medium]
Will this fail on test re-runs if the directory is left behind from a prior
run?

> +	if (mount("tmpfs", TMPFS_DIR, "tmpfs", 0, NULL) < 0)
> +		fail_exit("mount tmpfs at %s", TMPFS_DIR);
[ ... ]
> +/* Stage 2: Executed after the kexec reboot. */
> +static void run_stage_2(int luo_fd, int state_session_fd)
> +{
[ ... ]
> +	ksft_print_msg("[STAGE 2] Attaching the restored mount at %s...\n",
> +		       TMPFS_DIR);
> +	if (mkdir(TMPFS_DIR, 0755) < 0)
> +		fail_exit("mkdir %s", TMPFS_DIR);

[Severity: Medium]
Will this unconditionally fail with EEXIST in run_stage_2() if the underlying
root filesystem is persistent across kexec?

> +	if (move_mount_empty_from(mnt_fd, TMPFS_DIR) < 0)
> +		fail_exit("move_mount of the restored mount to %s", TMPFS_DIR);
> +	close(mnt_fd);
[ ... ]
> +	ksft_print_msg("[STAGE 2] Test data verified successfully.\n");
> +	if (luo_session_finish(session_fd) < 0)
> +		fail_exit("luo_session_finish for test session");
> +	close(session_fd);
> +
> +	if (luo_session_finish(state_session_fd) < 0)
> +		fail_exit("luo_session_finish for state session");
> +	close(state_session_fd);
> +
> +	ksft_print_msg("\n--- TMPFS KEXEC TEST PASSED ---\n");
> +}

[Severity: Medium]
Does the test exit successfully here without calling umount() or rmdir() to
clean up the environment? If so, does it leak the /tmpfs directory and its
mount on the host system?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=6

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file
  2026-09-23 22:44 ` [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file Pratyush Yadav
@ 2026-09-23 23:24   ` sashiko-bot
  0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-09-23 23:24 UTC (permalink / raw)
  To: Pasha Tatashin, Mike Rapoport, Andrew Morton, Lorenzo Stoakes,
	Pratyush Yadav, Baolin Wang, Samiullah Khawaja, Hugh Dickins,
	David Hildenbrand, David Matlack, Alexander Graf
  Cc: kexec, linux-kernel, linux-mm

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] mm: restored file permission bits are corrupted by current_umask()
- [High] mm: TOCTOU race allows unprivileged userspace to disrupt tmpfs file retrieve
- [High] mm: memory leak of KHO folios on tmpfs_luo_file_retrieve failure
- [Medium] mm: potential double free / UAF of ser and folios on successful retrieve
--

commit 4087da995ba7fe7684120abaf9ec59deb366b97b
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

mm/memfd_luo: allow preserving a tmpfs file

This commit introduces tmpfs file preservation across kexec live updates.
It preserves regular files within a tmpfs mount by serializing their
properties and memory folios, and restores them when the mount is
retrieved.

> diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
> --- a/mm/memfd_luo.c
> +++ b/mm/memfd_luo.c
> @@ -1005,62 +1005,51 @@ static int tmpfs_luo_file_retrieve(struct liveupdate_file_op_args *args)
[ ... ]
> 	err = liveupdate_get_file_incoming(args->session, ser->mnt_token,
> 					   &mnt_file);
> 	if (err) {
> 		pr_err("failed to retrieve tmpfs mount: %pe\n", ERR_PTR(err));
> 		goto free_ser;
> 	}

[Severity: High]
If this path jumps to the free_ser error label, does it permanently leak
the physical memory reserved for the KHO folios?

The free_ser error label frees the ser metadata, but does not discard
the physical folios or the ser->folios vmalloc region. Because the function
returns an error, tmpfs_luo_file_finish() may skip its cleanup logic if
args->retrieve_status is set to a non-zero error code. This would permanently
leak the memory of all files preserved inside that mount.

[ ... ]
> 	/*
> 	 * TODO: This is racy. This will link the file in the mount so after
> 	 * this call userspace can already open the file and write to it.
> 	 * Ideally we should first create the inode and set it up, and only
> 	 * then link it to the root.
> 	 */
> 	file = file_open_root(&mnt_file->f_path, ser->name,
> 			      O_RDWR | O_CREAT | O_EXCL | O_LARGEFILE,
> 			      ser->mode);

[Severity: High]
Does this code inadvertently strip the file permissions via the
orchestrator's umask?

VFS creation functions like file_open_root(O_CREAT) will apply
~current_umask() to the newly created file, which could alter the
preserved ser->mode. Since the kernel code lacks a subsequent chmod()
or vfs_setattr() step to enforce the exact preserved mode, the restored
file might have incorrect permission bits.

[Severity: High]
Does linking the file into the namespace here open a TOCTOU race window?

Since file_open_root() immediately links the restored file into the tmpfs
namespace, unprivileged userspace could potentially open and write or mmap
the file before memfd_luo_retrieve_folios() is called. This would instantiate
page cache folios, causing the subsequent shmem_add_to_page_cache() inside
memfd_luo_retrieve_folios() to hit an -EEXIST collision and permanently drop
the preserved data folios.

[ ... ]
> 	args->file = file;
> 	kho_restore_free(ser);
> 
> 	return 0;

[Severity: Medium]
Could this lead to a double-free or use-after-free regression of the KHO
folios on a successful retrieve?

This code frees the ser object but leaves args->serialized_data as a
dangling pointer.

[ ... ]
> static void tmpfs_luo_file_finish(struct liveupdate_file_op_args *args)
> {
> 	struct memfd_luo_folio_ser *folios_ser;
> 	struct tmpfs_luo_file_ser *ser;
> 
> 	/*
> 	 * A successful retrieve() already consumed the preserved memory, and a
> 	 * failed one cleaned up what it could. Only a file that was never
> 	 * retrieved is left to discard here.
> 	 */
> 	if (args->retrieve_status || !args->serialized_data)
> 		return;

[Severity: Medium]
If the LUO core framework sets args->retrieve_status to 0 upon a successful
return, tmpfs_luo_file_finish() might bypass this early return, dereference
the dangling args->serialized_data pointer, and mistakenly double-free the
folios. Could args->serialized_data be explicitly cleared after it is freed
to prevent this?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260923224408.3745689-1-pratyush@kernel.org?part=5

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-09-23 23:24 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23 22:43 [RFC PATCH 0/6] luo: tmpfs preservation Pratyush Yadav
2026-09-23 22:44 ` [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id Pratyush Yadav
2026-09-23 22:55   ` sashiko-bot
2026-09-23 22:44 ` [RFC PATCH 2/6] shmem: add tmpfs_create_mount() to create tmpfs mounts internally Pratyush Yadav
2026-09-23 22:54   ` sashiko-bot
2026-09-23 22:44 ` [RFC PATCH 3/6] fs/namespace: Add vfs_open_detached_mount() Pratyush Yadav
2026-09-23 23:01   ` sashiko-bot
2026-09-23 22:44 ` [RFC PATCH 4/6] mm/memfd_luo: allow preserving a tmpfs mount Pratyush Yadav
2026-09-23 23:06   ` sashiko-bot
2026-09-23 22:44 ` [RFC PATCH 5/6] mm/memfd_luo: allow preserving a tmpfs file Pratyush Yadav
2026-09-23 23:24   ` sashiko-bot
2026-09-23 22:44 ` [RFC PATCH 6/6] selftests/liveupdate: add tmpfs kexec test Pratyush Yadav
2026-09-23 23:14   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®