[PATCH] mm/memfd_luo: carry the exec-bit state in the preserved record
Haolong Wang
18766350203 at 163.com
Tue Oct 13 20:12:35 PDT 2026
Thanks for the review. Both findings were real.
On the exec-mode snapshot: the sample itself was atomic -- the mode is
read under inode_lock, in the same critical section that sets
SHMEM_F_MAPPING_FROZEN -- but it was taken far too early. The frozen
mapping only rejects growing, shrinking and fallocate(); fchmod() of the
exec bits (until F_SEAL_EXEC is set) and F_ADD_SEALS are not checked on
that path at all, and freeze() refreshed only f_pos. v2 closes this from
both ends: freeze() takes the final sample of the exec mask and the seals
under the inode lock and, in the same critical section, sets a new
SHMEM_F_SEAL_STATE_FROZEN inode flag before dropping the lock. From
that point on shmem_setattr() rejects exec-bit changes, memfd_add_seals()
rejects new seals, and shmem_set_acl() rejects access-ACL updates. The
flag is cleared by a new .unfreeze callback when the update is rolled
back, and defensively by unpreserve().
Two more things fell out of that work:
- A single "some exec bit was set" flag was not enough: an execute mask
of 0100 (mode 0700) came back as 0111, which is the inversion this
series is about. The record now carries the three execute bits
separately, retrieve() restores the exact mask and rejects unknown flag
bits, and luo_kexec_simple.c round-trips an execute mask of 0100 with
its full 0x3e seal set, plus a memfd without execute bits
(MFD_NOEXEC_SEAL: mask 0000, seal set 0x20, still writable), which is
the state the original report was about.
- An access ACL rewrites inode->i_mode without going through
shmem_setattr(), so the frozen state has to cover that path too.
Scope note: F_SEAL_EXEC itself is still bypassable by an access ACL
write (posix_acl_update_mode() updates the mode directly, with no seal
check anywhere on that path). That is a pre-existing
F_SEAL_EXEC/vm.memfd_noexec issue, deliberately not part of this
series; I will report it separately with a minimal repro.
On the folio leak: also real. retrieve() now releases the preserved
folios on every failure path by sharing the cleanup with
memfd_luo_finish(). The folio array is restored before the file is
allocated, so the later error paths (memfd_alloc_file(),
memfd_add_seals()) can release the folios directly instead of allocating
memory while already handling a failure, and memfd_luo_retrieve_folios()
takes ownership as soon as it runs, so nothing is released twice. It
predates the exec-bit work, so it is a separate commit with a Fixes tag.
Two limitations 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 before consuming anything (for example
its own kvmalloc() fails), the folio array cannot be walked at all and
those folios stay reserved. That API is not failure-atomic and cannot
be retried after a partial failure, so it needs a KHO-side fix or an
allocation-free discard/iterator API.
On record versions: the record is now written as "memfd-v3", and a
read-only "memfd-v2" handler is registered next to it. The compatible
strings are matched exactly, so the matrix is:
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
Rows two and four are both behaviour changes, and neither direction is
interoperable: an executable memfd preserved by an older kernel comes back
without execute bits (row two -- the safe reading; the old reader never
looked at the flags, it restored the default executable mode), and a new
record handed to an older kernel finds no matching handler at all, so the
deserialization fails with -ENOENT instead of the memfd being restored as
executable again (row four, which is why the version is bumped). The
legacy handler only reads records written by an older kernel: its
.can_preserve() always returns false, so an outgoing preservation is
always serialized as "memfd-v3".
v2 will follow as a separate thread.
Both changes are covered by a QEMU live-update run (preserve -> kexec ->
retrieve) with the liveupdate selftest: the memfd with execute mask 0100
and seal set 0x3e round-trips exactly, the memfd without execute bits
comes back with mask 0000, seal set 0x20 and still writable, and the same
test against a kernel without the change reports "execute mask changed
across kexec: 0111 != 0100". The numbers are in the v2 cover letter.
More information about the kexec
mailing list