* [PATCH v2 0/2] mm/memfd_luo: preserve the sealed exec-bit state; fix retrieve() leaks
@ 2026-10-14 3:12 Haolong Wang
2026-10-14 3:12 ` [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
2026-10-14 3:12 ` [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
0 siblings, 2 replies; 6+ messages in thread
From: Haolong Wang @ 2026-10-14 3:12 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec, linux-mm, linux-kernel
v1: https://lore.kernel.org/all/20261014003558.357783-1-18766350203@163.com/
Sashiko reviewed v1 and reported two issues, both of them real: the execute
bits were sampled long before the memfd stopped being usable, so the record
could be stale by the time it was handed to the next kernel, and retrieve()
leaked the preserved folios on its error paths. Both are fixed here, together
with a third problem that the same reasoning exposes (the record carried a
single "some execute bit was set" flag, so a partially executable memfd was
restored with execute bits it never had).
Changes since v1:
- Record the user/group/other execute bits separately instead of one "some
execute bit was set" flag: an execute mask of 0100 was restored as 0111,
inverting the guarantee this series is about.
- Take the final sample of the execute mask and the seals in freeze(), under
the inode lock, and freeze that state for the rest of the transition.
shmem_setattr() (chmod), shmem_set_acl() (an access ACL rewrites the mode
without going through shmem_setattr()) and memfd_add_seals() all reject
changes from that point on, and a new .unfreeze callback clears the state
when the update is rolled back.
- Reject records with flag bits this version does not know.
- Bump the record version to "memfd-v3" and keep reading "memfd-v2" records
through a read-only handler, so that a record is never restored by a kernel
that does not understand its flags.
- Release the preserved folios when retrieve() fails, by sharing the cleanup
with memfd_luo_finish(). This one predates the exec-bit work and is a
separate patch with a Fixes tag.
- selftest: an execute mask of 0100 with its full 0x3e seal set, a memfd
without execute bits (MFD_NOEXEC_SEAL: mask 0000, seal set 0x20, still
writable), and a skip when vm.memfd_noexec forbids MFD_EXEC.
- Document the execute bits the record carries and the restrictions that
apply once the update is committed to.
On record versions. The flags cannot tell "the execute bits were clear" apart
from "the writer did not know about the flags", so records written before the
flags existed have to be told apart by the version string, which is compared
with strcmp(). The version is therefore bumped and a read-only handler for
"memfd-v2" is registered next to the current one:
record writer reading kernel and handler result
old v2 old kernel, v2 handler default executable mode
old v2 new kernel, legacy v2 reader no execute bits
new v3 new kernel, v3 handler exact exec mask and seals
new v3 old kernel no handler, -ENOENT
The legacy handler only reads records written by an older kernel: its
.can_preserve() always returns false, so it is never selected for an outgoing
preservation and every new record is serialized as "memfd-v3"; a "memfd-v3"
record is never handed to it either, because the compatible strings have to
match exactly. If registration of the legacy v2 reader fails, the already
registered v3 handler is unregistered as well, so the kernel never ends up with
only one of the two handlers registered.
Row two is a one-time behaviour change for records written by a kernel without
these flags: an executable memfd preserved by such a kernel comes back without
execute bits. That is the safe reading, but it is not what the old reader did
-- it never looked at the flags at all, it restored the default (executable)
mode -- so the change applies to every such record, not only to the ones that
were sealed with F_SEAL_EXEC. Without the version bump an old kernel would
instead read a new record, ignore the flags and restore the default executable
mode, undoing exactly the guarantee this series restores; with "memfd-v3" it
has no matching handler at all, so the deserialization of that session fails
with -ENOENT instead of the memfd being restored wrongly.
Two things I could not close here, both written down in the patches:
- f_pos can still change after freeze(). The LUO file API has no global
userspace quiescence before .freeze() runs, so this needs a framework-level
answer rather than a memfd one.
- If kho_restore_vmalloc() fails, the folio array cannot be walked and the
folios stay reserved; that API is not failure-atomic and cannot be retried
after a partial failure. A KHO-side fix or an allocation-free
discard/iterator API would close it.
Testing:
- the kernel builds from this tree with the liveupdate selftest config (plus
TMPFS_POSIX_ACL/TMPFS_XATTR, so that the new ACL path is compiled);
mm/memfd_luo.o, mm/shmem.o and mm/memfd.o build without warnings
- QEMU live-update round trip (preserve -> kexec -> retrieve) with the new
selftest: PASSED -- the memfd with execute mask 0100 and seal set 0x3e
comes back with the same mask and seal set, and the memfd without execute
bits comes back with mask 0000, seal set 0x20, data intact and writable
- the same test against a kernel without this change: "execute mask changed
across kexec: 0111 != 0100"
- checkpatch.pl --strict on the series: 0 errors, 0 warnings
- not covered by the run: the freeze-time rejection (it happens inside the
reboot() path, so userspace cannot observe it) and the ACL path at runtime
Haolong Wang (2):
mm/memfd_luo: carry the sealed exec-bit state in the preserved record
mm/memfd_luo: release preserved folios when retrieve fails
include/linux/kho/abi/memfd.h | 24 +-
include/linux/shmem_fs.h | 20 ++
mm/memfd.c | 6 +
mm/memfd_luo.c | 240 +++++++++++++++---
mm/shmem.c | 35 ++-
.../selftests/liveupdate/luo_kexec_simple.c | 208 +++++++++++++++
6 files changed, 490 insertions(+), 43 deletions(-)
base-commit: 3a0b8fa2eb36afc88b62a95f33f0c77c71fa5ded
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record
2026-10-14 3:12 [PATCH v2 0/2] mm/memfd_luo: preserve the sealed exec-bit state; fix retrieve() leaks Haolong Wang
@ 2026-10-14 3:12 ` Haolong Wang
2026-10-14 3:26 ` sashiko-bot
2026-10-14 3:12 ` [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
1 sibling, 1 reply; 6+ messages in thread
From: Haolong Wang @ 2026-10-14 3:12 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec, linux-mm, linux-kernel
F_SEAL_EXEC freezes the execute bits as they are at sealing time: a memfd
becomes permanently executable or permanently un-executable, bit by bit.
The preserved state of a memfd (struct memfd_luo_ser) carries the seals
but not the inode mode, and the restore path creates the file with
memfd_alloc_file("", MFD_ALLOW_SEALING) -- the default 0777 -- without
consulting the vm.memfd_noexec policy (that policy is applied on the
memfd_create() syscall path only).
A memfd whose execute mask was 0000 and whose seal set was F_SEAL_EXEC is
therefore restored with an execute mask of 0111 and F_SEAL_EXEC
re-applied on top of it: the guarantee inverts, and it is the seal that
made the memfd safe. Because the restored file is executable,
memfd_add_seals() also implies F_SEAL_WRITE, F_SEAL_FUTURE_WRITE,
F_SEAL_SHRINK and F_SEAL_GROW (the W^X rule), so the restored memfd comes
back with its write seals locked and its execute mask permanently
irrevocable (shmem_setattr() rejects any exec-bit change). This is a
functional regression that needs no attacker: the owner's own pwrite() to
the memfd succeeds before the update and fails with EPERM after it. The
inversion is also silent: F_GET_SEALS keeps reporting the seal.
Carry the state in the existing ser->flags field, which commit
8a552d68a86e ("mm: memfd_luo: preserve file seals") added for exactly
this kind of extension ("Unused flag bits must be set to 0"): preserve
records the user/group/other execute bits separately, retrieve restores
the exact mask on the fresh anonymous inode before the seals are
re-applied -- the W^X rule in memfd_add_seals() looks at the mode when it
decides whether to imply the write seals, so the mode has to be in place
first -- and a record with flag bits this version does not know is
rejected with -EOPNOTSUPP. The file is not published at that point, so
no other thread can observe the intermediate state. Only the execute
bits are carried: the read/write bits are not preserved today, so they
keep coming back at their 0666 default (0777 minus the execute bits).
A single "some execute bit was set" flag was considered first and
dropped: it turns an execute mask of 0100 (mode 0700) into 0111, which is
the very inversion this change is about, and it is the case
tools/testing/selftests/memfd/memfd_test.c asserts against. Reusing the
MFD_NOEXEC_SEAL alloc flag was also considered and dropped: it would
silently add F_SEAL_EXEC to memfds that never had it (a plain
non-executable memfd without the seal would come back with the seal
attached and its execute bits irrevocable), and it would couple the
restore to the current policy rather than to the state that was
preserved. A record that does not say a bit was set is restored without
it -- which is what a memfd created under vm.memfd_noexec=2 looks like,
and is also the safe reading for records written before these flags
existed (they are read through a separate handler, see "Record versions"
below).
The memfd stays usable between preserve() and freeze(): the frozen
mapping only pins down the size and the folios, while the file position,
the execute bits (as long as F_SEAL_EXEC is not set) and the seals can
still change. A seal added after preserve() would not be in the record,
and a chmod() that clears the execute bits would be lost, turning the W^X
state of the restored file around. freeze() is the last point at which
the record can be updated before it is handed to the next kernel, so it
re-samples the execute mask, the seals and the file position under the
inode lock that memfd_add_seals(), shmem_setattr() and shmem_set_acl()
also take, and, in the same critical section, freezes that state for the
rest of the transition: a change made after the sample cannot diverge
from the record either. The state is cleared again by the new .unfreeze
callback when the update is rolled back, and defensively by unpreserve().
A file that gained seals this version does not know fails the freeze,
which aborts the live update instead of handing over a stale record. The
ACL path is included because an access ACL rewrites inode->i_mode from
posix_acl_update_mode() without going through shmem_setattr(); the same
hole lets an access ACL rewrite the execute bits of a memfd that
F_SEAL_EXEC is supposed to protect, which is a pre-existing issue and is
left to a separate report.
With this, the execute mask and the seal set round-trip exactly: a memfd
without execute bits (execute mask 0000, seal set 0x20, which is what
MFD_NOEXEC_SEAL and vm.memfd_noexec=2 produce) comes back with the same
execute mask and seal set and stays writable by its owner; a memfd whose
execute mask was 0100 and whose seal set was 0x3e (F_SEAL_EXEC plus the
write seals the W^X rule implies) is restored with the same execute mask
and seal set, instead of an execute mask of 0111; a plain non-executable
memfd (0666, unsealed) stays unsealed with chmod-able execute bits; the
baseline executable memfd stays 0777 and still execve's; and
memfd_create() under vm.memfd_noexec=2 still forces NX on fresh memfds.
No seal is invented for any of them.
Measured on a QEMU live-update run (preserve -> kexec -> retrieve) with
this selftest: the memfd whose execute mask was 0100 and whose seal set
was 0x3e comes back with the same execute mask and the same seal set, and
the memfd without execute bits comes back with mask 0000, seal set 0x20,
its data intact and still writable by its owner. The same test against a
kernel without this change reports "execute mask changed across kexec:
0111 != 0100".
Refusing to preserve memfds with F_SEAL_EXEC was considered and rejected:
with vm.memfd_noexec >= 1 every memfd created without an explicit MFD_EXEC
gets MFD_NOEXEC_SEAL forced on it (and = 2 refuses the MFD_EXEC opt-out
outright), so the refusal would disable the whole preservation feature
exactly on the systems that harden the most. Preserving the full inode
mode would be the alternative, but three bits in the already-extensible
flags field are enough to reproduce the state F_SEAL_EXEC protects, and
the record layout is unchanged.
Record versions. The flags cannot tell "the execute bits were clear"
apart from "the writer did not know about the flags", so a record written
before the flags existed has to be told apart by the version string, which
is compared with strcmp(). The version is therefore bumped to
"memfd-v3" and a read-only handler for "memfd-v2" is registered next to
the current one:
record writer reading kernel and handler result
old v2 old kernel, v2 handler default executable mode
old v2 new kernel, legacy v2 reader no execute bits
new v3 new kernel, v3 handler exact exec mask and seals
new v3 old kernel no handler, -ENOENT
The legacy handler only reads records written by an older kernel: its
.can_preserve() always returns false, so it is never selected for an
outgoing preservation and every new record is serialized as "memfd-v3";
a "memfd-v3" record is never handed to it either, because the compatible
strings have to match exactly.
The second row is a one-time behaviour change for records written by a
kernel without these flags: an executable memfd preserved by such a kernel
comes back without execute bits. That is the safe reading, but it is not
what the old reader did -- it never looked at the flags and restored the
default (executable) mode -- so it is a change for every such record, not
only for the ones sealed with F_SEAL_EXEC. Without the version bump an
old kernel would instead read a new record, ignore the flags and restore
the default executable mode, undoing exactly the guarantee this patch
restores; with "memfd-v3" it has no matching handler at all, so the
deserialization of that session fails with -ENOENT instead of the memfd
being restored wrongly.
Changes in v2:
- record the three execute bits separately: a single "some execute bit
was set" flag restored an execute mask of 0100 as 0111
- take the final sample of the execute mask and the seals at freeze()
time: the memfd stays usable between preserve() and freeze(), so a
change in that window used to be lost (reported by Sashiko AI review)
- freeze that state for the rest of the transition (shmem_setattr(),
shmem_set_acl(), memfd_add_seals()), cleared by .unfreeze on rollback
- reject records with flag bits this version does not know
- bump the record version to "memfd-v3" and keep reading "memfd-v2"
records through a read-only handler: flags == 0 is read as "no execute
bits", which is a one-time change for the executable memfds written by
an older kernel
- selftest: an execute mask of 0100 with the full 0x3e seal set, a memfd
without execute bits (MFD_NOEXEC_SEAL, seal set 0x20, still writable),
and a skip when vm.memfd_noexec forbids MFD_EXEC
- document the execute bits the record carries and the restrictions that
apply once the update is committed to
Fixes: 8a552d68a86e ("mm: memfd_luo: preserve file seals")
Assisted-by: Pi:glm-5.3-flash
Signed-off-by: Haolong Wang <18766350203@163.com>
---
include/linux/kho/abi/memfd.h | 24 +-
include/linux/shmem_fs.h | 20 ++
mm/memfd.c | 6 +
mm/memfd_luo.c | 162 +++++++++++++-
mm/shmem.c | 35 ++-
.../selftests/liveupdate/luo_kexec_simple.c | 208 ++++++++++++++++++
6 files changed, 437 insertions(+), 18 deletions(-)
diff --git a/include/linux/kho/abi/memfd.h b/include/linux/kho/abi/memfd.h
index 08b10fea2afc..f773dd95401d 100644
--- a/include/linux/kho/abi/memfd.h
+++ b/include/linux/kho/abi/memfd.h
@@ -87,7 +87,29 @@ struct memfd_luo_ser {
struct kho_vmalloc folios;
} __packed;
+/*
+ * The execute permission bits are packed in user/group/other order. A record
+ * with none of these flags set is restored without execute permissions, which
+ * is also the safe interpretation for records written before these flags
+ * existed.
+ */
+#define MEMFD_LUO_FL_EXEC_USR BIT(0)
+#define MEMFD_LUO_FL_EXEC_GRP BIT(1)
+#define MEMFD_LUO_FL_EXEC_OTH BIT(2)
+#define MEMFD_LUO_FL_EXEC_MASK (MEMFD_LUO_FL_EXEC_USR | \
+ MEMFD_LUO_FL_EXEC_GRP | \
+ MEMFD_LUO_FL_EXEC_OTH)
+#define MEMFD_LUO_ALL_FLAGS MEMFD_LUO_FL_EXEC_MASK
+
/* The compatibility string for memfd file handler */
-#define MEMFD_LUO_FH_COMPATIBLE "memfd-v2"
+#define MEMFD_LUO_FH_COMPATIBLE "memfd-v3"
+/*
+ * Records written before the execute-bit flags existed. They carry no flag
+ * bits and are restored without execute permissions: the flags cannot tell
+ * "the execute bits were clear" and "the writer did not know about the flags"
+ * apart, and restoring without them is the safe reading. The current kernel
+ * still accepts these records but never writes them.
+ */
+#define MEMFD_LUO_FH_COMPATIBLE_V2 "memfd-v2"
#endif /* _LINUX_KHO_ABI_MEMFD_H */
diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index e729b9b0e38d..40e1ce87c30e 100644
--- a/include/linux/shmem_fs.h
+++ b/include/linux/shmem_fs.h
@@ -32,6 +32,12 @@ struct swap_iocb;
* isn't directly visible to userspace.
*/
#define SHMEM_F_MAPPING_FROZEN BIT(2)
+/*
+ * Disallow changes to the seal set and execute bits captured by the final LUO
+ * snapshot. This is set only for the reboot transition and is cleared if the
+ * transition is rolled back.
+ */
+#define SHMEM_F_SEAL_STATE_FROZEN BIT(3)
struct shmem_inode_info {
spinlock_t lock;
@@ -208,6 +214,20 @@ static inline void shmem_freeze(struct inode *inode, bool freeze)
SHMEM_I(inode)->flags &= ~SHMEM_F_MAPPING_FROZEN;
}
+/* Must be called with inode lock taken exclusive. */
+static inline void shmem_freeze_seal_state(struct inode *inode, bool freeze)
+{
+ if (freeze)
+ SHMEM_I(inode)->flags |= SHMEM_F_SEAL_STATE_FROZEN;
+ else
+ SHMEM_I(inode)->flags &= ~SHMEM_F_SEAL_STATE_FROZEN;
+}
+
+static inline bool shmem_seal_state_frozen(struct inode *inode)
+{
+ return SHMEM_I(inode)->flags & SHMEM_F_SEAL_STATE_FROZEN;
+}
+
/*
* If fallocate(FALLOC_FL_KEEP_SIZE) has been used, there may be pages
* beyond i_size's notion of EOF, which fallocate has committed to reserving:
diff --git a/mm/memfd.c b/mm/memfd.c
index abe13b291ddc..578ac31ed1a9 100644
--- a/mm/memfd.c
+++ b/mm/memfd.c
@@ -278,6 +278,12 @@ int memfd_add_seals(struct file *file, unsigned int seals)
goto unlock;
}
+ /* LUO has taken its final snapshot; do not let the seal set diverge. */
+ if (shmem_file(file) && shmem_seal_state_frozen(inode)) {
+ error = -EPERM;
+ goto unlock;
+ }
+
if (*file_seals & F_SEAL_SEAL) {
error = -EPERM;
goto unlock;
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 59de210bee5f..6fa086fe809b 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -50,6 +50,16 @@
* memfds are always opened with ``O_RDWR`` and ``O_LARGEFILE``. This property
* is maintained.
*
+ * Execute Permission Bits
+ * The user, group, and other execute bits are preserved individually. This
+ * is required to faithfully restore the state protected by ``F_SEAL_EXEC``.
+ *
+ * .. note::
+ * A record written by an older kernel (``memfd-v2``) does not carry the
+ * execute bits. It is restored without execute permissions, which is the
+ * safe reading: an executable memfd preserved by such a kernel comes back
+ * non-executable, and the next update records the exact state.
+ *
* Seals
* File seals set on the memfd are preserved and re-applied on restore.
* Only seals known to this LUO version (see ``MEMFD_LUO_ALL_SEALS``) may
@@ -84,6 +94,35 @@
#include "internal.h"
+static void memfd_luo_record_exec_mode(struct memfd_luo_ser *ser, umode_t mode)
+{
+ u32 flags = 0;
+
+ if (mode & 0100)
+ flags |= MEMFD_LUO_FL_EXEC_USR;
+ if (mode & 0010)
+ flags |= MEMFD_LUO_FL_EXEC_GRP;
+ if (mode & 0001)
+ flags |= MEMFD_LUO_FL_EXEC_OTH;
+
+ ser->flags &= ~MEMFD_LUO_FL_EXEC_MASK;
+ ser->flags |= flags;
+}
+
+static umode_t memfd_luo_restore_exec_mode(u32 flags)
+{
+ umode_t mode = 0;
+
+ if (flags & MEMFD_LUO_FL_EXEC_USR)
+ mode |= 0100;
+ if (flags & MEMFD_LUO_FL_EXEC_GRP)
+ mode |= 0010;
+ if (flags & MEMFD_LUO_FL_EXEC_OTH)
+ mode |= 0001;
+
+ return mode;
+}
+
static int memfd_luo_preserve_folios(struct file *file,
struct kho_vmalloc *kho_vmalloc,
struct memfd_luo_folio_ser **out_folios_ser,
@@ -300,6 +339,14 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
ser->size = inode_size;
ser->seals = seals;
+ /*
+ * F_SEAL_EXEC freezes each execute permission bit independently, so
+ * preserve the complete user/group/other execute mask. A record with no
+ * execute flags is restored without execute permissions, which is also
+ * the safe interpretation for records written before these flags existed.
+ */
+ memfd_luo_record_exec_mode(ser, inode->i_mode);
+
err = memfd_luo_preserve_folios(args->file, &ser->folios,
&folios_ser, &nr_folios);
if (err)
@@ -323,7 +370,9 @@ static int memfd_luo_preserve(struct liveupdate_file_op_args *args)
static int memfd_luo_freeze(struct liveupdate_file_op_args *args)
{
+ struct inode *inode = file_inode(args->file);
struct memfd_luo_ser *ser;
+ int seals;
if (WARN_ON_ONCE(!args->serialized_data))
return -EINVAL;
@@ -331,14 +380,47 @@ static int memfd_luo_freeze(struct liveupdate_file_op_args *args)
ser = phys_to_virt(args->serialized_data);
/*
- * The pos might have changed since prepare. Everything else stays the
- * same.
+ * The file is still usable between preserve() and freeze(), so its mode
+ * and seals can differ from the values recorded by preserve(). Take the
+ * final snapshot under the inode lock shared with memfd_add_seals(),
+ * shmem_setattr(), and shmem_set_acl(), then freeze those state fields
+ * before dropping the lock. This closes the window in which a later
+ * chmod(), ACL update, or F_ADD_SEALS could otherwise diverge from the
+ * state handed to the next kernel.
*/
+ inode_lock(inode);
+
+ seals = memfd_get_seals(args->file);
+ if (seals < 0) {
+ inode_unlock(inode);
+ return seals;
+ }
+
+ /* The file must not have gained seals that this version does not know. */
+ if (seals & ~MEMFD_LUO_ALL_SEALS) {
+ inode_unlock(inode);
+ return -EOPNOTSUPP;
+ }
+
+ memfd_luo_record_exec_mode(ser, inode->i_mode);
+ ser->seals = seals;
ser->pos = args->file->f_pos;
+ shmem_freeze_seal_state(inode, true);
+
+ inode_unlock(inode);
return 0;
}
+static void memfd_luo_unfreeze(struct liveupdate_file_op_args *args)
+{
+ struct inode *inode = file_inode(args->file);
+
+ inode_lock(inode);
+ shmem_freeze_seal_state(inode, false);
+ inode_unlock(inode);
+}
+
static void memfd_luo_unpreserve(struct liveupdate_file_op_args *args)
{
struct inode *inode = file_inode(args->file);
@@ -348,6 +430,7 @@ static void memfd_luo_unpreserve(struct liveupdate_file_op_args *args)
return;
inode_lock(inode);
+ shmem_freeze_seal_state(inode, false);
shmem_freeze(inode, false);
ser = phys_to_virt(args->serialized_data);
@@ -520,14 +603,16 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
struct memfd_luo_folio_ser *folios_ser;
struct memfd_luo_ser *ser;
struct file *file;
+ umode_t exec_mode;
int err;
ser = phys_to_virt(args->serialized_data);
if (!ser)
return -EINVAL;
- /* Make sure the file only has seals supported by this version. */
- if (ser->seals & ~MEMFD_LUO_ALL_SEALS) {
+ /* Make sure the record only contains state supported by this version. */
+ if ((ser->seals & ~MEMFD_LUO_ALL_SEALS) ||
+ (ser->flags & ~MEMFD_LUO_ALL_FLAGS)) {
err = -EOPNOTSUPP;
goto free_ser;
}
@@ -543,6 +628,17 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
goto free_ser;
}
+ /*
+ * Reproduce the exact execute mask before the seals are re-applied:
+ * F_SEAL_EXEC freezes each execute bit, and the W^X rule in
+ * memfd_add_seals() looks at the mode when deciding whether to imply the
+ * write seals. The fresh anonymous inode has not been published, so no
+ * other thread can observe this intermediate state.
+ */
+ exec_mode = memfd_luo_restore_exec_mode(ser->flags);
+ file->f_inode->i_mode &= ~0111;
+ file->f_inode->i_mode |= exec_mode;
+
err = memfd_add_seals(file, ser->seals);
if (err) {
pr_err("failed to add seals: %pe\n", ERR_PTR(err));
@@ -592,6 +688,7 @@ static unsigned long memfd_luo_get_id(struct file *file)
static const struct liveupdate_file_ops memfd_luo_file_ops = {
.freeze = memfd_luo_freeze,
+ .unfreeze = memfd_luo_unfreeze,
.finish = memfd_luo_finish,
.retrieve = memfd_luo_retrieve,
.preserve = memfd_luo_preserve,
@@ -606,15 +703,62 @@ static struct liveupdate_file_handler memfd_luo_handler = {
.compatible = MEMFD_LUO_FH_COMPATIBLE,
};
+/*
+ * Reads records written before the execute-bit flags existed. Those have no
+ * flag bits and are restored without execute permissions (see the note on
+ * old records in the DOC section above). can_preserve() never claims a file,
+ * so an outgoing preservation is always written by the handler above and
+ * never as "memfd-v2".
+ */
+static bool memfd_luo_can_preserve_v2(struct liveupdate_file_handler *handler,
+ struct file *file)
+{
+ return false;
+}
+
+static const struct liveupdate_file_ops memfd_luo_file_ops_v2 = {
+ .freeze = memfd_luo_freeze,
+ .unfreeze = memfd_luo_unfreeze,
+ .finish = memfd_luo_finish,
+ .retrieve = memfd_luo_retrieve,
+ .preserve = memfd_luo_preserve,
+ .unpreserve = memfd_luo_unpreserve,
+ .can_preserve = memfd_luo_can_preserve_v2,
+ .get_id = memfd_luo_get_id,
+ .owner = THIS_MODULE,
+};
+
+static struct liveupdate_file_handler memfd_luo_handler_v2 = {
+ .ops = &memfd_luo_file_ops_v2,
+ .compatible = MEMFD_LUO_FH_COMPATIBLE_V2,
+};
+
static int __init memfd_luo_init(void)
{
- int err = liveupdate_register_file_handler(&memfd_luo_handler);
+ int err;
+
+ err = liveupdate_register_file_handler(&memfd_luo_handler);
+ if (err) {
+ if (err != -EOPNOTSUPP) {
+ pr_err("Could not register luo filesystem handler: %pe\n",
+ ERR_PTR(err));
+ return err;
+ }
+
+ return 0;
+ }
- if (err && err != -EOPNOTSUPP) {
- pr_err("Could not register luo filesystem handler: %pe\n",
- ERR_PTR(err));
+ err = liveupdate_register_file_handler(&memfd_luo_handler_v2);
+ if (err) {
+ liveupdate_unregister_file_handler(&memfd_luo_handler);
+
+ if (err != -EOPNOTSUPP) {
+ pr_err("Could not register the memfd-v2 record reader: %pe\n",
+ ERR_PTR(err));
+ return err;
+ }
- return err;
+ return 0;
}
return 0;
diff --git a/mm/shmem.c b/mm/shmem.c
index b51f83c970bb..55f4dbd429eb 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -1326,11 +1326,10 @@ static int shmem_setattr(struct mnt_idmap *idmap,
if (error)
return error;
- if ((info->seals & F_SEAL_EXEC) && (attr->ia_valid & ATTR_MODE)) {
- if ((inode->i_mode ^ attr->ia_mode) & 0111) {
- return -EPERM;
- }
- }
+ if ((attr->ia_valid & ATTR_MODE) &&
+ ((info->seals & F_SEAL_EXEC) || shmem_seal_state_frozen(inode)) &&
+ ((inode->i_mode ^ attr->ia_mode) & 0111))
+ return -EPERM;
if (S_ISREG(inode->i_mode) && (attr->ia_valid & ATTR_SIZE)) {
loff_t oldsize = inode->i_size;
@@ -1394,6 +1393,24 @@ static int shmem_setattr(struct mnt_idmap *idmap,
return error;
}
+#ifdef CONFIG_TMPFS_POSIX_ACL
+static int shmem_set_acl(struct mnt_idmap *idmap, struct dentry *dentry,
+ struct posix_acl *acl, int type)
+{
+ struct inode *inode = d_inode(dentry);
+
+ /*
+ * An access ACL can rewrite the inode's execute bits without going
+ * through shmem_setattr(). Once LUO has captured its final seal/mode
+ * snapshot, reject that alternate mutation path as well.
+ */
+ if (type == ACL_TYPE_ACCESS && shmem_seal_state_frozen(inode))
+ return -EPERM;
+
+ return simple_set_acl(idmap, dentry, acl, type);
+}
+#endif
+
static void shmem_evict_inode(struct inode *inode)
{
struct shmem_inode_info *info = SHMEM_I(inode);
@@ -5156,7 +5173,9 @@ static const struct inode_operations shmem_inode_operations = {
.setattr = shmem_setattr,
#ifdef CONFIG_TMPFS_XATTR
.listxattr = shmem_listxattr,
- .set_acl = simple_set_acl,
+#ifdef CONFIG_TMPFS_POSIX_ACL
+ .set_acl = shmem_set_acl,
+#endif
.fileattr_get = shmem_fileattr_get,
.fileattr_set = shmem_fileattr_set,
#endif
@@ -5184,7 +5203,7 @@ static const struct inode_operations shmem_dir_inode_operations = {
#endif
#ifdef CONFIG_TMPFS_POSIX_ACL
.setattr = shmem_setattr,
- .set_acl = simple_set_acl,
+ .set_acl = shmem_set_acl,
#endif
};
@@ -5195,7 +5214,7 @@ static const struct inode_operations shmem_special_inode_operations = {
#endif
#ifdef CONFIG_TMPFS_POSIX_ACL
.setattr = shmem_setattr,
- .set_acl = simple_set_acl,
+ .set_acl = shmem_set_acl,
#endif
};
diff --git a/tools/testing/selftests/liveupdate/luo_kexec_simple.c b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
index d7ac1f3dc4cb..30836043f894 100644
--- a/tools/testing/selftests/liveupdate/luo_kexec_simple.c
+++ b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-2.0-only
+#define _GNU_SOURCE
/*
* Copyright (c) 2025, Google LLC.
@@ -8,16 +9,211 @@
* across a single kexec reboot.
*/
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <linux/memfd.h>
+
#include "luo_test_utils.h"
+#ifndef F_SEAL_SHRINK
+#define F_SEAL_SHRINK 0x0002
+#endif
+#ifndef F_SEAL_GROW
+#define F_SEAL_GROW 0x0004
+#endif
+#ifndef F_SEAL_WRITE
+#define F_SEAL_WRITE 0x0008
+#endif
+#ifndef F_SEAL_FUTURE_WRITE
+#define F_SEAL_FUTURE_WRITE 0x0010
+#endif
+#ifndef F_SEAL_EXEC
+#define F_SEAL_EXEC 0x0020
+#endif
+#ifndef MFD_NOEXEC_SEAL
+#define MFD_NOEXEC_SEAL 0x0008U
+#endif
+
+/*
+ * F_SEAL_EXEC implies the write seals when the memfd has execute bits (the
+ * W^X rule in memfd_add_seals()), so a memfd that was chmod()ed to 0700
+ * before F_SEAL_EXEC was added ends up with exactly this seal set.
+ */
+#define F_WX_SEALS (F_SEAL_EXEC | F_SEAL_SHRINK | F_SEAL_GROW | \
+ F_SEAL_WRITE | F_SEAL_FUTURE_WRITE)
+
#define TEST_SESSION_NAME "test-session"
#define TEST_MEMFD_TOKEN 0x1A
+#define TEST_EXEC_MEMFD_TOKEN 0x1B
+#define TEST_NX_MEMFD_TOKEN 0x1C
#define TEST_MEMFD_DATA "hello kexec world"
/* Constants for the state-tracking mechanism, specific to this test file. */
#define STATE_SESSION_NAME "kexec_simple_state"
#define STATE_MEMFD_TOKEN 999
+static void create_and_preserve_exec_memfd(int session_fd)
+{
+ struct liveupdate_session_preserve_fd arg = {
+ .size = sizeof(arg),
+ .token = TEST_EXEC_MEMFD_TOKEN,
+ };
+ int mfd, seals;
+
+ mfd = memfd_create("exec-mode-test", MFD_ALLOW_SEALING | MFD_EXEC);
+ if (mfd < 0) {
+ /* vm.memfd_noexec=2 refuses the MFD_EXEC opt-out. */
+ if (errno == EACCES)
+ ksft_exit_skip("vm.memfd_noexec forbids MFD_EXEC\n");
+ fail_exit("memfd_create for execute-mode test");
+ }
+
+ if (fchmod(mfd, 0700) < 0)
+ fail_exit("fchmod for execute-mode test");
+ if (fcntl(mfd, F_ADD_SEALS, F_SEAL_EXEC) < 0)
+ fail_exit("F_ADD_SEALS for execute-mode test");
+
+ /* The execute bits pull in the write seals: record the whole set. */
+ seals = fcntl(mfd, F_GET_SEALS);
+ if (seals < 0)
+ fail_exit("F_GET_SEALS for execute-mode test");
+ if (seals != F_WX_SEALS)
+ ksft_exit_fail_msg("execute-mode seals 0x%x != 0x%x\n", seals,
+ F_WX_SEALS);
+ ksft_print_msg("[STAGE 1] exec-mode memfd: execute mask 0100, seals 0x%x\n",
+ seals);
+
+ arg.fd = mfd;
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &arg) < 0)
+ fail_exit("preserve execute-mode memfd");
+
+ close(mfd);
+}
+
+static void restore_and_verify_exec_memfd(int session_fd)
+{
+ struct liveupdate_session_retrieve_fd arg = {
+ .size = sizeof(arg),
+ .token = TEST_EXEC_MEMFD_TOKEN,
+ };
+ struct stat st;
+ int mfd, seals;
+
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_RETRIEVE_FD, &arg) < 0)
+ fail_exit("retrieve execute-mode memfd");
+ mfd = arg.fd;
+
+ if (fstat(mfd, &st) < 0)
+ fail_exit("fstat execute-mode memfd");
+ if ((st.st_mode & 0111) != 0100)
+ ksft_exit_fail_msg("execute mask changed across kexec: 0%o != 0100\n",
+ st.st_mode & 0111);
+
+ seals = fcntl(mfd, F_GET_SEALS);
+ if (seals < 0)
+ fail_exit("F_GET_SEALS for execute-mode test");
+ if (seals != F_WX_SEALS)
+ ksft_exit_fail_msg("seals changed across kexec: 0x%x != 0x%x\n",
+ seals, F_WX_SEALS);
+
+ ksft_print_msg("[STAGE 2] exec-mode memfd: execute mask 0%o, seals 0x%x\n",
+ st.st_mode & 0111, seals);
+
+ close(mfd);
+}
+
+/*
+ * The originally reported case: a memfd without execute bits and with
+ * F_SEAL_EXEC (what memfd_create(MFD_NOEXEC_SEAL) and vm.memfd_noexec=2
+ * produce) has to come back without execute bits, with the same seal set, and
+ * still writable by its owner -- F_SEAL_EXEC must not drag in the write seals
+ * for a memfd that has no execute bits.
+ */
+static void create_and_preserve_nx_memfd(int session_fd)
+{
+ struct liveupdate_session_preserve_fd arg = {
+ .size = sizeof(arg),
+ .token = TEST_NX_MEMFD_TOKEN,
+ };
+ struct stat st;
+ int mfd, seals;
+
+ mfd = memfd_create("nx-mode-test", MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL);
+ if (mfd < 0)
+ fail_exit("memfd_create for NX-mode test");
+
+ if (fstat(mfd, &st) < 0)
+ fail_exit("fstat for NX-mode test");
+ if (st.st_mode & 0111)
+ ksft_exit_fail_msg("NX-mode memfd has execute bits (0%o)\n",
+ st.st_mode);
+
+ seals = fcntl(mfd, F_GET_SEALS);
+ if (seals < 0)
+ fail_exit("F_GET_SEALS for NX-mode test");
+ if (seals != F_SEAL_EXEC)
+ ksft_exit_fail_msg("NX-mode seals 0x%x != 0x%x\n", seals,
+ F_SEAL_EXEC);
+
+ if (pwrite(mfd, TEST_MEMFD_DATA, sizeof(TEST_MEMFD_DATA) - 1, 0) !=
+ sizeof(TEST_MEMFD_DATA) - 1)
+ fail_exit("NX-mode memfd is not writable");
+
+ ksft_print_msg("[STAGE 1] NX-mode memfd: execute mask 0000, seals 0x%x\n",
+ seals);
+
+ arg.fd = mfd;
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &arg) < 0)
+ fail_exit("preserve NX-mode memfd");
+
+ close(mfd);
+}
+
+static void restore_and_verify_nx_memfd(int session_fd)
+{
+ struct liveupdate_session_retrieve_fd arg = {
+ .size = sizeof(arg),
+ .token = TEST_NX_MEMFD_TOKEN,
+ };
+ char buf[sizeof(TEST_MEMFD_DATA)] = {0};
+ struct stat st;
+ int mfd, seals;
+
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_RETRIEVE_FD, &arg) < 0)
+ fail_exit("retrieve NX-mode memfd");
+ mfd = arg.fd;
+
+ if (fstat(mfd, &st) < 0)
+ fail_exit("fstat NX-mode memfd");
+ if (st.st_mode & 0111)
+ ksft_exit_fail_msg("NX memfd came back executable: execute mask 0%o\n",
+ st.st_mode & 0111);
+
+ seals = fcntl(mfd, F_GET_SEALS);
+ if (seals < 0)
+ fail_exit("F_GET_SEALS for NX-mode test");
+ if (seals != F_SEAL_EXEC)
+ ksft_exit_fail_msg("NX memfd seals 0x%x != 0x%x\n", seals,
+ F_SEAL_EXEC);
+
+ if (pread(mfd, buf, sizeof(buf) - 1, 0) != sizeof(buf) - 1)
+ fail_exit("pread NX-mode memfd");
+ if (strcmp(buf, TEST_MEMFD_DATA))
+ ksft_exit_fail_msg("NX memfd data changed: %s\n", buf);
+
+ if (pwrite(mfd, "x", 1, 0) != 1)
+ ksft_exit_fail_msg("NX memfd is not writable by its owner\n");
+
+ ksft_print_msg("[STAGE 2] NX-mode memfd: execute mask 0000, seals 0x%x, writable\n",
+ seals);
+
+ close(mfd);
+}
+
/* Stage 1: Executed before the kexec reboot. */
static void run_stage_1(int luo_fd)
{
@@ -40,6 +236,12 @@ static void run_stage_1(int luo_fd)
TEST_MEMFD_TOKEN);
}
+ ksft_print_msg("[STAGE 1] Preserving mode 0700 + F_SEAL_EXEC memfd...\n");
+ create_and_preserve_exec_memfd(session_fd);
+
+ ksft_print_msg("[STAGE 1] Preserving NX (no execute bits) + F_SEAL_EXEC memfd...\n");
+ create_and_preserve_nx_memfd(session_fd);
+
close(luo_fd);
daemonize_and_wait();
}
@@ -68,6 +270,12 @@ static void run_stage_2(int luo_fd, int state_session_fd)
fail_exit("restore_and_verify_memfd for token %#x", TEST_MEMFD_TOKEN);
close(mfd);
+ ksft_print_msg("[STAGE 2] Verifying exact execute-mode round trip...\n");
+ restore_and_verify_exec_memfd(session_fd);
+
+ ksft_print_msg("[STAGE 2] Verifying NX round trip...\n");
+ restore_and_verify_nx_memfd(session_fd);
+
ksft_print_msg("[STAGE 2] Test data verified successfully.\n");
ksft_print_msg("[STAGE 2] Finalizing test session...\n");
if (luo_session_finish(session_fd) < 0)
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails
2026-10-14 3:12 [PATCH v2 0/2] mm/memfd_luo: preserve the sealed exec-bit state; fix retrieve() leaks Haolong Wang
2026-10-14 3:12 ` [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
@ 2026-10-14 3:12 ` Haolong Wang
2026-10-14 3:22 ` sashiko-bot
1 sibling, 1 reply; 6+ messages in thread
From: Haolong Wang @ 2026-10-14 3:12 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec, linux-mm, linux-kernel
retrieve() only frees the serialization structure on its error paths, but
the folios it describes are still preserved at that point:
memfd_luo_finish() deliberately skips its own cleanup for records whose
retrieve() failed, since it relies on retrieve() to have released them.
The folios and the folio array are therefore never handed back and stay
reserved in the next kernel.
Release them from retrieve() as well, by sharing the cleanup with
memfd_luo_finish(). After the record has been validated, the folio array
is restored before the file is allocated, so a failure from
memfd_alloc_file() or memfd_add_seals() can release the folios directly
instead of allocating memory while already handling a failure; a record
this version does not support is rejected before that and uses the shared
cleanup too. The folios may only be released while they are still fully
preserved: once memfd_luo_retrieve_folios() has run it has consumed every
folio it touched -- the ones added to the file go away with it, the ones
that could not be added are released in place -- and releasing them a
second time would warn and free memory that is in use.
Two error paths still cannot release the folios: if
kho_restore_vmalloc() fails, the array cannot be walked at all, and that
API is not failure-atomic, so a partial failure cannot be retried either.
Closing those needs a KHO-side fix or an allocation-free discard/iterator
API and is left alone here.
Found by code inspection while extending the preserved record (reported
by Sashiko AI review).
Fixes: f85b1c6af5bc ("liveupdate: luo_file: remember retrieve() status")
Assisted-by: Pi:glm-5.3-flash
Signed-off-by: Haolong Wang <18766350203@163.com>
---
mm/memfd_luo.c | 78 ++++++++++++++++++++++++++++++++++----------------
1 file changed, 53 insertions(+), 25 deletions(-)
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 6fa086fe809b..e7dbb81e75a3 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -467,9 +467,31 @@ static void memfd_luo_discard_folios(const struct memfd_luo_folio_ser *folios_se
}
}
-static void memfd_luo_finish(struct liveupdate_file_op_args *args)
+/*
+ * Release a preserved record: return its folios to the page allocator and free
+ * the record itself. Only valid for records whose folios were never handed to
+ * a file -- handing a folio to a file consumes its last reference, and
+ * memfd_luo_discard_folios() would release it a second time.
+ */
+static void memfd_luo_release_ser(struct memfd_luo_ser *ser)
{
struct memfd_luo_folio_ser *folios_ser;
+
+ if (ser->nr_folios) {
+ folios_ser = kho_restore_vmalloc(&ser->folios);
+ if (!folios_ser)
+ goto out;
+
+ memfd_luo_discard_folios(folios_ser, ser->nr_folios);
+ vfree(folios_ser);
+ }
+
+out:
+ kho_restore_free(ser);
+}
+
+static void memfd_luo_finish(struct liveupdate_file_op_args *args)
+{
struct memfd_luo_ser *ser;
/*
@@ -484,17 +506,7 @@ static void memfd_luo_finish(struct liveupdate_file_op_args *args)
if (!ser)
return;
- if (ser->nr_folios) {
- folios_ser = kho_restore_vmalloc(&ser->folios);
- if (!folios_ser)
- goto out;
-
- memfd_luo_discard_folios(folios_ser, ser->nr_folios);
- vfree(folios_ser);
- }
-
-out:
- kho_restore_free(ser);
+ memfd_luo_release_ser(ser);
}
static int memfd_luo_retrieve_folios(struct file *file,
@@ -600,7 +612,7 @@ static int memfd_luo_retrieve_folios(struct file *file,
static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
{
- struct memfd_luo_folio_ser *folios_ser;
+ struct memfd_luo_folio_ser *folios_ser = NULL;
struct memfd_luo_ser *ser;
struct file *file;
umode_t exec_mode;
@@ -614,18 +626,28 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
if ((ser->seals & ~MEMFD_LUO_ALL_SEALS) ||
(ser->flags & ~MEMFD_LUO_ALL_FLAGS)) {
err = -EOPNOTSUPP;
- goto free_ser;
+ goto release_ser;
}
/*
- * The seals are preserved. Allow sealing here so they can be added
- * later.
+ * Restore the folio array before allocating the file. Once this succeeds,
+ * every later error path can release the preserved folios directly instead
+ * of allocating memory again while already handling a failure.
*/
+ if (ser->nr_folios) {
+ folios_ser = kho_restore_vmalloc(&ser->folios);
+ if (!folios_ser) {
+ /* kho_restore_vmalloc() is not safe to retry after failure. */
+ err = -EINVAL;
+ goto free_ser;
+ }
+ }
+
file = memfd_alloc_file("", MFD_ALLOW_SEALING);
if (IS_ERR(file)) {
pr_err("failed to setup file: %pe\n", file);
err = PTR_ERR(file);
- goto free_ser;
+ goto discard_folios;
}
/*
@@ -648,15 +670,12 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
vfs_setpos(file, ser->pos, MAX_LFS_FILESIZE);
i_size_write(file_inode(file), ser->size);
- if (ser->nr_folios) {
- folios_ser = kho_restore_vmalloc(&ser->folios);
- if (!folios_ser) {
- err = -EINVAL;
- goto put_file;
- }
-
- err = memfd_luo_retrieve_folios(file, folios_ser, ser->nr_folios);
+ if (folios_ser) {
+ /* retrieve_folios() consumes every preserved data folio. */
+ err = memfd_luo_retrieve_folios(file, folios_ser,
+ ser->nr_folios);
vfree(folios_ser);
+ folios_ser = NULL;
if (err)
goto put_file;
}
@@ -668,9 +687,18 @@ static int memfd_luo_retrieve(struct liveupdate_file_op_args *args)
put_file:
fput(file);
+discard_folios:
+ if (folios_ser) {
+ memfd_luo_discard_folios(folios_ser, ser->nr_folios);
+ vfree(folios_ser);
+ }
free_ser:
kho_restore_free(ser);
return err;
+
+release_ser:
+ memfd_luo_release_ser(ser);
+ return err;
}
static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails
2026-10-14 3:12 ` [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
@ 2026-10-14 3:22 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-10-14 3:22 UTC (permalink / raw)
To: Haolong Wang, Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec
> retrieve() only frees the serialization structure on its error paths, but
> the folios it describes are still preserved at that point:
> memfd_luo_finish() deliberately skips its own cleanup for records whose
> retrieve() failed, since it relies on retrieve() to have released them.
> The folios and the folio array are therefore never handed back and stay
> reserved in the next kernel.
>
> Release them from retrieve() as well, by sharing the cleanup with
> memfd_luo_finish(). After the record has been validated, the folio array
> is restored before the file is allocated, so a failure from
> memfd_alloc_file() or memfd_add_seals() can release the folios directly
> instead of allocating memory while already handling a failure; a record
> this version does not support is rejected before that and uses the shared
> cleanup too. The folios may only be released while they are still fully
> preserved: once memfd_luo_retrieve_folios() has run it has consumed every
> [ ... ]
> by Sashiko AI review).
>
> Fixes: f85b1c6af5bc ("liveupdate: luo_file: remember retrieve() status")
> Assisted-by: Pi:glm-5.3-flash
> Signed-off-by: Haolong Wang <18766350203@163.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261014031254.432582-1-18766350203@163.com?part=2
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record
2026-10-14 3:12 ` [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
@ 2026-10-14 3:26 ` sashiko-bot
2026-10-14 3:52 ` Haolong Wang
0 siblings, 1 reply; 6+ messages in thread
From: sashiko-bot @ 2026-10-14 3:26 UTC (permalink / raw)
To: Haolong Wang, Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mm: data race on SHMEM_I(inode)->flags via SysV SHM LUO preservation
--
commit e3ce8882f8b9c8b13e7c221a9ec8a0c481093c2f
Author: Haolong Wang <18766350203@163.com>
mm/memfd_luo: carry the sealed exec-bit state in the preserved record
This commit extends the memfd live update (LUO) preservation to record
the exact execute permission bits. This ensures that the restored memfd
retains its original execution state and seal protection, rather than
reverting to a default that might invert the W^X guarantee. It also adds
a compatibility layer for older record versions.
> diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
> index e729b9b0e38d4..40e1ce87c30e0 100644
> --- a/include/linux/shmem_fs.h
> +++ b/include/linux/shmem_fs.h
[ ... ]
> @@ -208,6 +214,20 @@ static inline void shmem_freeze(struct inode *inode, bool freeze)
> SHMEM_I(inode)->flags &= ~SHMEM_F_MAPPING_FROZEN;
> }
>
> +/* Must be called with inode lock taken exclusive. */
> +static inline void shmem_freeze_seal_state(struct inode *inode, bool freeze)
> +{
> + if (freeze)
> + SHMEM_I(inode)->flags |= SHMEM_F_SEAL_STATE_FROZEN;
> + else
> + SHMEM_I(inode)->flags &= ~SHMEM_F_SEAL_STATE_FROZEN;
> +}
[Severity: Medium]
Is it possible for this non-atomic flag update to race with SysV SHM?
The LUO memfd handler's can_preserve check admits files if they are
shmem_file() and have no links. SysV SHM files are internally created
as unlinked shmem files, meaning a user can obtain a file descriptor
for an attached SysV SHM segment via /proc/self/map_files/ and pass it
to the LUO preservation ioctl.
If a SysV SHM file reaches this freeze path, shmem_freeze_seal_state()
modifies SHMEM_I(inode)->flags using non-atomic bitwise operations
under inode_lock. Concurrently, a user can call shmctl(SHM_LOCK) which
modifies the exact same flags field via shmem_lock() while holding only
the ipc_lock_object spinlock.
Could this concurrent read-modify-write cause lost flag updates,
potentially bypassing the LUO seal freeze state or corrupting the SysV
SHM locked state?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261014031254.432582-1-18766350203@163.com?part=1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record
2026-10-14 3:26 ` sashiko-bot
@ 2026-10-14 3:52 ` Haolong Wang
0 siblings, 0 replies; 6+ messages in thread
From: Haolong Wang @ 2026-10-14 3:52 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec, linux-mm, linux-kernel, sashiko-reviews
Thanks, this is valid, and it covers the pre-existing bit as well.
SysV SHM is created through shmem_kernel_file_setup(), which is the wrapper
that passes S_PRIVATE down to __shmem_file_setup():
shmem_kernel_file_setup() -> __shmem_file_setup(..., S_PRIVATE)
shmem_file_setup() -> __shmem_file_setup(..., 0)
Both end in clear_nlink(), so the inode is unlinked either way and the
existing predicate
return shmem_file(file) && !inode->i_nlink;
admits the segment. A descriptor obtained through /proc/<pid>/map_files/
refers to the same inode, so it reaches the preserve ioctl, and the flags
word it then shares is the one SHMEM_F_LOCKED lives in: shmem_lock() states
it itself ("What serializes the accesses to info->flags? ipc_lock_object()
when called from shmctl_do_lock()"), while the LUO side holds the inode
lock. That mismatch is older than this series -- SHMEM_F_MAPPING_FROZEN
was already updated the same way, so the new seal-state freeze only adds
another bit to it.
I will add a preparatory patch that rejects kernel-private shmem inodes
from the memfd LUO handler:
return shmem_file(file) && !IS_PRIVATE(inode) && !inode->i_nlink;
Real memfds come from shmem_file_setup() without S_PRIVATE, so memfd
support is untouched. What the check removes is exactly the
shmem_kernel_file_setup() set: SysV SHM, big_key payloads, the XFS buffer
and scrub files, and the anonymous /dev/zero mapping file. Because
SHMEM_F_LOCKED is only maintained for SysV segments, dropping those also
removes the handler's only concurrent writer of that word. That is why
this does not need the flags word synchronized or the shmem_lock()
read-modify-writes turned into atomic bit operations: both sides would have
to change for that to help (one atomic side still loses against one plain
read-modify-write side), and it would be a patch for the shmem and ipc
maintainers rather than for this series.
One thing I am not claiming: this does not narrow the handler to
memfd_create() objects. Unlinked non-private shmem files, a deleted tmpfs
file for example, keep being accepted; they are simply not part of this
race.
Measured on the same tree, with CONFIG_CHECKPOINT_RESTORE and CONFIG_SYSVIPC
enabled so that a segment descriptor is reachable, running the preserve
ioctl on the descriptor taken from /proc/self/map_files (memfd_luo is the
only file handler registered in that configuration):
kernel SysV SHM fd memfd unlinked tmpfs
without the patch accepted (0) accepted accepted
with the patch -ENOENT (errno 2) accepted accepted
The sysv descriptor reads back as "/SYSV00000000 (deleted)"; the two other
objects are the controls that show the ioctl path still works and that the
check is limited to kernel-private files.
v3 will follow as a separate thread: the new patch first, then the two
patches of v2 unchanged.
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-14 3:52 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 3:12 [PATCH v2 0/2] mm/memfd_luo: preserve the sealed exec-bit state; fix retrieve() leaks Haolong Wang
2026-10-14 3:12 ` [PATCH v2 1/2] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
2026-10-14 3:26 ` sashiko-bot
2026-10-14 3:52 ` Haolong Wang
2026-10-14 3:12 ` [PATCH v2 2/2] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
2026-10-14 3:22 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.