* [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files
@ 2026-10-14 3:52 Haolong Wang
2026-10-14 3:52 ` [PATCH v3 1/3] mm/memfd_luo: " Haolong Wang
` (3 more replies)
0 siblings, 4 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
Previous revisions:
https://lore.kernel.org/all/20261014003558.357783-1-18766350203@163.com/
https://lore.kernel.org/all/20261014031254.432582-1-18766350203@163.com/
Sashiko reviewed v2: the folio-release patch came back clean ("no issues.
It looks great!"), while the exec-bit patch got one report that holds up.
The handler's can_preserve() accepts any unlinked shmem file, and SysV SHM
segments are unlinked shmem files -- a segment descriptor obtained through
/proc/<pid>/map_files/ goes straight into the preservation path. That
matters because preservation updates shmem_inode_info.flags under the inode
lock, while SHM_LOCK and SHM_UNLOCK update SHMEM_F_LOCKED in the same word
through shmem_lock(), serialized by ipc_lock_object() instead: two
read-modify-write cycles on one word with no common lock, so either the
frozen bits or SHMEM_F_LOCKED can be lost. The mismatch is older than this
series (the pre-existing SHMEM_F_MAPPING_FROZEN bit has it too), so v3
starts with a small preparatory patch that rejects kernel-private
(S_PRIVATE) shmem inodes; real memfds are created without that flag. The
other two patches are unchanged from v2.
Patch 1 is the new one: can_preserve() now also requires
!IS_PRIVATE(inode). shmem_kernel_file_setup() is what marks kernel-private
objects, which covers SysV SHM, big_key payloads, the XFS buffer and scrub
files and the anonymous /dev/zero mapping file, so the check drops exactly
those. It deliberately does not narrow the handler to memfd_create()
objects: unlinked non-private shmem files stay eligible, they are simply
not part of the race, because SHMEM_F_LOCKED is only maintained for SysV
segments.
Patch 2 carries the user/group/other execute bits in the record and takes
the final sample -- execute mask, seals and file position -- in freeze()
under the inode lock, freezing that state against shmem_setattr(),
shmem_set_acl() and memfd_add_seals() until the transition is committed or
rolled back, and it rejects records with flag bits this version does not
know. The record version becomes "memfd-v3" and a read-only "memfd-v2"
reader is registered next to it.
Patch 3 releases the preserved folios when retrieve() fails, by sharing the
cleanup with memfd_luo_finish(). It predates the exec-bit work and carries
a Fixes tag of its own.
Changes since v2:
- new patch 1/3: reject S_PRIVATE shmem inodes in can_preserve(), which
removes the handler's exposure to the flags read-modify-write race with
SHMEM_F_LOCKED (reported by Sashiko AI review)
- patch 2/3 and 3/3 are unchanged from v2
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.
- 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.
- Release the preserved folios when retrieve() fails (now patch 3/3).
- 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.
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 and 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, on a kernel built from this revision: 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 the exec-bit change: "execute
mask changed across kexec: 0111 != 0100"
- the admission patch, measured on the same tree with
CONFIG_CHECKPOINT_RESTORE and CONFIG_SYSVIPC enabled so that a SysV
segment descriptor is reachable (memfd_luo is the only file handler
registered in that configuration):
kernel SysV SHM fd memfd unlinked tmpfs
without patch 1/3 accepted (0) accepted accepted
with patch 1/3 -ENOENT (errno 2) accepted accepted
the segment descriptor comes from /proc/self/map_files and reads back as
"/SYSV00000000 (deleted)"; the memfd and the unlinked tmpfs file are the
controls that show the ioctl path still works and that the check is
limited to kernel-private files
- 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 (3):
mm/memfd_luo: reject kernel-private shmem files
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 | 247 +++++++++++++++---
mm/shmem.c | 35 ++-
.../selftests/liveupdate/luo_kexec_simple.c | 208 +++++++++++++++
6 files changed, 496 insertions(+), 44 deletions(-)
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v3 1/3] mm/memfd_luo: reject kernel-private shmem files
2026-10-14 3:52 [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Haolong Wang
@ 2026-10-14 3:52 ` Haolong Wang
2026-10-14 3:52 ` [PATCH v3 2/3] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
` (2 subsequent siblings)
3 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
memfd_luo_can_preserve() accepts any unlinked shmem file. That includes
SysV SHM files, which are created with shmem_kernel_file_setup() and
marked S_PRIVATE: a privileged process can obtain a file descriptor for
an attached segment through /proc/<pid>/map_files and hand that
descriptor to LUO.
The preservation path then updates SHMEM_I(inode)->flags under the inode
lock when it sets SHMEM_F_MAPPING_FROZEN, while SHM_LOCK and SHM_UNLOCK
update the same word through shmem_lock(), serialized by
ipc_lock_object() instead. Two read-modify-write cycles on one word with
no common lock can lose either the LUO frozen bit or SHMEM_F_LOCKED, so
the frozen state the handler depends on may quietly not be in effect.
The seal-state freeze this series adds (SHMEM_F_SEAL_STATE_FROZEN) has
the same synchronization requirement.
Reject S_PRIVATE shmem inodes from the memfd LUO handler. Real memfds
are created with shmem_file_setup() without S_PRIVATE, while
shmem_kernel_file_setup() deliberately uses the flag for kernel-private
objects: SysV SHM, big_key payloads, the XFS buffer and scrub files, and
the anonymous /dev/zero mapping file. Those must not be preserved as
memfds, and dropping them also removes the only concurrent writer of that
flags word from the handler's reach, so no shmem-wide locking change is
needed here.
The predicate is deliberately not narrowed to memfd_create() objects:
unlinked non-private shmem files stay eligible. SHMEM_F_LOCKED is only
maintained for SysV segments, so they do not share this race and keep
being preserved as before.
Measured with QEMU runs on this tree, with CONFIG_CHECKPOINT_RESTORE and
CONFIG_SYSVIPC enabled and liveupdate=on, so that a descriptor for a
segment is reachable; memfd_luo is the only file handler registered in
that configuration:
kernel SysV SHM fd memfd unlinked tmpfs
without this patch accepted (0) accepted accepted
with this patch -ENOENT (errno 2) accepted accepted
The SysV descriptor came from /proc/self/map_files and reads back as
"/SYSV00000000 (deleted)"; the memfd and the unlinked tmpfs file are the
controls that show the ioctl path still works and that the check is
limited to kernel-private objects. The SysV case is the same guarantee
that the pre-existing SHMEM_F_MAPPING_FROZEN bit relies on, so the
mismatch is older than this series; atomic flag updates or a shared lock
would be a change across shmem and ipc, and this patch removes the
handler's exposure to it instead.
Fixes: b3749f174d68 ("mm: memfd_luo: allow preserving memfd")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/all/sashiko-outbox-170776@kernel.org/
Assisted-by: Pi:glm-5.3-flash
Signed-off-by: Haolong Wang <18766350203@163.com>
---
mm/memfd_luo.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/mm/memfd_luo.c b/mm/memfd_luo.c
index 59de210bee5f..6f8b84530995 100644
--- a/mm/memfd_luo.c
+++ b/mm/memfd_luo.c
@@ -582,7 +582,12 @@ static bool memfd_luo_can_preserve(struct liveupdate_file_handler *handler,
{
struct inode *inode = file_inode(file);
- return shmem_file(file) && !inode->i_nlink;
+ /*
+ * shmem_kernel_file_setup() marks kernel-private files S_PRIVATE. In
+ * particular, SysV SHM updates SHMEM_I(inode)->flags under its IPC lock,
+ * which does not serialize with the inode lock used by LUO.
+ */
+ return shmem_file(file) && !IS_PRIVATE(inode) && !inode->i_nlink;
}
static unsigned long memfd_luo_get_id(struct file *file)
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v3 2/3] mm/memfd_luo: carry the sealed exec-bit state in the preserved record
2026-10-14 3:52 [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Haolong Wang
2026-10-14 3:52 ` [PATCH v3 1/3] mm/memfd_luo: " Haolong Wang
@ 2026-10-14 3:52 ` Haolong Wang
2026-10-14 3:52 ` [PATCH v3 3/3] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
2026-10-14 9:43 ` [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Pratyush Yadav
3 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
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 6f8b84530995..333502ea3735 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));
@@ -597,6 +693,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,
@@ -611,15 +708,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 v3 3/3] mm/memfd_luo: release preserved folios when retrieve fails
2026-10-14 3:52 [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Haolong Wang
2026-10-14 3:52 ` [PATCH v3 1/3] mm/memfd_luo: " Haolong Wang
2026-10-14 3:52 ` [PATCH v3 2/3] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
@ 2026-10-14 3:52 ` Haolong Wang
2026-10-14 9:43 ` [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Pratyush Yadav
3 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
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 333502ea3735..f1f07d90f73d 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 v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files
2026-10-14 3:52 [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Haolong Wang
` (2 preceding siblings ...)
2026-10-14 3:52 ` [PATCH v3 3/3] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
@ 2026-10-14 9:43 ` Pratyush Yadav
2026-10-14 10:01 ` Haolong Wang
3 siblings, 1 reply; 6+ messages in thread
From: Pratyush Yadav @ 2026-10-14 9:43 UTC (permalink / raw)
To: Haolong Wang
Cc: Pasha Tatashin, Mike Rapoport, Pratyush Yadav, Andrew Morton,
kexec, linux-mm, linux-kernel
Hi,
I refuse to review this series. It looks entirely LLM generated with no
quality controls at all. LLM generated content is fine, but you can't
just post low quality patches and expect humans to spend time reviewing
them.
The commit messages are unreadable LLM ramblings, the code is all over
the place and makes very little sense, you have made 3 versions in the
same day, your cover letter doesn't even explain your use case and why
you make this change, and so on.
Future postings with similar quality will also be ignored.
Please spend time to understand the kernel development process and spend
_your own_ time understanding your code and polishing it.
[...]
--
Regards,
Pratyush Yadav
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files
2026-10-14 9:43 ` [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Pratyush Yadav
@ 2026-10-14 10:01 ` Haolong Wang
0 siblings, 0 replies; 6+ messages in thread
From: Haolong Wang @ 2026-10-14 10:01 UTC (permalink / raw)
To: Pasha Tatashin, Mike Rapoport, Pratyush Yadav
Cc: Andrew Morton, kexec, linux-mm, linux-kernel
Hi Pratyush,
Understood, and sorry for the noise. The Assisted-by: trailers are
accurate: I used an AI assistant to draft these patches, and the quality
problem is mine -- I did not review the output carefully enough. I am
withdrawing the series and will not post another revision until I can explain
every line myself and have a much smaller patch with a clear use case.
Please disregard v1-v3.
Thanks for taking the time to say it plainly.
Haolong Wang
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-14 10:02 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:52 [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Haolong Wang
2026-10-14 3:52 ` [PATCH v3 1/3] mm/memfd_luo: " Haolong Wang
2026-10-14 3:52 ` [PATCH v3 2/3] mm/memfd_luo: carry the sealed exec-bit state in the preserved record Haolong Wang
2026-10-14 3:52 ` [PATCH v3 3/3] mm/memfd_luo: release preserved folios when retrieve fails Haolong Wang
2026-10-14 9:43 ` [PATCH v3 0/3] mm/memfd_luo: preserve the sealed exec-bit state; reject kernel-private shmem files Pratyush Yadav
2026-10-14 10:01 ` Haolong Wang
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.