* [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; 14+ 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] 14+ 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
2026-09-24 13:26 ` Pratyush Yadav
0 siblings, 1 reply; 14+ 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] 14+ messages in thread* Re: [RFC PATCH 1/6] liveupdate: luo_file: look up outgoing tokens by id
2026-09-23 22:55 ` sashiko-bot
@ 2026-09-24 13:26 ` Pratyush Yadav
0 siblings, 0 replies; 14+ messages in thread
From: Pratyush Yadav @ 2026-09-24 13:26 UTC (permalink / raw)
To: sashiko-bot
Cc: Pasha Tatashin, Andrew Morton, Samiullah Khawaja, Pratyush Yadav,
David Hildenbrand, Alexander Graf, Baolin Wang, Mike Rapoport,
David Matlack, Hugh Dickins, Lorenzo Stoakes, sashiko-reviews,
linux-kernel, kexec, linux-mm
On Wed, Sep 23 2026, sashiko-bot@kernel.org wrote:
> 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
> --
Let me reply to Sashiko for this patch since I think we should take this
regardless of where this series goes. I think struct file is the wrong
primitive for liveupdate_get_token_outgoing().
I'll ignore Sashiko on other patches. It does raise some good points,
but code correctness isn't the main point of the series. I'll fix them
up on the main posting if we agree to go in this direction.
>
> 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.
The IDs are supposed to be unique in the whole session. That's the whole
point of the ID mechanism. If handlers use overlapping IDs, the handlers
are broken and should be fixed.
>
> Could the API require the expected file handler context to scope the search
> safely?
This can be done but only when there is a need for it, which we have not
seen so far. Without a use-case, namespacing the IDs per-file-handler is
overengineering.
>
>> {
>> 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.
That's a broken caller and should be fixed.
>
> 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.
A very large number of files with dependencies need to be preserved for
this to be a problem. We haven't hit this bottleneck so far and don't
think will hit it at least in the near future.
>
> 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?
Not a bad idea to be fair. But that's an independent fixup.
>
>> if (tokenp)
>> *tokenp = luo_file->token;
>> err = 0;
--
Regards,
Pratyush Yadav
^ permalink raw reply [flat|nested] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ 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; 14+ 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] 14+ messages in thread